解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4
前言
本篇继续讲解泛型。上一篇讲解了泛型类的定义细节。本篇继续讲解泛型接口。
泛型接口
使用泛型可定义接口,即在接口中定义的方法可以带泛型参数。然后由继承接口的类实现泛型方法。用法和继承泛型类基本没有区别。
不变、协变和逆变
在.Net4.0之前,泛型接口是不变的。.Net4.0通过协变和逆变为泛型接口和泛型委托增加了重要的扩展。
注:本书总体非常好,但在协变和逆变方面,我认为是有缺陷的。我一直偏好通过读书籍来了解技术,而不是逛论坛,但协变和逆变的问题我研究了本书多次,都没搞懂而放弃了,反正平时不需要理解这个也能编程,问题不大。但今天要做课程输出了,就必须得搞懂了。
然后我就结合网络上的文章来帮助理解,发现本书的命名和微软官方的命名的不同。本书中叫“协变”和“抗变”,而微软官方叫协变和逆变。在我最终理解清楚了这两个概念后,我认为“抗变”的命名是不合理的。所以我还是采用了微软官方的说法。而可能正是因为“抗变”的概念误导而导致逻辑分析时一直理不顺。后面我也会稍加分析为什么抗变的命名不合理。
要理解协变和逆变,并不容易。我网上搜索了不少文章。包括我前几次研究没透彻的那几次,也没少在网上找文章,但我认为都有这样或者那样的问题没讲清楚。所以,我本篇文章讲协变和逆变,也不一定能让大家都理解。但我尽量针对我理解过程中的困难,和网上的文章不足,重新构思整个理解思路,尽量把来龙去脉都交代到位。
注:泛型委托有同样的课题,原理相同,这里不表,后面委托章节会再提及
首先,协变和逆变,对应的是“不变”的概念。什么是不变:
string val1 = "abc"; //声明类型 = 实例化类型,是所谓不变
List<string> list1 = new List<string>(); //声明泛型类型 = 实例化泛型类型,是所谓不变
其次,协变和逆变是个通用概念,不只是泛型接口或者泛型委托的专有概念:
object user = new User(); //实例化类型是声明类型的子类,是所谓协变
IEnumerable<object> list2 = new List<string>(); //实例化泛型类型是声明泛型类型的子类,是所谓协变(这同时也是泛型接口)
上面举了协变的例子,逆变先不举例,这个比较逆天,要先有铺垫知识后面才能继续讲。
在这个例子里,我们可以对协变的概念用土话再描述下,所谓协变,是“符合对象继承规则,符合编译器验证规则,在类型间能够顺畅的转变,即所谓协变”。而逆变呢,并不是“不能够顺畅的转变”(那就是“抗变”了),我认为是不对的。逆变我认为应该理解为:“反向的变,逆向的变”。
第三,关注类型转换的矛盾。回到泛型接口,泛型接口实现的内部代码和外部代码间的类型转换点在哪里?当然是在泛型接口的方法上,这是泛型接口的“界面”。而方法中定义的返回值和实际返回值可能是继承关系,定义的传入参数和实际传入参数也可能是继承关系。而这将导致问题,从而最终需要通过给T增加关键字in或者out,来限制方法参数和返回值的行为。因此,我们下面需要仔细分析泛型接口的方法的传入参数和返回值。
第四,在讲协变和逆变前,先举个“不变”的泛型例子作为基础。这个例子中,T既作为方法参数,又作为方法返回值。
/// <summary>
/// 同时有输入T和输出T的泛型
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IBase<T>
{
T Test(T param); //T既作为传入参数,又作为传出的返回值
}
public class Sub<T> : IBase<T>
{
public T Test(T param) { return default(T); }
}
/// <summary>
/// 泛型的"不变"测试
/// </summary>
public static class Gen
{
public static void Test()
{
IBase<string> b = new Sub<string>(); //声明泛型类型 = 实例化泛型类型: 即所谓“不变”
var result = b.Test("");
Console.ReadKey();
}
}
如果,在使用过程中,泛型类型是“不变”的,那么就没有协变和逆变的问题。但我们也需要探究这段代码执行的原理,为理解协变和逆变打下基础。下面这个图是我在网上一片文章上找的,描述的就是执行b.Test("")时的执行流程,务必读懂:

