编程语言中的协方差和逆方差有什么区别?[已关闭]

任何人都可以解释编程语言理论中的协方差和逆变的概念吗?


答案 1

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

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

(我使用数学定义iff来表示当且仅当

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

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

如果我们的集合类是可变的,那么协方差就没有意义了,因为我们可能会假设我们的例程可以像上面那样添加一些其他水果(不是苹果)。因此,我们只应该喜欢不可变的集合类是协变的!List


答案 2

协方差和逆方差之间是有区别
非常粗略地说,如果运算保持类型的顺序,则该运算是协变的,如果它反转此顺序,则它是逆变

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

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

当然,可以将不同的值插入到数组中,因为最终它们都来自.Net框架。换句话说,是一种非常一般或较大的类型。现在,这里有一个支持协方差的位置:
将较小类型的值分配给较大类型的变量System.ObjectSystem.Object

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

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

想想看——在某种程度上,这是你所期望的,但话又说回来,事实并非如此。毕竟,虽然派生自 ,但不会派生自 。在此示例中,对协方差的语言支持使赋值成为可能,这在许多情况下都是您会发现的。方差是使语言更直观地工作的功能。stringobjectstring[]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 执行某些操作(这无关紧要),另一个更通用,可以使用从 派生的任何类型。在事情方面,你现在也有这些:WomanPersonWoman

delegate void AcceptWomanDelegate(Woman person);

static void DoWork(Woman woman, AcceptWomanDelegate acceptWoman) {
    acceptWoman(woman);
}

DoWork是一个可以获取 a 和对函数的引用的函数,该函数也接受 a ,然后将 的实例传递给委托。考虑一下您在这里拥有的元素的多态性大于 ,并且大于 。被认为大于差异目的。WomanWomanWomanPersonWomanWorkWithPersonWorkWithWomanWorkWithPersonAcceptWomanDelegate

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

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

创建实例。然后调用 DoWork,传入实例以及对该方法的引用。后者显然与委托类型兼容——一个参数类型为,没有返回类型。不过,第三行有点奇怪。该方法采用 as 参数,而不是 所要求的 。但是,与委托类型兼容。逆变使它成为可能,因此在委托的情况下,较大的类型可以存储在较小类型的变量中。再一次,这是一个直观的事情:如果WorkWithPerson可以与任何人一起工作,那么传递一个女人就不会错,对吧?WomanWomanWorkWithWomanAcceptWomanDelegateWomanWorkWithPersonPersonWomanAcceptWomanDelegateWorkWithPersonWorkWithPersonAcceptWomanDelegate

到现在为止,您可能想知道所有这些与泛型有何关系。答案是方差也可以应用于泛型。前面的示例使用和数组。此处,代码使用泛型列表而不是数组:objectstring

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

如果尝试此操作,您会发现这不是 C# 中受支持的方案。在 C# 版本 4.0 和 .Net Framework 4.0 中,已清理泛型中的方差支持,现在可以将 new 关键字用于泛型类型参数。它们可以定义和限制特定类型参数的数据流方向,从而允许方差起作用。但是在 的情况下,类型的数据在两个方向上流动 — 类型上有返回值的方法,而其他方法接收这样的值。List<T>TList<T>T

这些方向性限制的要点是允许在有意义的地方使用方差,但要防止出现诸如前面的数组示例中提到的运行时错误之类的问题。当类型参数正确修饰为 inout 时,编译器可以在编译时检查并允许或不允许其方差。微软已经努力将这些关键字添加到.Net框架中的许多标准界面中,例如:IEnumerable<T>

public interface IEnumerable<out T>: IEnumerable {
    // ...
}

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

IEnumerable<object> objectSequence=new List<object>();
IEnumerable<string> stringSequence=new List<string>();
objectSequence=stringSequence;

自 4.0 版起,此代码对于 C# 编译器来说是可接受的,因为由于 type 参数上的 out 说明符,此代码是协变的。IEnumerable<T>T

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

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

裁判: