【问题标题】:Covariance and Contravariance on the same type argument同一类型参数的协变和逆变
【发布时间】:2010-12-24 20:22:52
【问题描述】:

C# 规范规定参数类型不能同时是协变和逆变的。

这在创建协变或逆变接口时很明显,您分别用“out”或“in”装饰类型参数。没有同时允许两者的选项(“outin”)。

这种限制仅仅是一种特定于语言的约束,还是存在基于类别理论的更深层次、更根本的原因,使您不希望您的类型既是协变的又是逆变的?

编辑:

我的理解是数组实际上既是协变的又是逆变的。

public class Pet{}
public class Cat : Pet{}
public class Siamese : Cat{}
Cat[] cats = new Cat[10];
Pet[] pets = new Pet[10];
Siamese[] siameseCats = new Siamese[10];

//Cat array is covariant
pets = cats; 
//Cat array is also contravariant since it accepts conversions from wider types
cats = siameseCats; 

【问题讨论】:

  • 我对你的最后一句话感到困惑;这是怎么证明逆变的? “Siamese”是比“Cat”的类型,就像“Cat”是比“Pet”窄的类型一样。
  • 所有的猫都是宠物,所有的暹罗猫都是猫,所以这只证明了协方差。
  • 埃里克——你说得对,这没有任何意义。
  • 在某些情况下,数组是逆变的,就像它们是协变的一样,但是对于写入而不是读取,即如果例程需要一个可以存储 Derived 的数组,给定一个 Base 数组,它就有可能工作。实现这一目标的最佳方法可能是数组实现 IReadableByIndex,这将是协变的,IWritableByIndex,它将是逆变的,因此只需要读取或写入数组的例程可以使正确的选择。
  • 顺便说一句,如果数组对象支持带有方法 CompareAt(int index1, int index2) 和 SwapAt(int index1, int index2)。

标签: c# covariance contravariance variance


【解决方案1】:

正如其他人所说,泛型类型既是协变的又是逆变的在逻辑上是不一致的。到目前为止,这里有一些很好的答案,但让我再补充两个。

首先,请阅读我关于方差“有效性”主题的文章:

http://blogs.msdn.com/b/ericlippert/archive/2009/12/03/exact-rules-for-variance-validity.aspx

根据定义,如果一个类型是“协变有效的”,那么它不能以逆变方式使用。如果它是“逆变有效的”,那么它不能以协变方式使用同时协变有效和逆变有效的东西不能以协变或逆变方式使用。也就是说,它是不变的。所以,协变和逆变的联合:它们的联合是不变的

其次,让我们假设您实现了自己的愿望,并且有一个类型注释可以按照我认为您想要的方式工作:

interface IBurger<in and out T> {}

假设您有一个IBurger&lt;string&gt;。因为它是协变的,所以可以转换为IBurger&lt;object&gt;。因为它是逆变的,所以它又可以转换为IBurger&lt;Exception&gt;,即使“字符串”和“异常”没有任何共同之处。基本上“进出”意味着IBurger&lt;T1&gt; 对于任何两个引用类型T1 和T2 都可以转换为any 类型IBurger&lt;T2&gt;这有什么用?你会用这样的功能做什么?假设您有一个IBurger&lt;Exception&gt;,但该对象实际上是一个IBurger&lt;string&gt;。你能用它做什么,既利用类型参数是异常的事实,又允许类型参数是一个完整的谎言,因为“真正的”类型参数是一个完全不相关的类型?

回答您的后续问题:涉及数组的隐式引用类型转换是协变的;它们是逆变的。你能解释一下为什么你错误地认为它们是逆变的吗?

【讨论】:

  • IBurger 示例正是我想要的。
  • 至于“你能解释为什么你错误地认为它们是逆变的”,在你指出它显然不是逆变之后。
  • @Eric Lippert:我在回答这个question 后找到了这个答案。我唯一想说的是,我理解并欣赏你的 In-N-Out Burger 笑话。
  • @Jason:很高兴你喜欢它,不过我会告诉你,我的幽默感的主要功能是取悦,这是一件好事。跨度>
  • @Eric Lippert:我知道你的意思。在我给她讲笑话后,我的伴侣不断对我说“我很高兴觉得这很有趣”。
【解决方案2】:

协变和逆变是相互排斥的。您的问题就像询问集合 A 是否既可以是集合 B 的超集又可以是集合 B 的子集。为了使集合 A 既是集合 B 的子集又是集合 B 的超集,集合 A 必须等于集合 B,那么你会问集合 A 是否等于集合 B。

换句话说,在同一个参数上要求协变和逆变就像要求根本没有方差(不变性),这是默认的。因此,不需要关键字来指定它。

【讨论】:

  • 嗯,那是可能的,它要求A完全等于B。换句话说,不变性。
  • Ben:谢谢,希望我已经澄清了。
  • 我认为不变性是指既不应用协变也不应用逆变。
  • 另外,数组不是既是协变的又是逆变的?如果是这样,那么两者并不相互排斥,因为该功能实际上存在于野外。
  • @William:数组是严格协变的。不变性是协变和逆变的交集。
【解决方案3】:

协方差对于您从未输入的类型是可能的(例如,成员函数可以将其用作返回类型或out 参数,但从不用作输入参数)。对于从不输出的类型(例如,作为输入参数,但从不作为返回类型或out 参数),逆变是可能的。

如果你让一个类型参数既是协变的又是逆变的,你不能输入也不能输出——你根本不能使用它。

【讨论】:

    【解决方案4】:

    没有 out 和 in 关键字参数是协变和逆变不是吗?

    in 表示参数只能作为函数参数类型

    out 表示参数只能作为返回值类型

    没有in和out表示可以作为参数类型,也可以作为返回值类型

    【讨论】:

    • 只要协变和逆变的交集是不变的,是的。我认为 OP 想要协变和逆变的结合,这是不可能的。
    【解决方案5】:

    这种限制仅仅是一种特定于语言的约束,还是存在基于类别理论的更深层次、更根本的原因,使您不希望您的类型既是协变的又是逆变的?

    不,有一个基于基本逻辑(或只是常识,随你喜欢)的更简单的原因:一个陈述不能同时为真或不为真。

    协方差表示S &lt;: T ⇒ G&lt;S&gt; &lt;: G&lt;T&gt;,逆变表示S &lt;: T ⇒ G&lt;T&gt; &lt;: G&lt;S&gt;。很明显,这些不可能同时为真。

    【讨论】:

    • 这对我来说并不明显。为什么这永远不可能是真的?是不是可以在集合上定义关系,例如 G <: g> 和 G >: G ?它们可能不是有用或有趣的关系,但我不明白为什么它们不可能
    • @Eric Lippert:你是对的,当然。在这种特定情况下,我将&lt;: 视为通常的子类型关系。一个类型不能同时是另一个类型的超类型和子类型,除非它们是 same 类型。 (至少,我看不出这在 C# 的类型系统中是如何工作的。)假设 ST 是不同的类型,我认为拥有 G&lt;S&gt;G&lt;T&gt; 并不可取是同一类型。
    • 嗯,类型系统中的实际关系是赋值兼容性,而不是subtyping;有细微的差别。在 CLR 类型系统中,int 和 uint 不是彼此的子类型,但它们是相互兼容的赋值。因为数组在 CLR 类型系统中是协变的,所以 int[] 和 uint[] 相互兼容,即使它们也不是子类型。在这种 G 是“创建数组类型”的特定情况下,恰好 G<: g>and G :> G 但 G != G.
    【解决方案6】:

    你可以用“协变”做什么?

    Covariant 使用修饰符out,表示该类型可以是方法的输出,但不能是输入参数。

    假设你有这些类和接口:

    interface ICanOutput<out T> { T getAnInstance(); }
    
    class Outputter<T> : ICanOutput<T>
    {
        public T getAnInstance() { return someTInstance; }
    }
    

    现在假设你有 TBig 继承 TSmall 的类型。这意味着TBig 实例也始终是TSmall 实例;但TSmall 实例并不总是TBig 实例。 (选择名称是为了便于将TSmall 放入TBig 中进行可视化)

    当你这样做时(一个经典的co变体赋值):

    //a real instance that outputs TBig
    Outputter<TBig> bigOutputter = new Outputter<TBig>();
    
    //just a view of bigOutputter
    ICanOutput<TSmall> smallOutputter = bigOutputter;
    
    • bigOutputter.getAnInstance() 将返回 TBig
    • 因为smallOutputter 被分配了bigOutputter
      • 在内部,smallOutputter.getAnInstance() 将返回 TBig
      • 并且TBig可以转换为TSmall
      • 转换完成,输出为TSmall

    如果它是相反的(好像它是相反变体):

    //a real instance that outputs TSmall
    Outputter<TSmall> smallOutputter = new Outputter<TSmall>();
    
    //just a view of smallOutputter
    ICanOutput<TBig> bigOutputter = smallOutputter;
    
    • smallOutputter.getAnInstance() 将返回 TSmall
    • 因为bigOutputter 被分配了smallOutputter
      • 在内部,bigOutputter.getAnInstance() 将返回 TSmall
      • 但是TSmall不能转换成TBig!!
      • 这是不可能的。

    这就是为什么contravariant”类型不能用作输出类型


    你可以用“逆变”做什么?

    同上思路,逆变使用修饰符in,表示类型可以是方法的输入参数,但不能作为输出参数。

    假设你有这些类和接口:

    interface ICanInput<in T> { bool isInstanceCool(T instance); }
    
    class Analyser<T> : ICanInput<T>
    {
        bool isInstanceCool(T instance) { return instance.amICool(); }
    }
    

    再次,假设类型TBig 继承TSmall。这意味着TBig 可以做TSmall 所做的所有事情(它拥有所有TSmall 成员等等)。但是TSmall 不能做TBig 所做的一切(TBig 有更多成员)。

    当你这样做时(经典的contra变体赋值):

    //a real instance that can use TSmall methods
    Analyser<TSmall> smallAnalyser = new Analyser<TSmall>();
        //this means that TSmall implements amICool
    
    //just a view of smallAnalyser
    ICanInput<TBig> bigAnalyser = smallAnalyser;
    
    • smallAnalyser.isInstanceCool:
      • smallAnalyser.isInstanceCool(smallInstance)可以使用smallInstance中的方法
      • smallAnalyser.isInstanceCool(bigInstance) 也可以使用方法(它只查看TBigTSmall 部分)
    • 因为bigAnalyser 被分配了smallAnalyer
      • 完全可以拨打bigAnalyser.isInstanceCool(bigInstance)

    如果是相反的(好像它是co变体):

    //a real instance that can use TBig methods
    Analyser<TBig> bigAnalyser = new Analyser<TBig>();
        //this means that TBig has amICool, but not necessarily that TSmall has it    
    
    //just a view of bigAnalyser
    ICanInput<TSmall> smallAnalyser = bigAnalyser;
    
    • 对于bigAnalyser.isInstanceCool
      • bigAnalyser.isInstanceCool(bigInstance)可以使用bigInstance中的方法
      • 但是bigAnalyser.isInstanceCool(smallInstance)TSmall 中找不到TBig 方法!!!并且不能保证这个 smallInstance 甚至是 TBig 转换的。
    • 因为smallAnalyser 被分配了bigAnalyser
      • 调用smallAnalyser.isInstanceCool(smallInstance) 将尝试在实例中查找TBig 方法
      • 它可能找不到TBig 方法,因为这个smallInstance 可能不是TBig 实例。

    这就是为什么covariant”类型不能用作输入参数


    同时加入

    现在,当您将两个“不能”加在一起时会发生什么?

    • 不能这个+不能那个=什么都不能

    你能做什么?

    我还没有测试过这个(还......我在想我是否有理由这样做),但它似乎没问题,只要你知道你会有一些限制。

    如果您将仅输出所需类型的方法和仅将其作为输入参数的方法明确分离,则可以使用两个接口来实现您的类。

    • 一个接口使用in,并且只有不输出T的方法
    • 另一个使用out 的接口只有不以T 作为输入的方法

    在需要的情况下使用每个接口,但不要试图将一个分配给另一个。

    【讨论】:

      【解决方案7】:

      泛型类型参数不能同时是协变和逆变的。

      为什么?这与 inout 修饰符施加的限制有关。如果我们想让我们的泛型类型参数既是协变的又是逆变的,我们基本上会说:

      • 我们接口的所有方法都没有返回 T
      • 我们接口的所有方法都不接受 T

      这实质上会使我们的通用接口非通用。

      我在另一个question下详细解释过:

      【讨论】:

        猜你喜欢
        • 2013-11-29
        • 1970-01-01
        • 2012-04-07
        • 2017-05-13
        • 1970-01-01
        • 1970-01-01
        • 2020-01-25
        • 2013-02-17
        • 1970-01-01
        相关资源
        最近更新 更多