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

时间:2013-03-25 08:55:13

标签: c# .net generics covariance contravariance

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

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

但是这个,我希望由于泛型类型约束而起作用

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
}

给出此错误

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

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

有人可以解释为什么约束不允许这个编译吗?

6 个答案:

答案 0 :(得分:5)

这是一个逆转问题 - Action<MyInterface>应该可以将任何MyInterface个实例作为参数,但是您尝试将Action<T>存储在T的位置MyInterface的某个子类型,这是不安全的。

例如,如果你有:

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());

如果Action<T>类型比“{1}}类型小”,则只能为Action<U> T分配U。例如

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

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

答案 1 :(得分:3)

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

答案 2 :(得分:2)

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

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(); });

那会发生什么?我们创建了一个代表老虎和突袭的代表,将其传递给Subscribe<Tiger>,将其转换为Action<IAnimal>,然后传递长颈鹿然后突袭。

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

答案 3 :(得分:1)

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

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

Action<IList> listAction = null;
Action<IEnumerable> 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

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

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

答案 4 :(得分:1)

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

public void SomeMethod<T>(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<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;
}

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

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

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

这个问题涉及Covariance and contravariance。您可以从this文章中了解方差,Eric Lippert也有关于方差的非常有趣的文章,this是第一篇文章,您可以在他的博客中找到其余文章。

P.S。编辑李的评论。

答案 5 :(得分:0)

如果T仅限于某个界面,您可以直接使用该界面:

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

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

public IDisposable Subscribe(Action<MyInterface> callback)
{
  myActionList.Add(callback); // does compile
  return null
}

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

如果你想对类型的REGARDLESS做同样的操作,那么泛型是很有用的,如果你把类型限制到某个接口你已经打败了泛型的目的,那么应该只使用那个接口。