【问题标题】:Why is Action<Action<T>> covariant?为什么 Action<Action<T>> 是协变的?
【发布时间】:2013-05-09 15:48:16
【问题描述】:

这是我很难理解的事情。我知道Action&lt;T&gt; 是逆变的,可能是这样声明的。

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

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

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

如果“Derived -> Base”对替换为“Action -> Action”对,也是一样的自然。

【问题讨论】:

    标签: c# .net covariance contravariance


    【解决方案1】:

    好的,首先让我们弄清楚Action&lt;Action&lt;T&gt;&gt;协变 的意思。您的意思是以下陈述成立:

    • 如果可以将引用类型 X 的对象分配给引用类型 Y 的变量,则可以将 Action&lt;Action&lt;X&gt;&gt; 类型的对象分配给引用类型 Action&lt;Action&lt;Y&gt;&gt; 的变量。

    好吧,让我们看看这是否有效。假设我们有类 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&lt;Animal&gt;:

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

    然后会发生什么?我们将委托传递给Meta,它用一条新鱼调用委托,然后我们喂鱼。没问题。

    T 仍然不在输出位置。如果有人可以尝试解释其背后的推理/逻辑,我将不胜感激。

    “输入/输出”位置的东西是助记符;协变类型 tend 将 T 置于输出位置,而逆变类型 tend 将 T 置于输入位置,但这并非普遍适用。在大多数情况下,这是真的,这就是我们选择inout 作为关键字的原因。但真正重要的是类型只能以类型安全的方式使用。

    这是另一种思考方式。协方差保留箭头的方向。你画一个箭头string --&gt; object,你可以画出“相同”的箭头IEnumerable&lt;string&gt; --&gt; IEnumerable&lt;object&gt;。逆变反转箭头的方向。这里的箭头是X --&gt; 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#

    【讨论】:

    • 这很有意义。感谢您澄清输入/输出位置的东西不是判断类型差异的明确方法。我想我对此有点困惑。我也很喜欢箭头的比喻。
    • @Tejas:这不是类比;这实际上就是在范畴论中定义协方差的方式。范畴论有两种基本的事物“对象”和连接成对对象的“箭头”。协变函数是保留相关对象对之间箭头方向的函数。
    • 嗯我不知道。好吧,我要去阅读您的博客系列,希望这可以消除我对方差的剩余疑虑/误解。再次感谢。
    【解决方案2】:

    考虑这个例子:

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

    【讨论】:

    • 是的,我理解将类型包含在 Action 中的“规则”会翻转差异。但就像我说的,我不明白这条规则背后的逻辑。
    • @Tejas 你的意思是你不知道为什么像in 这样的逆变器必须翻转方差?
    • 嗯,Eric Lippert 解释得很好,但我很困惑为什么每次你在 Action 中包装一个类型时,它会翻转差异。您从纯粹的“数学”角度解释它做得很好。我一直在寻找关于为什么会发生这种情况的更详细的解释。不过还是谢谢你的回答!
    【解决方案3】:

    考虑一下:

    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&lt;Action&lt;Derived&gt;&gt; 期望 domore() 存在,但 Base 仅传递 dosth()

    【讨论】:

      【解决方案4】:

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

      object -&gt; Base -&gt; Child

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

      object -&gt; IEnumerable&lt;object&gt; -&gt; IEnumerable&lt;Base&gt; -&gt; IEnumerable&lt;Child&gt;

      object -&gt; IEnumerable&lt;object&gt; -&gt; IEnumerable&lt;IEnumerable&lt;object&gt; -&gt; ...

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

      object -&gt; Action&lt;Child&gt; -&gt; Action&lt;Base&gt; -&gt; Action&lt;object&gt;

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

      object -&gt; Action&lt;Action&lt;object&gt;&gt; -&gt; Action&lt;Action&lt;Base&gt;&gt; -&gt; Action&lt;Action&lt;Child&gt;&gt; -&gt; Action&lt;object&gt;

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

      【讨论】:

        猜你喜欢
        • 2011-09-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-13
        • 2012-09-09
        • 1970-01-01
        相关资源
        最近更新 更多