C#中协变与逆变的个人理解
读了园子中一些前辈的关于C#中协变与逆变的文章,收获很大,分享一下我的个人理解,希望用较浅显的方式理解这个比较绕弯的概念。
协变与逆变应该是CLR的特性,我仅对我熟悉的C#举例说明。
说白了,它主要解决的是一个类型转换的问题,用一个最简单的泛型表达式就是:
S<T1> = S<T2>
当然这只是一个抽象的表达式,而且只包含了一个泛型类型参数,意思是将一个S<T2>的实例赋值给一个S<T1>的实例。S可能是一个接口或委托,T1和T2是有父子关系(或子父关系)的两个引用类型。在.NET4.0之前,这样的直接转换是不可能的,有时我们不得不写一些转换函数来实现。协变与逆变可以在一定程度上方便地解决这个问题。
我们来看看怎么理解这个表达式。
S<T1>是暴露给使用者的类型,S<T2>是真正做事的类型,所以通俗来讲,作为使用者,我们交给S<T1>的参数实际是交给了S<T2>,而我们通过S<T1>拿到的返回值实际是S<T2>给我们的。
画个图来表示就是:
图虽然粗糙,说明道理即可。可以看出,如果要使表达式S<T1>=S<T2>成立,必须满足两点:
- S<T1>中的所有参数都必须类型安全地转换为S<T2>中的对应参数;
- S<T2>中的所有返回值都必须类型安全地转换为S<T1>的对应返回值。
当然如果参数或者返回值的类型是与泛型类型无关的类型,那么上面两条必然满足。而且需要尤其注意的是:这里的参数和返回值类型未必是泛型参数类型本身,可能就是T1,也可能是A<T1>,还可能是A<B<C<T1>>>等等,但无疑都要满足上面的两个条件。
我认为理解了上面的关系之后,协变与逆变自然可以一步步推理出来了。
假设TBase是TChild的父类,如果满足S<TBase>=S<TChild>就说S支持对T的协变,.NET4.0有个新写法S<out T>。
那么应用上面的两个规则看看会发生什么:
- S<TBase>中的所有参数都必须类型安全地转换为S<TChild>中的对应参数;
- 如果参数类型就是T本身,那么要求TBase可以类型安全地转换为TChild,即TChild=TBase,父类怎么可能类型安全地转换为子类?所以泛型类型参数绝对不可能出现在参数列表中,否则会引发编译的错误。
- 如果参数类型是A<T>,那么要求A<TBase>可以类型安全地转换为A<TChild>,即A<TChild>=A<TBase>。这要求A要支持对T的逆变,即A<in T>。所以我们自然而然推出了一个重要原则:方法参数的协变-逆变互换原则(该原则取自装配脑袋的博文:http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html)。
- 如果参数是更复杂的多层嵌套,那么递归套用该原则即可。
- S<TChild>中的所有返回值都必须类型安全地转换为S<TBase>中的对应返回值;
- 如果参数类型是T类型本身,那么要求TChild可以类型安全地转换为TBase,即TBase=TChild,这个毫无疑问没问题。所以T类型可以出现在返回值的位置上。
- 如果参数类型是A<T>,那么要求A<TChild>可以类型安全地转换为A<TBase>,即A<TBase>=A<TChild>。这要求A支持对T的协变,即A<in T>。又一个重要原则:方法返回值的协变-逆变一致原则。
- 同上。
逆变可以采用同样的推理过程,不再详述。
协变与逆变都是我们在定义这个接口或者委托的时候规定好的,至于参数与返回值的限定都只是这个规定产生的结果。一步步缕下来思路还是比较清晰的,虽然不像TBase=TChild这样一眼可以看出,但起码有个推理的方向,实在不行只要想想这个丑陋的图片就好了。
只是自己的理解,如果有什么纰漏,欢迎指出。 :)