C#与CLR学习笔记(6)—— 轻松理解协变与逆变

19 篇文章 3 订阅
3 篇文章 0 订阅

前言

协变(covariance)和 逆变(contravariance)是我们在学习泛型委托和泛型接口中遇到的比较抽象的一组概念。它们的使用方法,其实是非常简单的,即:

in 标记的逆变量只能出现在泛型接口的方法的输入参数中,或者作为泛型委托的输入参数;
out 标记的协变量只能作为泛型接口方法或者泛型委托的输出参数。

但是,为什么需要将变量标记为逆变或协变,即它们产生的原理,以及工作原理,却是不太好理解。这里我们就探究一下。

本质与原因

简单本质

开门见山地说吧,协变和逆变的本质很简单,就是实现 将一个 泛型类型变量 转换为 泛型实参不同的 同一种泛型类型变量。文字表述不好理解,举个栗子:假设我定义了一个泛型委托 MyDelegate<T>。那么,将T标记为协变或者逆变,就可以实现 将一个 MyDelegate<T1> 变量转换为MyDelegate<T2>变量,其中T1T2是两个泛型的实际参数。
当然,T1T2之间需要满足一定的关联:

  • 协变量:泛型类型参数可以从一个类更改为它的某个基类。例如下面的代码是合法的:
public delegate void MyDelegate<out T>();

MyDelegate<BaseClass> d_base;
MyDelegate<DerivedClass> d_son = new MyDelegate<DerivedClass>(DoSomething);
d_base = d_son; 
  • 逆变量:泛型类型参数可以从一个类更改为它的某个派生类。例如下面的代码是合法的:
public delegate void MyDelegate<in T>();

MyDelegate<BaseClass> d_base = new MyDelegate<BaseClass>(DoSomething);
MyDelegate<DerivedClass> d_son = new MyDelegate<DerivedClass>(DoSomething);
d_son = d_base;

这样一来,协变和逆变就很好理解了,它们本质就是 类型转换,只不过是针对泛型类型的。

产生原因

为什么泛型类型需要协变和逆变呢?我们知道,DerivedClass是可以隐式地转换为BaseClass的。但是当具有继承关系的类型是 泛型类型的实际参数 的时候,情况就不一样了。泛型类型会丢失泛型参数的继承关系
泛型类型的一个特点就是,只要泛型实际参数的类型不同,CLR就会创建不同的类型对象(Type object,可参考这篇文章),把它们当做不同的类型。这也是好理解的。于是乎,即使DerivedClass是派生自BaseClass的,但是 IMyInterface<BaseClass>IMyInterface<DerivedClass>就是完全不同的两个类型,它们之间不再有任何继承关系。这就是问题产生的原因。
这种情况称为泛型的 不变性(invariance)。在 .NET 4 之前,尚未引入inout关键字,所有泛型类型的泛型参数都是不变量(invariant),这意味着泛型类型不可更改。举一个简单案例:List<int>List<long> 是不同的集合类型,不能将List<int> 转换为 List<long>
泛型的不变性(invariance)对我们的编码限制很大,因为有时候我们需要将泛型实参不同的同种泛型类型相互转化,于是便产生了协变和逆变。考虑这种情况:我们有一个泛型接口IMyInterface<T>,它定义了一个方法DoSomething(),这个方法是返回泛型实参T的一个实例的,见下:

public interface IMyInterface<T>
{
    T DoSomething();
}
public class MyImplementation<T> : IMyInterface<T>
{
    public T DoSomething()
    {
        return (T)Activator.CreateInstance(typeof(T));
    }
}

static void Main(string[] args)
{
    IMyInterface<BaseClass> interface1;
    IMyInterface<DerivedClass> interface2 = new MyImplementation<DerivedClass>();
    interface1 = interface2;//编译失败
    interface1.DoSomething();
}

在客户端调用时,我们定义了IMyInterface<BaseClass>IMyInterface<DerivedClass>。由于这个接口的方法返回的是泛型实参的实例,因此我们希望能将 IMyInterface<DerivedClass>转换为IMyInterface<BaseClass>不影响 接口方法的调用,因为,前者返回的是派生类,可自动隐式转化为其父类。但是,上述Main中的转化代码会编译失败,提示无法转换。
为了提高泛型类型的灵活性,.NET 4 引入泛型参数的 inout 关键字。我们将上述 IMyInterface<T> 修改为 IMyInterface<out T>,那么Main中的代码就可以编译通过了。
这就是协变产生的原因,也是为什么上文中提到,协变要求泛型参数T只能出现在输出位置的原因。
对于逆变,原因是类似的。如果泛型参数T是作为方法的输入参数或者委托的输入参数使用,那么,一个 IMyInterface<DerivedClass>变量传入的参数就是一个 DerivedClass 实例,那么我们希望可以将IMyInterface<DerivedClass>变量转化为IMyInterface<BaseClass>变量,因为后者需要 BaseClass 实例作为输入,前者的DerivedClass 实例可以自动转换为BaseClass 实例,不影响后者方法的调用。因此,逆变就被引入了。
例如,下面代码能通过编译,因为 Action 委托被定义为 Action<in T>(T obj)

Action<BaseClass> actionBase = MyAction;
Action<DerivedClass> actionDerived;
actionDerived = actionBase ;

广义上的协变与逆变

其实逆变和协变这两个概念与泛型是无关的。在.NET中,参数类型本来就是协变的,即:方法可以接受其参数类型的子类型实例作为实际参数;方法的返回类型是逆变的,返回结果(子类)可以被转换成其父类。
逆变可以这样理解:把一个表达式的父类形式替换成其子类的相应形式;
而协变就是:把一个表达式的子类形式替换为其父类的相应形式。
例如:
DoSomething(BaseClass) 这一个定义可以被 DoSomething(DerivedClass) 这样调用,就是协变的;
DerivedClass d = GetDerivedClass() 可以被 BaseClass b = GetDerivedClass() 这样调用,就是逆变的。

参考文献

[1] Eric Lippert 系列文章
[2] 《C#高级编程》,Christian Nagel
[3] 《CLR via C#》,第四版

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值