执行过程:
- 执行IBase<string>.Test(""),泛型类型是string
- 因为创建的实例是Sub<string>,所以调用的是Sub<string>.Test("")
- 执行结果返回类型是string
- 结果再转化为IBase<string>.Test("")的返回值,也是string
因为实例化的泛型类型是不变的(参数和返回值都是string),所以执行过程中没有发生泛型类型的类型转换,所以执行成功。
第五,协变需求的产生,以及问题的出现。如果是按上面的方式,在泛型接口中就无法体现继承关系的优势。因为只能用确定类作为泛型类型,不能同时使用基类和派生类。那么我们尝试在以上案例的基础上尝试一下使用基类和派生类。
上述代码修改点:将声明的string泛型类型改为object泛型类型,其他不变。

我们发现编译出错了。为什么会这样呢?我们再分析一下执行过程。

执行过程:
- 执行IBase<object>.Test(""),泛型类型是object
- 因为创建的实例是Sub<string>,所以调用的是Sub<string>.Test("")。但这里出问题了。因为这需要将参数类型object转换为子类string,这做不到隐式转换,因为string是object的派生类。
- 编译失败
从上面例子我们可以发现,双向类型转化不再可行。那么只能单向的类型转换。这就是关键字in和out的由来。
如果要实现泛型接口的协变,必须对泛型接口的使用进行限制,那就是out关键字。使用out关键字,来表示T只能用于输出(作为返回值)。同理,对于逆变,就是使用关键字in,来表示T只能用于传入参数。
第六,道理搞明白了,我们再画一下协变和逆变的执行流程图

