17

这是我很难理解的事情。我知道这Action<T>是逆变的,可能是这样声明的。

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

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

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

如果将“Derived -> Base”对替换为“Action -> Action”对,则同样自然。

4

4 回答 4

25

好的,首先让我们弄清楚你所说Action<Action<T>>covariant是什么意思。您的意思是以下陈述成立:

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

好吧,让我们看看这是否有效。假设我们有类Fish并且Animal具有明显的继承性。

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

发生什么了?显然和以前一样。

我们可以在这里出错吗?如果我们传递除了afto以外的东西怎么办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#中的方差规则

于 2013-05-09T16:31:59.843 回答
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 多态性类似,而逆变式则向后工作。

于 2013-05-09T16:25:03.363 回答
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().

于 2013-05-09T16:27:37.157 回答
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>

ps 带逆变,对象层次不再是树,实际上是有向无环图

于 2013-05-09T17:31:01.753 回答