泛型中的协变与逆变
01 泛型
协变和逆变应用在泛型泛型接口和委托中。
当我们在使用泛型类和泛型方法的时候,我们想要的是什么?
- 增加代码可重用性
- 实现参数多态
- 类型安全
- 不会产生运行时转换或装箱操作的成本或风险
泛型类是不变量。 换而言之,如果一个输入参数指定
List<BaseClass>
,且你尝试提供List<DerivedClass>
,则会出现编译时错误。
即使有了以上那么多优点,单纯使用泛型仍然有具有“硬伤”:
- 由于不确定导致泛型类型参数在初始化之前都是未知的,换言之,不能假定泛型类型参数具有某些性质(继承、类型..)和某些功能。
C#通过添加约束的方式,将不确定变得更确定解决不确定性带来的以上问题。
- 使用
new
关键字约束泛型类声明中的类型实参必须有公共的无参数构造函数; - 使用
where
子句指定对用作泛型类型、方法、委托或本地函数中类型参数的参数类型的约束。 约束可指定接口、基类或要求泛型类型为引用、值或非托管类型。 它们声明类型参数必须具备的功能。 - 使用协变与逆变让泛型不仅仅限制于提前定义好的
new
关键字约束和where
子句约束,简单的进行方法内部的固定转换。
协变和逆变给与泛型接口和委托这样一种特殊的能力:通过限制泛型参数的输入输出方向使之成为遵守继承规则自由转换的可变量——泛型的多态。
首先通过一个例子看看协变与逆变的威力:
interface IContravariant<T> { }
class MyClass<T> : IContravariant<T> { }
static class Program
{
static void Main(string[] args)
{
IContravariant<Object> mObj = new MyClass<Object>();
IContravariant<String> mStr = new MyClass<String>();
mObj = mStr; //CS0266 无法将类型“IContravariant<string>”隐式转换为“IContravariant<object>”。存在一个显式转换(是否缺少强制转换?)
mStr = mObj; //CS0266 无法将类型“IContravariant<object>”隐式转换为“IContravariant<string>”。存在一个显式转换(是否缺少强制转换?)
}
}
如果将以上代码中IContravariant
接口的代码稍加修改:
//1. 改成:
interface IContravariant<out T> { }
//则Main方法中 mObj = mStr; 能够正常运行
//2. 改成:
interface IContravariant<in T> { }
//则Main方法中 mStr = mObj; 能够正常运行
out
和in
在此例中标记了IContravariant<T>
接口的协变/逆变特性,通过协变与逆变实现了类似具有继承关系类之间的互相转化。
下一节是更为具体的讲解。
如果泛型接口或委托的泛型参数被声明为协变或逆变,该泛型接口或委托则被称为“变体”。
02
协变与逆变的定义
- 如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的;
- 如果某个参数类型可以由其基类替换,那么这个类型就是支持逆变的。
转换
在上例中,为什么加入in
或out
关键字就能实现两个泛型类之间的安全转化?
让我们来看看是什么限制了泛型类之间的转换。
让我们先往上文中的MyClass<T>
中加入一些方法:
interface IContravariant<T> { }
class MyClass<T> : IContravariant<T>
{
public T M1() {return 0;}
public void M2(T param) {}
}
当试图将类型IContravariant<string>
转换为IContravariant<object>
时:
- M1方法的返回值尝试从
string
类型隐式转化为object
类型,毫无疑问可以转换成功; - M2方法从传入
string
类型变为尝试传入object
类型的参数,很显然会导致编译器报错。
当试图将类型IContravariant<object>
转换为IContravariant<string>
时: - M1方法的返回值尝试从
object
类型隐式转化为string
类型,很显然也会导致编译器报错。 - 此时M2方法从传入
object
类型变为尝试传入string
类型的参数,毫无疑问不会报错。
通过上述例子相信已经可以看出一些端倪,能安全转换需要两个条件:
- 变式(父)的方法参数能安全转为原式(子)的参数;
- 原式(子)的返回值能安全的转为变式的返回值。
C#为了让以上转换能够在合理的条件下成功,约定了使用in
和out
标识符限制泛型参数只能用来输入/输出。仍然是上面的例子,再次将泛型<T>
标识为out
(<out T>
),则M2方法将会报错,此时删去M1方法就可以安全的从IContravariant<string>
转换为IContravariant<object>
,这个转换过程称为协变。类似的,若将泛型<T>
标识为in
(<in T>
),则M1方法将会报错,此时删去M1方法就可以安全的从IContravariant<object>
转换为IContravariant<string>
,这个转换过程被称为逆变。
- 用
in
修饰的泛型参数不能用于输出,具有可逆变性。 - 用
out
修饰的泛型参数不能用于输入,具有可协变性。
使用协变与逆变要记住以下两点:
- 当前仅支持接口和委托的逆变与协变,不支持类和方法。但数组也有协变性。
- 值类型不参与逆变与协变。
03 接口与委托中的变体
自定义变体接口上例已经给出,自定义委托也是类似写法,这里需要注意的就是.NET中自带的变体:
IEnumerable<out T>
Action<in T>
Func<out TResult>
正是因为有了协变和逆变,在使用泛型的时候才能兼顾类型安全和不那么严格的一一对应所带来的变化,从而使泛型具有更大的适用性。
参考资料
-
在引用类型系统时,协变、逆变和不变性具有如下定义
Covariance
使你能够使用比原始指定的类型派生程度更大的类型。Contravariance
使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。Invariance
这意味着,你只能使用原始指定的类型;固定泛型类型参数既不是协变类型,也不是逆变类型。
-
zhangweiwen:逆变与协变详解
-
王柏成:10分钟浅谈泛型协变与逆变
-
装配中的脑袋:.NET 4.0中的泛型协变和反变