再贴两个协变和逆变的demo,
协变:
public interface IBase1<out T>
{
/// <summary>
/// 使用out修饰T,表明T只能用于方法的返回值(即输出)
/// </summary>
/// <returns></returns>
T Test();
}
public class Sub1<T> : IBase1<T>
{
public T Test() { return default(T); }
}
/// <summary>
/// 协变测试
/// </summary>
public static class Covariance
{
public static void Test()
{
IBase1<object> a = new Sub1<string>(); //声明泛型父类,实例化泛型子类,在协变时(使用out关键字)可以成立
var result = a.Test();
Console.ReadKey();
}
}
逆变:
public interface IBase2<in T>
{
/// <summary>
/// 用in修饰T,表明T只能作为方法的输入参数
/// </summary>
/// <param name="param"></param>
void Test(T param);
}
public class Sub2<T> : IBase2<T>
{
public void Test(T param) { Console.WriteLine("T默认值:" + default(T)); }
}
/// <summary>
/// 逆变测试
/// </summary>
public static class Contravariant
{
public static void Test()
{
IBase2<string> a = new Sub2<object>(); //声明泛型子类,实例化泛型父类,在逆变时(使用out关键字)可以成立
a.Test("");
Console.ReadKey();
}
}
第七、最后我们来想想为什么逆变叫“逆变”,而且不应该是“抗变”?
我们看这个逆变的执行代码:
IBase2<string> a = new Sub2<object>(); //声明泛型子类,实例化泛型父类,在逆变时(使用out关键字)可以成立
定义为子类,却声明为父类,这是不是很逆天?这不符合我们通常的思路。我想这应该就是叫“逆变”原因。逆变不应该是抗变,它没有抗拒类型转变,而是成功执行了类型转变。只不过,它的赋值语句看起来,怎么类型定义和类型初始化是反过来的?因而得名吧。
第八、协变,逆变就在身边
虽然以前没搞懂协变和逆变的概念,然而程序照写,似乎也没什么妨碍。这应该就是在实际应用中,还没碰到非常复杂的应用场景,所以对这个没感觉。实际上泛型接口,泛型委托的协变逆变就在身边。比如:
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2) //协变
public delegate TResult Func<in T, out TResult>(T arg) //协变,逆变
public interface IEnumerable<out T> : IEnumerable //协变
我们平时感觉不到我们在应用协变逆变,主要还是因为我们只讲in,out理解为方法输入参数、方法返回值。泛型类型在99%场景下都是“不变”的(如输入string,输出string),所以对此没有感知。
最后,我文中有些重要图片来自这篇文章,我觉得讲解的也非常好,给了我很大帮助。大家可以看看,某些方面讲解的更细:
逆变与协变详解
下一篇,我们开始讲泛型结构。
觉得文章有意义的话,请动动手指,分享给朋友一起来共同学习进步。
欢迎关注本人如下公众号 “产品技术知与行” ,打造全面的结构化知识库,包括原创文章、免费课程(C#,Java,Js)、技术专题、视野知识、源码下载等内容。

扫描二维码关注
解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4的更多相关文章
- .NET Core CSharp初级篇 1-8泛型、逆变与协变
.NET Core CSharp初级篇 1-8 本节内容为泛型 为什么需要泛型 泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助.比如说遇到两个模块的功能非常相似,只是一个是处理in ...
- Java泛型的逆变
在上篇<Java泛型的协变>这篇文章中遗留以下问题——协变不能解决将子类型添加到父类型的泛型列表中.本篇将用逆变来解决这个问题. 实验准备 我们首先增加以下方法,见代码清单1所示. 代码清 ...
- ios开发ios9新特性关键字学习:泛型,逆变,协变,__kindof
一:如何去学习?都去学习什么? 1:学习优秀项目的设计思想,多问几个为什么,为什么要这么设计,这么设计的好处是什么,还能不能在优化 ,如何应用到自己的项目中 2:学习优秀项目的代码风格,代码的封装设计 ...
- 《C#高级编程》之泛型--1创建泛型类
.NET自从2.0版本开始就支持泛型. 非泛型链表 闲话休提,马上来看下非泛型的简化链表类,它可以包含任意类型的对象. LinkedListNode.cs中: 在链表中,一个元素引用另一个元素,所以必 ...
- 重学《C#高级编程》(泛型与数组)
前段时间工作比较忙,就没有写随笔了,现在继续. 前两天重新看了泛型和数组两章,简单说下我自己的收获吧 泛型 我们知道数组是一种批量的数据格式,而泛型其实就是一种自定义的批量数据格式,当数组和C#现有的 ...
- C#核心语法讲解-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...
- C#核心语法-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)
泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...
- C#高级编程之泛型三(协变与逆变)
为何引入协变.逆变 我们知道一个子类对象可以赋值给一个基类对象 Animal animal = new Animal(); Animal cat = new Cat(); 那如果是用在泛型里面能行嘛? ...
- 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变
在net中json序列化与反序列化 准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...
随机推荐
- Linux yun命令使用报错:File "/usr/bin/yum", line 30 except KeyboardInterrupt, e:
原文参考:https://www.cnblogs.com/caiji/p/7891923.html 使用yum更新perl源,报错 问题出现原因: yum包管理是使用python2.x写的,将pyth ...
- java代码的编译、执行过程
Java代码编译是由Java源码编译器来完成,流程图如下所示: Java字节码的执行是由JVM执行引擎来完成,流程图如下所示: Java代码编译和执行的整个过程包含了以下三个重要的机制: Java源码 ...
- yum出现Loaded plugins: fastestmirror, security Loading mirror speeds from cached hostfile解决方法
yum出现Could not retrieve mirrorlist解决方法 Loaded plugins: fastestmirror, securityLoading mirror speeds ...
- class A<T> where T:new()
class A<T> where T:new() 这是类型参数约束,where表明了对类型变量T的约束关系.where T:A 表示类型变量是继承于A的,或者是A本身.where T: n ...
- Pycharm画五角星
import turtle turtle.setup(600,400,0,0) turtle.bgcolor('red') turtle.color('yellow') turtle.fillcolo ...
- 用Vue2仿京东省市区三级联动效果
三级联动,随着越来越多的审美,出现了很多种,好多公司都仿着淘宝的三级联动 ,好看时尚,so我们公司也一样……为了贴代码方便,我把写在data里面省市区的json独立了出来,下载贴进去即可用,链接如下 ...
- zepto与jquery冲突的解决
一般是不会把zepto和jquery一起来用的.但有时候要引入一些插件,可能就会遇到这样的问题. jquery noConflict() jquery有一个方法叫noConflict() ,可以把jq ...
- 浏览器本地数据库 IndexedDB 基础详解
一.概述 随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据. 现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的 ...
- Hadoop 数据去重
数据去重这个实例主要是为了读者掌握并利用并行化思想对数据进行有意义的筛选.统计大数据集上的数据种类个数.从网站日志中计算访问等这些看似庞杂的任务都会涉及数据去重.下面就进入这个实例的MapReduce ...
- 大数据与云计算的关系是什么,Hadoop又如何参与其中?Nosql在什么位置,与BI又有什么关系?
大数据与云计算的关系是什么,Hadoop又如何参与其中,Nosql在什么位置,与BI又有什么关系?以下这篇文字讲他们的关系讲的非常清楚. 在谈大数据的时候,首先谈到的就是大数据的4V特性,即类型复杂 ...