Action delegates, generics, covariance and contrav

2019-01-23 06:45发布

I have two business contract classes:

public BusinessContract

public Person : BusinessContract

In another class I have the following code:

private Action<BusinessContract> _foo;

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = bar;
}

The above won't even compile, which baffles me a bit. I'm constraining T to be BusinessContract, so why doesn't the compiler know that bar can be assigned to _foo?

In trying to get around this, we tried changing it to the following:

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = (Action<BusinessContract>)bar;
}

Now the compiler is happy, so I write the following code elsewhere in my application:

Foo<Person>( p => p.Name = "Joe" );

And the app blows up with an InvalidCastException at run-time.

I don't get it. Shouldn't I be able to cast my more specific type to a less specific type and assign it?

UPDATE

Jon answered the question so got the nod for that, but just to close the loop on this, here's how we ended up solving the problem.

private Action<BusinessContract> _foo;

public void Foo<T>( Action<T> bar ) where T : BusinessContract
{
    _foo = contract => bar( (T)contract );
}

Why are we doing this? We have a Fake DAL we use for unit testing. With one of the methods we need to give the test developer the ability to specify what the method should do when it's called during the test (it's a refresh method that updates a cached object from the database). The purpose of Foo is to set what should happen when refresh is called. IOW, elsewhere in this class we have the following.

public void Refresh( BusinessContract contract )
{
    if( _foo != null )
    {
        _foo( contract );
    }
}

The test developer could then, for example, decide they wanted to set the name to a different value when Refresh was called.

Foo<Person>( p => p.Name = "New Name" );

标签: c# delegates
2条回答
Summer. ? 凉城
2楼-- · 2019-01-23 07:31

It cannot be assigned because since you are using a contravariant instead of a covariant, there is no way to guarantee that the generic type can be assigned to foo.

查看更多
小情绪 Triste *
3楼-- · 2019-01-23 07:37

You've got the covariance and contravariance the wrong way round. Let's consider Action<object> and Action<string>. Removing the actual generics, you're trying to do something like this:

private Action<object> _foo;

public void Foo(Action<string> bar)
{
    // This won't compile...
    _foo = bar;
}

Now suppose we then write:

_foo(new Button());

That's fine, because Action<object> can be passed any object... but we've initialized it with a delegate which must take a string argument. Ouch.

This isn't type safe, so doesn't compile.

The other way would work though:

private Action<string> _foo;

public void Foo(Action<object> bar)
{
    // This is fine...
    _foo = bar;
}

Now when we invoke _foo, we have to pass in a string - but that's fine, because we've initialized it with a delegate which can take any object reference as a parameter, so it's fine that we happen to be giving it a string.

So basically Action<T> is contravariant - whereas Func<T> is covariant:

Func<string> bar = ...;
Func<object> foo = bar; // This is fine
object x = foo(); // This is guaranteed to be okay

It's not clear what you're trying to do with the action, so unfortunately I can't really give any advice on how to get around this...

查看更多
登录 后发表回答