编程语言中的协方差和逆变

谁能解释我,编程语言理论中协方差和逆变的概念?

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

S是T的子类型iff List [S]是List [T]的子类型

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

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

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

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

对我们其他人的逆转

以下是我们如何为C#4.0添加新的方差function的文章。 从底部开始。

http://blogs.msdn.com/ericlippert/archive/tags/Covariance+and+Contravariance/default.aspx

为了方便起见,这里是Eric Lippert关于方差的所有文章的有序链接列表:

  1. C#中的协方差和逆变,第一部分
  2. C#中的协方差和逆变,第二部分:arrays协方差
  3. C#中的协方差和逆变,第三部分:方法组转换方差
  4. C#中的协方差和逆差,第四部分:真实代表方差
  5. C#中的协方差和逆变,第五部分:高阶函数伤害了我的大脑
  6. C#中的协方差和逆变,第六部分:接口方差
  7. C#中的协方差和反演:第七部分:为什么我们需要一个语法?
  8. C#中的协方差和逆变,第八部分:语法选项
  9. C#中的协方差和逆差,第九部分:突破性的变化
  10. C#中的协方差和逆差,第十部分:处理歧义

协方差 变量之间存在区别。
非常粗略地说,如果一个操作保留了类型的顺序,那么它就是协变的,如果它颠倒了这个顺序,它就是逆变的。

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

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

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

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

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

想一想 – 在某种程度上,这是你所期望的,但事实并非如此。 毕竟,虽然string派生自object ,但是string[] 不是object[]派生的。 在这个例子中,协方差的语言支持使得任务无论如何都是可能的,这在很多情况下都会找到。 方差是一种使语言更直观地工作的function。

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

 // 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 。 现在考虑你有这两个function:

 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的实例传递给代表。 考虑一下这里的元素的多态性PersonWomanWorkWithPersonWorkWithWomanWorkWithPerson差异的目的, WorkWithPerson也被认为比AcceptWomanDelegate 更大

最后,您有以下三行代码:

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

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

到目前为止,您可能想知道所有这些与generics有何关系。 答案是方差也可以应用于generics。 前面的示例使用了objectstring数组。 这里代码使用通用列表而不是数组:

 List objectList=new List(); List stringList=new List(); objectList=stringList; 

如果你试试这个,你会发现这不是C#中支持的场景。 在C#4.0版和.Net framework 4.0中,已经清理了generics中的方差支持,现在可以使用新的关键字输入和输出generics类型参数。 他们可以定义和限制特定类型参数的数据流方向,从而允许方差起作用。 但是在List的情况下,类型T的数据在两个方向上流动 – 类型List有返回T值的方法,以及接收这些值的其他方法。

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

 public interface IEnumerable: IEnumerable { // ... } 

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

 IEnumerable objectSequence=new List(); IEnumerable stringSequence=new List(); objectSequence=stringSequence; 

从版本4.0开始,此代码对于C#编译器是可接受的,因为IEnumerable由于类型参数T上的out说明符而具有协变性。

使用generics类型时,重要的是要了解方差以及编译器应用各种技巧的方式,以使代码按照您期望的方式工作。

关于方差的内容比本章所述还要多,但这足以让所有进一步的代码都可以理解。

参考:

  • PROFESSIONAL Functional Programming in C#

Bart De Smet 在这里有关于协方差和逆变的博客文章。

当将方法绑定到委托时,C#和CLR都允许引用类型的协方差和反方差。 协方差意味着方法可以返回从委托的返回类型派生的类型。 Contra-variance意味着方法可以采用作为委托参数类型的基础的参数。 例如,给定一个如下定义的委托:

委托Object MyCallback(FileStream s);

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

喜欢这个:

String SomeMethod(Stream s);

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

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

Int32 SomeOtherMethod(Stream s);

尽管SomeOtherMethod的返回类型(Int32)是从MyCallback的返回类型(Object)派生的,但是不允许这种forms的协方差,因为Int32是一个值类型。

显然,值类型和void不能用于协方差和反方差的原因是因为这些事物的存储器结构变化,而引用类型的存储器结构总是指针。 幸运的是,如果您尝试执行不受支持的操作,C#编译器将产生错误。