编程语言中的协方差和逆变
谁能解释我,编程语言理论中协方差和逆变的概念?
协方差非常简单,从一些集合类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关于方差的所有文章的有序链接列表:
- C#中的协方差和逆变,第一部分
- C#中的协方差和逆变,第二部分:arrays协方差
- C#中的协方差和逆变,第三部分:方法组转换方差
- C#中的协方差和逆差,第四部分:真实代表方差
- C#中的协方差和逆变,第五部分:高阶函数伤害了我的大脑
- C#中的协方差和逆变,第六部分:接口方差
- C#中的协方差和反演:第七部分:为什么我们需要一个语法?
- C#中的协方差和逆变,第八部分:语法选项
- C#中的协方差和逆差,第九部分:突破性的变化
- 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
的实例传递给代表。 考虑一下这里的元素的多态性 。 Person
比Woman
, WorkWithPerson
比WorkWithWoman
。 WorkWithPerson
差异的目的, 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。 前面的示例使用了object
和string
数组。 这里代码使用通用列表而不是数组:
List
如果你试试这个,你会发现这不是C#中支持的场景。 在C#4.0版和.Net framework 4.0中,已经清理了generics中的方差支持,现在可以使用新的关键字输入和输出generics类型参数。 他们可以定义和限制特定类型参数的数据流方向,从而允许方差起作用。 但是在List
的情况下,类型T
的数据在两个方向上流动 – 类型List
有返回T
值的方法,以及接收这些值的其他方法。
这些方向限制的要点是允许在有意义的地方使用方差 ,但要防止出现前面一个数组示例中提到的运行时错误等问题 。 当使用in或out正确修饰类型参数时 ,编译器可以在编译时检查并允许或禁止其方差。 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#编译器将产生错误。