为什么操作<操作<T>>协变?



这是我很难理解的事情。我知道Action<T>是逆变的,并且可能被宣布为逆变。

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

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

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

如果将"派生的 -> Base"对替换为"操作 -> 操作"对,则自然也是如此。

好的,所以首先让我们明确一下你说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 to 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绕在两侧会反转箭头的方向;这就是"逆变"的意思:随着类型的变化,箭头朝相反的方向前进。显然,两次反转箭头的方向与保留箭头的方向是一回事。

延伸阅读:

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

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

最近关于编译器如何将方差确定为类型安全的问题:

C# 中的差异规则

考虑这个例子:

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 多态性,而逆变赋值则向后工作。

考虑一下:

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

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

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>

附言逆变,对象层次结构不再是一棵树,而实际上是一个有向无环图

相关内容

  • 没有找到相关文章

最新更新