26

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

4

4 回答 4

26

从某些集合类的角度来看,协方差非常简单并且最好考虑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是可变的,那么协方差就没有意义,因为我们可能假设我们的例程可以像上面那样添加一些其他水果(不是苹果)。因此,我们应该只希望不可变集合类是协变的!

于 2009-07-22T06:56:36.283 回答
13

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

排序本身旨在将更一般的类型表示为比更具体的类型更大。
这是 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派生自objectstring[] 但不派生自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大于,大于。 _ _ _ _ 为了方差的目的,也被认为大于WomanWorkWithPersonWorkWithWomanWorkWithPersonAcceptWomanDelegate

最后,你有这三行代码:

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不会错,对吧?

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

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

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

这些方向限制的重点是在有意义的地方允许变化,但要防止出现前面数组示例中提到的运行时错误等问题。当类型参数正确地用inout修饰时,编译器可以在编译时检查并允许或禁止其变化。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

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

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

参考:

于 2011-07-28T12:15:46.467 回答
1

方差,协方差,逆变,不变性

类型(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 typeoriginal type

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

Contravariance相反的子类型方向)你可以分配更少 derived typeoriginal type

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

Invariance也不CovarianceContravariance。也存在[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

【里氏原理】

[Java 泛型]

于 2020-07-06T09:15:01.720 回答
0

在将方法绑定到委托时,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# 编译器将产生错误。

于 2012-10-24T10:55:22.260 回答