【发布时间】:2010-11-12 21:41:19
【问题描述】:
谁能解释一下协变和逆变的概念 编程语言理论?
【问题讨论】:
-
我闻到了一道作业题。
标签: java c# programming-languages covariance contravariance
谁能解释一下协变和逆变的概念 编程语言理论?
【问题讨论】:
标签: java c# programming-languages covariance contravariance
方差、协方差、逆变、不变性
类型(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) covariant 或 X(T1) is covariant to X(T2) 当关系 T1 到 T2 与 X(T1) 到 X(T2) 相同时
Contravariance(相反子类型方向) 你可以分配 less 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 数组是协方差的
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
【讨论】:
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# 编译器将 如果您尝试执行不受支持的操作,则会产生错误。
【讨论】:
协变和逆变是有区别的。
非常粗略地说,如果一个操作保持类型的顺序,它就是协变的,如果它颠倒这个顺序,它就是逆变的。
排序本身旨在将更一般的类型表示为大于更具体的类型。
这是 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大于Woman,WorkWithPerson大于WorkWithWoman。
出于差异的目的,WorkWithPerson 也被认为大于AcceptWomanDelegate。
最后,你有了这三行代码:
Woman woman=new Woman();
DoWork(woman, WorkWithWoman);
DoWork(woman, WorkWithPerson);
创建了一个Woman 实例。然后调用 DoWork,传入Woman 实例以及对WorkWithWoman 方法的引用。后者显然与委托类型AcceptWomanDelegate 兼容——Woman 类型的一个参数,没有返回类型。
不过,第三行有点奇怪。方法WorkWithPerson 将Person 作为参数,而不是Woman,正如AcceptWomanDelegate 所要求的那样。不过,WorkWithPerson 与委托类型兼容。 逆变 使之成为可能,因此在委托的情况下,较大的类型 WorkWithPerson 可以存储在较小类型的变量 AcceptWomanDelegate 中。这又是直观的事情:如果WorkWithPerson 可以与任何Person 一起使用,那么传入Woman 不会错,对吧?
现在,您可能想知道这一切与泛型有何关系。答案是方差也可以应用于泛型。前面的示例使用了 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;
C# 编译器从 4.0 版开始可以接受此代码,因为由于类型参数 T 上的 out 说明符,IEnumerable<T> 是协变的。
在使用泛型类型时,重要的是要注意差异以及编译器应用各种技巧的方式,以使您的代码按您期望的方式工作。
关于方差的知识比本章介绍的要多,但这足以使所有进一步的代码易于理解。
参考:
【讨论】:
协方差非常简单,从某些集合类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 是可变的,那么协方差就没有意义,因为我们可能假设我们的例程可以像上面那样添加一些其他水果(不是苹果)。因此,我们应该只希望 不可变 集合类是协变的!
【讨论】:
List<T> 也不是,但 Java 通配符允许您在使用点(而不是声明)以协变或逆变方式处理 - 当然,将类型上的操作集限制为实际的操作集协变和逆变。
List<? extends Fruit> 是一种存在类型:即List[T] forSome T <: Fruit - forSome T <: fruit> 本身就是一种类型在这种情况下。尽管如此,Java 在这种类型中仍然不是协变的。例如,接受 List<? extends Fruit> 的方法不会接受 List<? extends Fruit>
S <= T iff List[T] <= List[S] 相对于add 操作,因为我们可以将add 类型为S 的事物转换为List[T] 如果S <= T。所以列表相对于add 操作是逆变的。