Action的通用约束无法按预期工作

我无法理解为什么以下代码段不会给我一个错误

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

但是这个,我期望由于generics类型约束而工作

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

给出了这个错误

 cannot convert from 'System.Action' to 'System.Action' 

我正在使用VS2012 sp1和.NET 4.5。

任何人都可以解释为什么约束不允许这个编译?

类和委托不是一回事。 System.Action表示具有MyInterface类型的单个参数的函数,而System.Action表示具有类型T : MyInterface的参数的方法T : MyInterface 。 函数签名是不兼容的, TMyInterface的衍生物并不相关,只有当T恰好是MyInterface ,签名才会兼容。

这是一个逆转问题 – 一个Action应该能够将任何MyInterface实例作为参数,但是你试图存储一个Action ,其中TMyInterface某个子类型,这是不安全的。

例如,如果你有:

 public class SomeImpl : MyInterface { } public class SomeOtherImpl : MyInterface { } List> list; list.Add(new Action(i => { })); ActionMyInterface act = list[0]; act(new SomeOtherImpl()); 

如果类型T比类型U “小”,则只能将Action分配给某个Action 。 例如

 Action act = new Action(o => { }); 

是安全的,因为字符串参数在对象参数所在的位置始终有效。

我发现在这些情况下,如果允许这种行为,可以考虑出现问题。 所以让我们考虑一下。

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

那会发生什么? 我们创建了一个代表老虎和扑克的委托,将其传递给Subscribe ,将其转换为Action ,然后传递长颈鹿,然后猛扑。

显然这必须是非法的。 唯一明智的做法是将Action转换为Action 。 这就是非法的地方。

where T: MyInterface约束意味着“实现MyInterface的任何类或结构的任何实例”。

所以你要做的事情可以简化为:

 Action listAction = null; Action enumAction = listAction; 

哪个不应该工作,而仍然是IList : IEnumerable 。 更多详情可在这找到:

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

因此,如果你真的需要使用generics而不仅仅是接口 – 你可以这样做,虽然它增加了复杂性和轻微的性能问题:

 public static IDisposable Subscribe(Action callback) where T : MyInterface { myActionList.Add(t => callback((T)t)); // this compiles and work return null; } 

类和委托的行为略有不同。 让我们看一个简单的例子:

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

在这个方法中你可以假设T至少是MyInterface ,所以你可以做这样的事情MyInterface e = arg; 因为args总是可以转换为MyInterface

现在让我们看看代理的行为:

 public class BaseClass { }; public class DerivedClass : BaseClass { }; private readonly IList> myActionList = new List>(); public void Subscribe(Action callback) where T: BaseClass { myActionList.Add(callback); // so you could add more 'derived' callback here Action return null; } 

现在我们将DerivedClass回调添加到myActionList,然后在某处调用委托:

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

但是你不能这样做,因为如果你有DerivedClass回调,你必须将它传递给DerivedClass作为参数。

这个问题涉及协方差和逆变 。 你可以从这篇文章中读到关于方差的文章,Eric Lippert也有关于方差的非常有趣的文章, 这是第一篇文章,你可以在他的博客中找到其余文章。

PS编辑了李评论。

如果T无论如何都限于某个接口,你可以直接使用该接口:

 public void SomeMethod(MyInterface arg) { MyInterface e = arg; } private readonly IList> myActionList = new IList>(); public IDisposable Subscribe(Action callback) { myActionList.Add(callback); // does compile return null } 

将工作和编译,几乎与你现在的相同。

如果你想在类型上执行相同的操作REGARDLESS,那么generics是很有用的,如果你将类型限制为某些接口,你已经打败了generics的目的,而应该只使用那个接口。