Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic constraint for Action doesn't work as expected

I am having some trouble understanding why the following snippet does not give me an error

public void SomeMethod<T>(T arg) where T : MyInterface
{
  MyInterface e = arg;
}

But this one, which I would expect to work due to the generic type constraint

private readonly IList<Action<MyInterface>> myActionList = new List<Action<MyInterface>>();

public IDisposable Subscribe<T>(Action<T> callback) where T: MyInterface
{
  myActionList.Add(callback); // doesn't compile
  return null
}

Gives this error

cannot convert from 'System.Action<T>' to 'System.Action<MyInterface>'

I am using VS2012 sp1 and .NET 4.5.

Can anyone explain why the constraint does not allow this to compile?

like image 872
Jim Jeffries Avatar asked Mar 25 '13 08:03

Jim Jeffries


People also ask

Can generic classes be constrained?

Declaring those constraints means you can use the operations and method calls of the constraining type. If your generic class or method uses any operation on the generic members beyond simple assignment or calling any methods not supported by System. Object, you'll apply constraints to the type parameter.

What are the constraints in generics?

The where clause in a generic definition specifies constraints on the types that are used as arguments for type parameters in a generic type, method, delegate, or local function. Constraints can specify interfaces, base classes, or require a generic type to be a reference, value, or unmanaged type.

What does the generic constraint of type interface do?

Interface Type Constraint You can constrain the generic type by interface, thereby allowing only classes that implement that interface or classes that inherit from classes that implement the interface as the type parameter.


5 Answers

This is a contravariance issue - an Action<MyInterface> should be able to take any MyInterface instance as an argument, however you are trying to store an Action<T> where T is some subtype of MyInterface, which is not safe.

For example if you had:

public class SomeImpl : MyInterface { }
public class SomeOtherImpl : MyInterface { }
List<Action<MyInterface>> list;

list.Add(new Action<SomeImpl>(i => { }));
ActionMyInterface act = list[0];
act(new SomeOtherImpl());

You can only assign an Action<T> to some Action<U> if the type T is 'smaller' than the type U. For example

Action<string> act = new Action<object>(o => { });

is safe since a string argument is always valid where an object argument is.

like image 130
Lee Avatar answered Oct 08 '22 13:10

Lee


Classes and delegates are not the same thing. System.Action<MyInterface> represents a function with a single parameter of type MyInterface whilst System.Action<T> represents a method with a parameter of type T : MyInterface. The function signatures are not compatible, it is not relevent that T is a derivative of MyInterface, the signature would only be compatible if T was exactly MyInterface.

like image 37
odyss-jii Avatar answered Oct 08 '22 15:10

odyss-jii


The where T: MyInterface constraint means "any instance of any class or struct which implements MyInterface".

So what you are trying to do can be simplified as this:

Action<IList> listAction = null;
Action<IEnumerable> enumAction = listAction;

Which is not supposed to work, while still IList : IEnumerable. More details can be found here:

http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx http://msdn.microsoft.com/en-us/library/dd799517.aspx

So if you really need to use generic and not just interface - you can do it like this, though it adds complexity and minor performance issues:

public static IDisposable Subscribe<T>(Action<T> callback) where T : MyInterface
{
    myActionList.Add(t => callback((T)t)); // this compiles and work
    return null;
}
like image 31
Lanorkin Avatar answered Oct 08 '22 13:10

Lanorkin


I find it helpful in these situations to consider what goes wrong if you allow the behaviour. So let's consider that.

interface IAnimal { void Eat(); }
class Tiger : IAnimal 
{ 
  public void Eat() { ... }
  public void Pounce() { ... } 
}
class Giraffe : IAnimal 
...
public void Subscribe<T>(Action<T> callback) where T: IAnimal
{
   Action<IAnimal> myAction = callback; // doesn't compile but pretend it does.
   myAction(new Giraffe()); // Obviously legal; Giraffe implements IAnimal
}
...
Subscribe<Tiger>((Tiger t)=>{ t.Pounce(); });

So what happens? We create a delegate that takes a tiger and pounces, pass that to Subscribe<Tiger>, convert that to Action<IAnimal>, and pass a giraffe, which then pounces.

Obviously that has to be illegal. The only place where it is sensible to make it illegal is the conversion from Action<Tiger> to Action<IAnimal>. So that's where it is illegal.

like image 31
Eric Lippert Avatar answered Oct 08 '22 14:10

Eric Lippert


Classes and delegates behave a little different. Let's see a simple example:

public void SomeMethod<T>(T arg) where T : MyInterface
{
  MyInterface e = arg;
}

In this method you could assume that T would be at least MyInterface, so you could do something like this MyInterface e = arg; because args always could be cast to MyInterface.

Now let's see how delegates behave:

public class BaseClass { };
public class DerivedClass : BaseClass { };
private readonly IList<Action<BaseClass >> myActionList = new List<Action<BaseClass>>();

public void Subscribe<T>(Action<T> callback) where T: BaseClass
{
  myActionList.Add(callback); // so you could add more 'derived' callback here Action<DerivedClass>
  return null;
}

Now we'r adding DerivedClass callback to myActionList and then somewhere you invoke delegates:

foreach( var action in myActionList ) {
   action(new BaseClass);
}

But you can't do that, because if you have DerivedClass callback you have to pass it DerivedClass as parameter.

This question refers to Covariance and contravariance. You could read about variance from this article, also Eric Lippert has very intersting articles about variance, this is the first article, you can find the rest in his blog.

P.S. Edited accorind to Lee comment.

like image 21
Andrew Avatar answered Oct 08 '22 15:10

Andrew