为什么Action <action <t>&gt;协变</动作<T>

时间:2013-05-09 15:48:17

标签: c# .net covariance contravariance

这是我很难绕过头脑的事情。我知道Action<T>是逆变的,可能是这样的。

internal delegate void Action<in T>(T t);

但是,我不明白为什么 Action<Action<T>>是协变的。 T仍未处于输出位置。如果有人能够尝试解释这背后的推理/逻辑,我真的很感激。

我挖了一下,发现this博客文章试图解释它。特别是,我并没有完全遵循“输入协方差解释”小节中的含义。

  

如果“Derived - &gt;基础“对被替换为”动作 - &gt;行动“对。

4 个答案:

答案 0 :(得分:22)

好的,首先让我们说清楚你的意思是说Action<Action<T>>协变。您的意思是以下声明成立:

  • 如果可以将引用类型X的对象分配给引用类型为Y的变量,则可以将类型为Action<Action<X>>的对象分配给引用类型为Action<Action<Y>>的变量。

好吧,让我们看看是否有效。假设我们的类FishAnimal具有明显的继承。

static void DoSomething(Fish fish)
{
    fish.Swim();
}

static void Meta(Action<Fish> action)
{
    action(new Fish());
}

...

Action<Action<Fish>> aaf = Meta;
Action<Fish> af = DoSomething;
aaf(af);

那是做什么的?我们将代表传递给DoSomething到Meta。这创造了一条新的鱼,然后DoSomething让鱼游泳。没问题。

到目前为止一切顺利。现在的问题是,为什么这是合法的?

Action<Action<Animal>> aaa = aaf;

好吧,让我们看看如果我们允许它会发生什么:

aaa(af);

会发生什么?和以前一样,很明显。

我们可以在这里搞错吗?如果我们将af以外的内容传递给aaa,请记住这样做会将其传递给Meta

那么,我们可以传递给aaa什么?任何Action<Animal>

aaa( (Animal animal) => { animal.Feed(); } );

会发生什么?我们将代表传递给Meta,用新鱼调用代表,我们喂鱼。没问题。

  

T仍未处于输出位置。如果有人能够尝试解释这背后的推理/逻辑,我真的很感激。

“输入/输出”位置是一个助记符;协变类型趋势使T在输出位置和逆变类型趋向使T在输入位置,但这不是普遍正确的。对于大多数情况,这是正确的,这就是我们选择inout作为关键字的原因。但真正重要的是这些类型只能以类型安全的方式使用。

这是考虑它的另一种方式。协方差保留箭头的方向。您绘制箭头string --> object,可以绘制“相同”箭头IEnumerable<string> --> IEnumerable<object>。逆变反转箭头的方向。这里的箭头是X --> Y,意味着对X的引用可以存储在Y类型的变量中:

Fish                         -->     Animal  
Action<Fish>                 <--     Action<Animal> 
Action<Action<Fish>>         -->     Action<Action<Animal>>
Action<Action<Action<Fish>>> <--     Action<Action<Action<Animal>>>
...

看看它是如何工作的?围绕两侧的Action 反转箭头的方向;这就是“逆变”意味着:随着类型的变化,箭头会进入 contra - 反向 - 方向。显然,反转箭头方向两次保留箭头方向是一回事。

进一步阅读:

我在设计功能时写的博客文章。从底部开始:

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/default.aspx

最近一个关于如何确定方差是否属于类型安全的问题:

Variance rules in C#

答案 1 :(得分:3)

考虑这个例子:

string s = "Hello World!";
object o = s; // fine

如果我们用Action

包裹它
Action<string> s;
Action<object> o;
s = o; // reverse of T's polymorphism

这是因为in参数的效果是使泛型类型的可分配层次结构与类型参数的层次结构相反。例如,如果这是有效的:

TDerived t1; 
TBase t2; 
t2 = t1; // denote directly assignable without operator overloading

然后

Action<TDerived> at1; 
Action<TBase> at2; 
at1 = at2; // contravariant

有效。然后

Action<Action<TDerived>> aat1;
Action<Action<TBase>> aat2;
aat2 = aat1; // covariant

以及

Action<Action<Action<TDerived>>> aaat1;
Action<Action<Action<TBase>>> aaat2;
aaat1 = aaat2; // contravariant

等等。

http://msdn.microsoft.com/en-us/library/dd799517.aspx解释了与正常分配相比的效果以及协变和逆变的工作原理。简而言之,协变赋值的工作方式与普通的OOP多态性相同,逆向变换也可以向后工作。

答案 2 :(得分:1)

考虑一下:

class Base { public void dosth(); }
class Derived : Base { public void domore(); }

使用T的行动:

// this is all clear
Action<Base> a1 = x => x.dosth();
Action<Derived> b1 = a1;

现在:

Action<Action<Derived>> a = x => { x(new Derived()); };
Action<Action<Base>> b = a;
// the line above is basically this:
Action<Action<Base>> b = x => { x(new Derived()); };

哪种方法有效,因为您仍然可以将new Derived()的结果视为Base。这两个课程都可以dosth()

现在,在这种情况下:

Action<Action<Base>> a2 = x => { x(new Derived()); };
Action<Action<Derived>> b2 = x => { x(new Derived()); };

使用new Derived()时,它仍然可以使用。但是,这一般不能说,因此是非法的。考虑一下:

Action<Action<Base>> a2 = x => { x(new Base()); };
Action<Action<Derived>> b2 = x => { x(new Base()); };

错误:Action<Action<Derived>>期望domore()存在,但Base仅提供dosth()

答案 3 :(得分:1)

有一个继承树,以对象为根。树上的路径通常看起来像这样

object -> Base -> Child

树上较高类型的对象总是可以分配给树上较低类型的变量。泛型类型中的协方差意味着实现的类型以跟在树之后的方式相关

object -> IEnumerable<object> -> IEnumerable<Base> -> IEnumerable<Child>

object -> IEnumerable<object> -> IEnumerable<IEnumerable<object> -> ...

逆变量意味着实现的类型以反转树的方式相关。

object -> Action<Child> -> Action<Base> -> Action<object>

当你更深入时,你必须再次反转树

object -> Action<Action<object>> -> Action<Action<Base>> -> Action<Action<Child>> -> Action<object>

P.S。对于逆变,对象层次结构不再是树,而实际上是有向无环图