【问题标题】:What is the difference between covariance and contra-variance in programming languages? [closed]编程语言中的协变和逆变有什么区别? [关闭]
【发布时间】:2010-11-12 21:41:19
【问题描述】:

谁能解释一下协变和逆变的概念 编程语言理论?

【问题讨论】:

标签: java c# programming-languages covariance contravariance


【解决方案1】:

方差、协方差、逆变、不变性

类型(T)

composite data type - 是由另一种类型构建的类型。例如可以是泛型、容器(集合)、可选[example]

method's type - 方法的parameter type(前置条件)和return type(后置条件)[About]

差异

Variance - 关于分配兼容性。这是一种使用derived type 而不是 original type 的能力。这是不是parent-child关系

X(T) - `composite data type` or `method type` X, with type T

Covariance(same 子类型化方向) 你可以分配 更多 derived type 而不是 original type

X(T) covariantX(T1) is covariant to X(T2) 当关系 T1 到 T2 与 X(T1) 到 X(T2) 相同时

Contravariance(相反子类型方向) 你可以分配 less derived type 然后 original type

X(T) contravariantX(T1) is contravariant to X(T2) 当关系 T1 到 T2 与 X(T2) 到 X(T1) 相同时

Invariance 既不是 Covariance 也不是 Contravariance。也有[Class Invariant]

示例

类方向,伪代码

class A {}
class B: A {}
class C: B {}
A <- B <- C

Java 中的引用类型数组是协变的

A[] aArray = new A[2];
B[] bArray = new B[2];

//A - original type, B - more derived type
//B[] is covariant to A[]
aArray = bArray;
class Generic<T> { }

//A - original type, B - more derived type
//Generic<B> is covariant to Generic<A>
//assign more derived type(B) than original type(A)
Generic<? extends A> ref = new Generic<B>(); //covariant

//B - original type, A - less derived type
//Generic<B> is contravariant to Generic<A>
//assign less derived type(A) then original type(B)
Generic<? super B> ref = new Generic<A>(); //contravariant

<sub_type> covariant/contravariant to <super_type>

Swift 数组是协方差的

let array:[B] = [C()]

Swift 泛型是不变的

class Wrapper<T> {
    let value: T
    
    init(value: T) {
        self.value = value
    }
}
let generic1: Wrapper<A> = Wrapper<B>(value: B()) //ERROR: Cannot assign value of type 'Wrapper<B>' to type 'Wrapper<A>'
let generic2: Wrapper<B> = Wrapper<A>(value: A()) //ERROR: Cannot assign value of type 'Wrapper<A>' to type 'Wrapper<B>'

迅速关闭: 闭包的返回类型是协方差
闭包的参数类型是逆变

var foo1: (A) -> C = { _ in return C() }
let bFunction2: (B) -> B = foo1
subtyping direction 
A <- B <-C

covariance
same subtyping direction(C fits in B) 
array:[B] = [C()]
closure:() -> B = () -> C

contravariance
opposite subtyping direction(B fits in A) 
(A) -> Void = (B) -> Void

[Liskov principle]

[Java generics]

