C#语言中的协变、逆变和不变性






3.22/5 (10投票s)
C#语言中的协变、逆变和不变性
引言
C#语言的最新版本——4.0——将包含一些有趣的功能。其中一项功能是允许在参数化委托和接口类型上使用协变和逆变。听起来很神秘,不是吗?
许多优秀的开发者从未听说过任何类型的“-variance”(尽管他们经常在不知不觉中使用它)。这与继承有很大关系——事实上,许多人认为这只是泛型提供的“另一件事”。那么,为什么要费心学习这些额外的知识呢?因为C#语言的操作符有时是协变的,有时是逆变的,有时是不变的。了解这些原理对于避免编译错误、讨厌的运行时异常以及能够使用更智能的编程技术至关重要。
本文简要介绍了C#编程语言中的协变和逆变。第一部分介绍了一些关键的理论概念和概念。第二部分重点介绍了C#语言中这项技术的应用。
理论
让我们从最枯燥(但必要的)部分——理论开始。如果一个类型之间的运算符按照从更具体到更一般的顺序排列这些类型,则称该运算符为协变的。类似地,如果一个类型之间的运算符按照相反的顺序排列它们,则称该运算符为逆变的。当不满足这些条件中的任何一个时,一个运算符被称为不变的。
记住这些定义可能并没有向你解释太多(然而,相反的情况也可能成立),让我们来看一些真实的例子。
用法
这些概念通常用于许多面向对象的编程语言(包括C#、C++和Java)中,以允许高度的灵活性和可扩展性。这种灵活性可以通过用派生自它的类型(逆变)或派生到给定类型的类型(协变)替换原始数据类型来实现。
这两个示例都将在示例中展示。假设我们有以下类模型……

……以及定义了以下方法
public Class2 Method(Class2 argument)
{
return null;
}
协方差
在C#中,每个方法的返回值都是协变的,而所有参数都是逆变的。这意味着此方法返回的值可以用C2类型的引用指向,也可以用任何C2派生(直接或间接)的类型的引用指向。因此,以下两个赋值都是正确的
Class2 = Method(null);Class1 = Method(null);
将此方法的返回值赋值给Class3
类型的引用将导致编译错误,因为返回值运算符不是逆变的。
逆变
但是,每个方法的所有参数都是逆变的(但不是协变的)。这意味着,除了传递方法签名所需类型的对象之外,还可以传递任何派生自它的类型的对象。让我们回到前面的例子。以下两行都是正确的
Method(new Class2());
Method(new Class3());
与前面的例子类似,用Class1
类型的参数调用此方法将导致编译错误,因为方法的参数不是协变的。
集合
集合在协变和逆变方面是相当值得注意的情况,因为它们的处理方式不同。类型安全强制它们保持不变。这意味着**只有**int[]
类型的对象(精确地)可以赋值给以下引用
int[] array;
看起来很苛刻,对吧?确实如此。这就是为什么Microsoft允许所有**引用**类型的数组是协变的。这意味着(回到前面的例子),您可以进行此赋值
Class1[] generalizedArray;Class2[] specializedArray = new Class2[]{new Class2()};
看起来很舒服?确实如此,但是这个解决方案也有一个主要的缺点。编译器不会捕获以下错误
generalizedArray[0] = new C1();
编译器甚至不会发出警告。你将得到一个讨厌的ArrayTypeMismatchException
运行时异常。非常痛苦。
结论
本文介绍了协变、逆变和不变性背后的基本概念和思想。它还介绍了在C#编程语言中使用这些概念以及一些简单的例子。
协变和逆变是很有趣的概念。至少掌握一些关于它们的知识将被证明非常有用,尤其是在C# 4.0即将问世的情况下,因为它将大量使用它们。
历史
- 2009年7月23日:初始发布