谁能解释编程语言理论中协变和逆变的概念?
4 回答
从某些集合类的角度来看,协方差非常简单并且最好考虑List
。我们可以使用一些类型参数对List
类进行参数化T
。也就是说,我们的列表包含T
一些类型的元素T
。列表将是协变的,如果
S 是 T 的子类型当且仅当 List[S] 是 List[T] 的子类型
(我使用数学定义iff来表示当且仅当。)
也就是说,aList[Apple]
是 a List[Fruit]
。如果有一些例程接受 aList[Fruit]
作为参数,并且我有 a List[Apple]
,那么我可以将其作为有效参数传入。
def something(l: List[Fruit]) {
l.add(new Pear())
}
如果我们的集合类List
是可变的,那么协方差就没有意义,因为我们可能假设我们的例程可以像上面那样添加一些其他水果(不是苹果)。因此,我们应该只希望不可变集合类是协变的!
协变和逆变是有区别的。
非常粗略地说,如果一个操作保持类型的顺序,它就是协变的,如果它颠倒这个顺序,它就是逆变的。
排序本身旨在将更一般的类型表示为比更具体的类型更大。
这是 C# 支持协方差的情况的一个示例。首先,这是一个对象数组:
object[] objects=new object[3];
objects[0]=new object();
objects[1]="Just a string";
objects[2]=10;
当然,可以将不同的值插入到数组中,因为最终它们都来自System.Object
.Net 框架。换句话说,System.Object
是一个非常通用或大的类型。现在这里有一个支持协方差的地方:
将较小类型的值分配给较大类型的变量
string[] strings=new string[] { "one", "two", "three" };
objects=strings;
type 的变量 objectsobject[]
可以存储一个实际上是 type 的值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) {
}
其中一个函数对 a 执行某些操作(无关紧要)Woman
,另一个更通用,可以使用从 . 派生的任何类型Person
。另一方面Woman
,您现在还拥有这些:
delegate void AcceptWomanDelegate(Woman person);
static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
acceptWoman(woman);
}
DoWork
是一个可以接受 aWoman
的函数和对也接受 a 的函数的引用Woman
,然后它将实例传递Woman
给委托。考虑您在此处拥有的元素的多态性。Person
大于,大于。
_ _ _ _ 为了方差的目的,也被认为大于。Woman
WorkWithPerson
WorkWithWoman
WorkWithPerson
AcceptWomanDelegate
最后,你有这三行代码:
Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);
创建一个Woman
实例。然后调用 DoWork,传入Woman
实例以及对该WorkWithWoman
方法的引用。后者显然与委托类型兼容AcceptWomanDelegate
——一个类型的参数Woman
,没有返回类型。不过,第三行有点奇怪。该方法WorkWithPerson
采用 aPerson
作为参数,而不是 a Woman
,如 所要求的那样AcceptWomanDelegate
。不过,WorkWithPerson
与委托类型兼容。逆变使这成为可能,因此在委托的情况下,较大的类型WorkWithPerson
可以存储在较小类型的变量中AcceptWomanDelegate
。再一次,这是直观的事情:如果WorkWithPerson
可以与 any 一起工作Person
,传递 aWoman
不会错,对吧?
到目前为止,您可能想知道这一切与泛型有何关系。答案是方差也可以应用于泛型。前面的示例使用了object
和string
数组。这里的代码使用通用列表而不是数组:
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<T>
,类型的数据是T
双向流动的——类型List<T>
上有返回T
值的方法,还有其他接收这些值的方法。
这些方向限制的重点是在有意义的地方允许变化,但要防止出现前面数组示例中提到的运行时错误等问题。当类型参数正确地用in或out修饰时,编译器可以在编译时检查并允许或禁止其变化。Microsoft 已努力将这些关键字添加到 .Net 框架中的许多标准接口中,例如IEnumerable<T>
:
public interface IEnumerable<out T>: IEnumerable {
// ...
}
对于这个接口,类型T
对象的数据流是很清楚的:它们只能从这个接口支持的方法中获取,而不是传递给它们。因此,可以构建一个类似于List<T>
前面描述的尝试的示例,但使用IEnumerable<T>
:
IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;
自 4.0 版以来,此代码对于 C# 编译器是可接受的,因为由于类型参数上的outIEnumerable<T>
说明符是协变的。T
使用泛型类型时,重要的是要注意差异以及编译器应用各种技巧的方式,以使您的代码按您期望的方式工作。
关于方差的知识比本章所涉及的要多,但这足以使所有进一步的代码易于理解。
参考:
方差,协方差,逆变,不变性
类型(T)
composite data type
- 是由另一种类型构建的类型。例如,它可以是通用的、容器(集合)、可选的[示例]
method's type
- 方法的parameter type
(前置条件)和return type
(后置条件)[关于]
方差
Variance
- 是关于分配兼容性。这是一种使用 aderived type
而不是 的 original type
能力。这不是 parent-child
关系
X(T) - `composite data type` or `method type` X, with type T
Covariance
(相同的子类型方向)您可以分配超过 derived type
original type
X(T) covariant
或者X(T1) is covariant to X(T2)
当关系 T1 到 T2 与 X(T1) 到 X(T2) 相同时
Contravariance
(相反的子类型方向)你可以分配更少 derived type
original type
X(T) contravariant
或者X(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 Array 是协方差的
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>'
Swift 闭包:闭包的返回类型是协变
闭包的参数类型是逆变
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
在将方法绑定到委托时,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 的返回类型 (Object),但由于 Int32 是值类型,因此不允许这种形式的协变。
显然,值类型和 void 不能用于协变和逆变的原因是因为这些东西的内存结构不同,而引用类型的内存结构始终是指针。幸运的是,如果您尝试执行不受支持的操作,C# 编译器将产生错误。