【讨论】:

    【解决方案2】:

    C# 和 CLR 都允许引用类型的协变和逆变: 将方法绑定到委托。协变意味着一个方法可以返回一个类型 派生自委托的返回类型。逆变意味着一个方法可以取一个 作为委托参数类型的基础的参数。例如,给定一个委托 定义如下:

    delegate Object MyCallback(FileStream s);
    

    可以构造一个绑定到原型方法的委托类型的实例

    像这样:

    String SomeMethod(Stream s);
    

    这里,SomeMethod 的返回类型(String)是派生自委托的返回类型(Object)的类型;这种协方差是允许的。 SomeMethod 的参数类型 (Stream) 是委托参数类型 (FileStream) 的基类;这种逆变是允许的。

    请注意,仅引用类型支持协变和逆变,值类型或 void 不支持。因此,例如,我无法将以下方法绑定到 MyCallback 委托:

    Int32 SomeOtherMethod(Stream s);
    

    尽管 SomeOtherMethod 的返回类型 (Int32) 派生自 MyCallback 的返回 type(Object),不允许这种形式的协方差,因为 Int32 是值类型。

    很明显,为什么值类型和void不能用于协方差和 逆变是因为这些事物的记忆结构不同,而 引用类型的内存结构始终是一个指针。幸运的是,C# 编译器将 如果您尝试执行不受支持的操作,则会产生错误。

    【讨论】:

      【解决方案3】:

      协变逆变是有区别的。
      非常粗略地说,如果一个操作保持类型的顺序,它就是协变的,如果它颠倒这个顺序,它就是逆变的。

      排序本身旨在将更一般的类型表示为大于更具体的类型。
      这是 C# 支持协方差的情况的一个示例。首先,这是一个对象数组:

      object[] objects=new object[3];
      objects[0]=new object();
      objects[1]="Just a string";
      objects[2]=10;
      

      当然可以在数组中插入不同的值,因为最终它们都派生自 .Net 框架中的System.Object。换句话说,System.Object 是一个非常通用的或 large 类型。现在这里有一个支持协方差的地方:
      将较小类型的值赋给较大类型的变量

      string[] strings=new string[] { "one", "two", "three" };
      objects=strings;
      

      object[] 类型的变量对象可以存储实际上是string[] 类型的值。

      想一想——在某种程度上,这是你所期望的,但事实并非如此。毕竟,虽然string 派生自object,但string[] 派生自object[]。此示例中对协方差的语言支持使赋值成为可能,这在许多情况下都会发现。 变体 是一种使语言更直观地工作的功能。

      围绕这些主题的考虑非常复杂。例如,根据前面的代码,这里有两种会导致错误的场景。

      // Runtime exception here - the array is still of type string[],
      // ints can't be inserted
      objects[2]=10;
      
      // Compiler error here - covariance support in this scenario only
      // covers reference types, and int is a value type
      int[] ints=new int[] { 1, 2, 3 };
      objects=ints;
      

      逆变的工作原理有点复杂。想象这些 两个类:

      public partial class Person: IPerson {
          public Person() {
          }
      }
      
      public partial class Woman: Person {
          public Woman() {
          }
      }
      

      Woman 显然是从Person 派生的。现在考虑你有这两个函数:

      static void WorkWithPerson(Person person) {
      }
      
      static void WorkWithWoman(Woman woman) {
      }
      

      其中一个函数使用Woman 执行某些操作(无关紧要),另一个更通用,可以使用从Person 派生的任何类型。在Woman 方面,您现在还拥有这些:

      delegate void AcceptWomanDelegate(Woman person);
      
      static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
          acceptWoman(woman);
      }
      

      DoWork 是一个可以接受Woman 的函数和对同样接受Woman 的函数的引用,然后它将Woman 的实例传递给委托。考虑您在此处拥有的元素的多态性Person大于WomanWorkWithPerson大于WorkWithWoman。 出于差异的目的,WorkWithPerson 也被认为大于AcceptWomanDelegate

      最后,你有了这三行代码:

      Woman woman=new Woman();
      DoWork(woman, WorkWithWoman);
      DoWork(woman, WorkWithPerson);
      

      创建了一个Woman 实例。然后调用 DoWork,传入Woman 实例以及对WorkWithWoman 方法的引用。后者显然与委托类型AcceptWomanDelegate 兼容——Woman 类型的一个参数,没有返回类型。 不过,第三行有点奇怪。方法WorkWithPersonPerson 作为参数,而不是Woman,正如AcceptWomanDelegate 所要求的那样。不过,WorkWithPerson 与委托类型兼容。 逆变 使之成为可能,因此在委托的情况下,较大的类型 WorkWithPerson 可以存储在较小类型的变量 AcceptWomanDelegate 中。这又是直观的事情:如果WorkWithPerson 可以与任何Person 一起使用,那么传入Woman 不会错,对吧?

      现在,您可能想知道这一切与泛型有何关系。答案是方差也可以应用于泛型。前面的示例使用了 objectstring 数组。这里的代码使用通用列表而不是数组:

      List<object> objectList=new List<object>();
      List<string> stringList=new List<string>();
      objectList=stringList;
      

      如果您尝试一下,您会发现这不是 C# 中支持的方案。在 C# 版本 4.0 和 .Net framework 4.0 中,泛型中的差异支持已被清理,现在可以使用新关键字 in out 带有泛型类型参数。他们可以为特定类型参数定义和限制数据流的方向,从而允许变化起作用。但在 List&lt;T&gt; 的情况下,T 类型的数据双向流动——List&lt;T&gt; 类型的方法有返回 T 值的方法,还有其他接收此类值的方法。

      这些方向限制的重点是在有意义的地方允许变化,但防止出现问题,例如前面一个数组示例中提到的运行时错误。当类型参数正确地用 inout 修饰时,编译器可以在编译时检查并允许或禁止其变化。 Microsoft 已经努力将这些关键字添加到 .Net 框架中的许多标准接口中,例如 IEnumerable&lt;T&gt;

      public interface IEnumerable<out T>: IEnumerable {
          // ...
      }
      

      对于这个接口,T 类型对象的数据流很清楚:它们只能从这个接口支持的方法中检索,而不是传递给它们 .因此,可以构造一个类似于前面描述的List&lt;T&gt; 尝试的示例,但使用IEnumerable&lt;T&gt;

      IEnumerable<object> objectSequence=new List<object>();
      IEnumerable<string> stringSequence=new List<string>();
      objectSequence=stringSequence;
      

      C# 编译器从 4.0 版开始可以接受此代码,因为由于类型参数 T 上的 out 说明符,IEnumerable&lt;T&gt; 是协变的。

      在使用泛型类型时,重要的是要注意差异以及编译器应用各种技巧的方式,以使您的代码按您期望的方式工作。

      关于方差的知识比本章介绍的要多,但这足以使所有进一步的代码易于理解。

      参考:

      【讨论】:

        【解决方案4】:

        协方差非常简单,从某些集合类List 的角度来看是最好的想法。我们可以参数化 List 类与一些类型参数T。也就是说,我们的列表包含T 类型的元素,其中一些是T。如果

        ,列表将是协变的

        S 是 T 的子类型当且仅当 List[S] 是 List[T] 的子类型

        (我使用数学定义 iff 来表示当且仅当。)

        也就是说,List[Apple] List[Fruit]。如果有一些例程接受List[Fruit] 作为参数,而我有List[Apple],那么我可以将其作为有效参数传入。

        def something(l: List[Fruit]) {
            l.add(new Pear())
        }
        

        如果我们的集合类List 是可变的,那么协方差就没有意义,因为我们可能假设我们的例程可以像上面那样添加一些其他水果(不是苹果)。因此,我们应该只希望 不可变 集合类是协变的!

        【讨论】:

        • 很好的定义,但它忽略了不仅类型可以被视为协/逆变的事实。例如,Java List&lt;T&gt; 也不是,但 Java 通配符允许您在使用点(而不是声明)以协变或逆变方式处理 - 当然,将类型上的操作集限制为实际的操作集协变和逆变。
        • 我相信List&lt;? extends Fruit&gt; 是一种存在类型:即List[T] forSome T &lt;: Fruit - forSome T <: fruit> 本身就是一种类型在这种情况下。尽管如此,Java 在这种类型中仍然不是协变的。例如,接受 List&lt;? extends Fruit&gt; 的方法不会接受 List&lt;? extends Fruit&gt;
        • OTOH, S &lt;= T iff List[T] &lt;= List[S] 相对于add 操作,因为我们可以将add 类型为S 的事物转换为List[T] 如果S &lt;= T。所以列表相对于add 操作是逆变的。
        • 如果您扩展您的答案以使用水果证明逆变性,我会发现它非常有用。感觉看完后我懂协变,但还是不懂逆变。
        猜你喜欢
        • 1970-01-01
        • 2021-03-24
        • 2010-10-22
        • 1970-01-01
        • 2013-07-29
        • 2011-03-29
        • 1970-01-01
        • 2011-02-09
        • 2019-05-02
        相关资源
        最近更新 更多