Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Action delegates, generics, covariance and contravariance

Tags:

c#

delegates

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" );
like image 755
Craig W. Avatar asked Aug 01 '11 21:08

Craig W.


People also ask

What is the difference between covariance and contravariance in delegates?

Covariance permits a method to have return type that is more derived than that defined in the delegate. Contravariance permits a method that has parameter types that are less derived than those in the delegate type.

What is covariance and contravariance in generics?

Covariance and contravariance are terms that refer to the ability to use a more derived type (more specific) or a less derived type (less specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types.

What is difference between covariance and contravariance?

In C#, covariance and contravariance enable implicit reference conversion for array types, delegate types, and generic type arguments. Covariance preserves assignment compatibility and contravariance reverses it.

What is covariance and contravariance in generics in Java?

Covariance can be translated as "different in the same direction," or with-different, whereas contravariance means "different in the opposite direction," or against-different. Covariant and contravariant types are not the same, but there is a correlation between them. The names imply the direction of the correlation.


1 Answers

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...

like image 149
Jon Skeet Avatar answered Nov 10 '22 02:11

Jon Skeet