# [C# 基础知识系列]专题九: 深入理解泛型可变性
**引言:**
在C# 2.0中泛型并不支持可变性的(可变性指的就是协变性和逆变性),我们知道在面向对象的继承中就具有可变性,当方法声明返回类型为Stream,我们可以在实现中返回一个FileStream的类型,此时就存在一个隐式的转化——从**FileStream类型(子类引用)——>Stream类型(父类引用),**并且引用类型的数组也存在这种从**子类引用——>父类引用**的转化,例如string[] 可以转化为object[](即这样的代码是可以通过编译的:string[] strs =new string[3]; object[] objs =strs;),此时我们肯定会想是否泛型中的泛型参数也可以支持这样的转化呢?然而在C# 2.0中是不支持的,但是就是因为有这样的需求,所以微软也考虑到这个问题的, 所以在C# 4.0中就引入了泛型的协变和逆变性。下面就具体来介绍下C# 4.0 中对协变和逆变的具体内容有哪些的。
**一、协变性**
协变性指的是——泛型类型参数可以从一个**派生类隐式转化为基类(大家可以这样记忆的,协变性即和谐的变化,生活中我们一般会说子女长的像他们的父母,这样听起来会感觉比较和谐点,这样就很容易记住协变了)**,在C#4.0中引入**out**关键字来标记泛型参数支持协变性。为了更好的说明泛型的协变性,下面就以.Net类库的中**public interface IEnumerable<out T>**这个接口来演示一个例子来帮助大家理解泛型协变:
```
List<object> listobject = new List<object>();
List<string> liststrs = new List<string>();
// AddRange方法接收的参数类型为IEnumerable<T> collection
// 下面的代码是传入的是List<string>类型的参数。
// 在MSDN中可以看出这个接口的定义为——IEnumerable<int T>。
// 所以 IEnumerable<T>泛型类型参数T支持协变性,所以可以
// 将List<string>转化为IEnumerable<string>(这个是继承的协变性支持的)
// 又因为这个IEnumerable<in T>接口委托支持协变性,所以可以把IEnumerable<string>转化为——>IEnumerable<object>类型。
// 所以编译器验证的时候就不会出现类型不能转化的错误了。
listobject.AddRange(liststrs); //成功
liststrs.AddRange(listobject); // 出错
```
代码中如果使用 这代码时 liststrs.AddRange(listobject); 就会出现编译时错误(无法从List<object>转换为IEnumerable<string>,因为List<object>可以因为继承的协变性转化为IEnumerable<object>,但是因为IEnumerable<out T>不支持逆变,即从object到string的转化,所以此时就会产生下面图中的错误了。), 错误提示截图如下:
![](https://box.kancloud.cn/2016-01-23_56a2eb2a80f70.png)
**二、逆变性**
逆变性指的是——泛型类型参数可以从一个**基类隐式转化为派生类(可以从生活中的例子来帮助大家记忆逆变的——如果说父母长的像他们的子女的话肯定觉得别扭,在高中语文中经常会找这样的语病的)**,在C# 4.0中引入**in**关键字来标记泛型参数支持逆变性.为了更好的说明泛型的逆变性,下面就以.Net类库的中接口**public interface IComparer<in T>**来演示一个例子来帮助大家理解泛型逆变**:**
```
class Program
{
static void Main(string[] args)
{
List<object> listobject = new List<object>();
List<string> liststrs = new List<string>();
// AddRange方法接收的参数类型为IEnumerable<T> collection
// 下面的代码是传入的是List<string>类型的参数。
// 在MSDN中可以看出这个接口的定义为——IEnumerable<int T>。
// 所以 IEnumerable<T>泛型类型参数T支持协变性,所以可以
// 将List<string>转化为IEnumerable<string>(这个是继承的协变性支持的)
// 又因为这个IEnumerable<in T>接口委托支持协变性,所以可以把IEnumerable<string>转化为——>IEnumerable<object>类型。
// 所以编译器验证的时候就不会出现类型不能转化的错误了。
listobject.AddRange(liststrs); //成功
////liststrs.AddRange(listobject); // 出错
IComparer<object> objComparer = new TestComparer();
IComparer<string> objComparer2 = new TestComparer();
// List<string>类型的 liststrs变量的sort方法接收的是IComparer<string>类型的参数
// 然而下面代码传入的是 IComparer<object>这个类型的参数,要编译成功的话,必须能够转化为IComparer<string>这个类型
// 正是因为IComparer<in T>泛型接口支持逆变,所以支持object转化为string类型
// 所以下面的这行代码可以编译通过,在.Net 4.0之前的版本肯定会编译错误,
// 大家可以把项目的目标框架改为.Net Framework 3.5或者更加低级的版本
// 这样下面这行代码就会出现编译错误,因为泛型的协变和逆变是C# 4.0 中新增加的特性,而.Net 4.0对应于C# 4.0。
liststrs.Sort(objComparer); // 正确
// 出错
////listobject.Sort(objComparer2);
}
}
public class TestComparer : IComparer<object>
{
public int Compare(object obj1,object obj2)
{
return obj1.ToString().CompareTo(obj2.ToString());
}
}
```
上面代码中如果使用 listobject.Sort(objComparer2);时,就会出现编译错误,错误原因看过上面协变中错误原因的解释应该都可以明白的,下面是错误的截图:
![](https://box.kancloud.cn/2016-01-23_56a2eb2a9405e.png)
为了进一步说明泛型的协变和逆变是在C# 4.0中(C# 4.0即对于.net Framework 4.0)的版本都不支持泛型的协变和逆变,大家从MSDN中也可以发现的。下面是一张比较的截图(大家可以自己具体去MSDN上查看的, 当版本改为3.5或更低级的版本时,看下泛型的定义是不是没有out或in关键字,即之前的版本不支持泛型的可变性):
![](https://box.kancloud.cn/2016-01-23_56a2eb2ae5f34.png)
**三、协变和逆变的注意事项**
并不是所有类型都支持泛型的协变和逆变的, 下面列出泛型的协变和你逆变中值得注意和明确的地方:
1\. 只有接口和委托支持协变和逆变(如Func<out TResult>,Action<in T>),类或泛型方法的类型参数都不支持协变和逆变。
2\. 协变和逆变只适用于引用类型,值类型不支持协变和逆变(因为可变性存在一个引用转换,而值类型变量存储的就是对象本身,而不是对象的引用),所以List<int>无法转化为Ienumerable<object>.
3\. 必须显示用in或out来标记类型参数。
4\. 委托的可变性不要再多播委托中使用,相信这点很多人都没有注意到的, 下面我举个例子来说明下,当大家遇到这样的问题可以知道为什么:
上面代码可以通过编译,因为泛型Func<out T>支持协变,所以将Func<string>转换为Func<object>类型,但是对象本身仍然为Func<string>类型,然而Delegate.Combine方法要求参数必须为相同类型——否则该方法无法确定要创建什么类型的委托(是Func<string>类型呢还是Func<object>?),所以上面代码在运行时会抛出ArgumetException(错误信息为——委托必须具有相同的类型)。我们可以稍微修改下上面代码来使其不出现运行时错误
**四、小结**
虽然可能这个系列对实际的开发中没有多大的帮助,但是我个人认为基础还是需要打扰,只有基础打好了,才可以让我们飞的更远,更容易掌握新的技术,所以我会一直坚持下去写完这个系列的, 希望对大家巩固基础知识有所帮助。(我觉得尤其是在校学生,应该更加注重基础知识的巩固,然后写一些例子来加深对基础知识的理解)。
本专题到这里也就介绍完了(对于泛型还有一个相当有趣的话题的,就是协变和逆变的相互作用,具体这点内容大家可以参考这篇文章的:[http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html](http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html)(因为我也是从这篇文章中知道这点的, 大家有兴趣的话可以去上面的链接具体看看怎么回事)),下一个专题我将和大家介绍C# 2.0中的另外一个新的特性——**可空类型**。
- C# 基础知识系列
- C# 基础知识系列 专题一:深入解析委托——C#中为什么要引入委托
- C# 基础知识系列 专题二:委托的本质论
- C# 基础知识系列 专题三:如何用委托包装多个方法——委托链
- C# 基础知识系列 专题四:事件揭秘
- C# 基础知识系列 专题五:当点击按钮时触发Click事件背后发生的事情
- C# 基础知识系列 专题六:泛型基础篇——为什么引入泛型
- C# 基础知识系列 专题七: 泛型深入理解(一)
- C# 基础知识系列 专题八: 深入理解泛型(二)
- C# 基础知识系列 专题九: 深入理解泛型可变性
- C#基础知识系列 专题十:全面解析可空类型
- C# 基础知识系列 专题十一:匿名方法解析
- C#基础知识系列 专题十二:迭代器
- C#基础知识 专题十三:全面解析对象集合初始化器、匿名类型和隐式类型
- C# 基础知识系列 专题十四:深入理解Lambda表达式
- C# 基础知识系列 专题十五:全面解析扩展方法
- C# 基础知识系列 专题十六:Linq介绍
- C#基础知识系列 专题十七:深入理解动态类型
- 你必须知道的异步编程 C# 5.0 新特性——Async和Await使异步编程更简单
- 全面解析C#中参数传递
- C#基础知识系列 全面解析C#中静态与非静态
- C# 基础知识系列 C#中易混淆的知识点
- C#进阶系列
- C#进阶系列 专题一:深入解析深拷贝和浅拷贝
- C#进阶系列 专题二:你知道Dictionary查找速度为什么快吗?
- C# 开发技巧系列
- C# 开发技巧系列 使用C#操作Word和Excel程序
- C# 开发技巧系列 使用C#操作幻灯片
- C# 开发技巧系列 如何动态设置屏幕分辨率
- C# 开发技巧系列 C#如何实现图片查看器
- C# 开发技巧 如何防止程序多次运行
- C# 开发技巧 实现属于自己的截图工具
- C# 开发技巧 如何使不符合要求的元素等于离它最近的一个元素
- C# 线程处理系列
- C# 线程处理系列 专题一:线程基础
- C# 线程处理系列 专题二:线程池中的工作者线程
- C# 线程处理系列 专题三:线程池中的I/O线程
- C# 线程处理系列 专题四:线程同步
- C# 线程处理系列 专题五:线程同步——事件构造
- C# 线程处理系列 专题六:线程同步——信号量和互斥体
- C# 多线程处理系列专题七——对多线程的补充
- C#网络编程系列
- C# 网络编程系列 专题一:网络协议简介
- C# 网络编程系列 专题二:HTTP协议详解
- C# 网络编程系列 专题三:自定义Web服务器
- C# 网络编程系列 专题四:自定义Web浏览器
- C# 网络编程系列 专题五:TCP编程
- C# 网络编程系列 专题六:UDP编程
- C# 网络编程系列 专题七:UDP编程补充——UDP广播程序的实现
- C# 网络编程系列 专题八:P2P编程
- C# 网络编程系列 专题九:实现类似QQ的即时通信程序
- C# 网络编程系列 专题十:实现简单的邮件收发器
- C# 网络编程系列 专题十一:实现一个基于FTP协议的程序——文件上传下载器
- C# 网络编程系列 专题十二:实现一个简单的FTP服务器
- C# 互操作性入门系列
- C# 互操作性入门系列(一):C#中互操作性介绍
- C# 互操作性入门系列(二):使用平台调用调用Win32 函数
- C# 互操作性入门系列(三):平台调用中的数据封送处理
- C# 互操作性入门系列(四):在C# 中调用COM组件
- CLR
- 谈谈: String 和StringBuilder区别和选择
- 谈谈:程序集加载和反射
- 利用反射获得委托和事件以及创建委托实例和添加事件处理程序
- 谈谈:.Net中的序列化和反序列化
- C#设计模式
- UML类图符号 各种关系说明以及举例
- C#设计模式(1)——单例模式
- C#设计模式(2)——简单工厂模式
- C#设计模式(3)——工厂方法模式
- C#设计模式(4)——抽象工厂模式
- C#设计模式(5)——建造者模式(Builder Pattern)
- C#设计模式(6)——原型模式(Prototype Pattern)
- C#设计模式(7)——适配器模式(Adapter Pattern)
- C#设计模式(8)——桥接模式(Bridge Pattern)
- C#设计模式(9)——装饰者模式(Decorator Pattern)
- C#设计模式(10)——组合模式(Composite Pattern)
- C#设计模式(11)——外观模式(Facade Pattern)
- C#设计模式(12)——享元模式(Flyweight Pattern)
- C#设计模式(13)——代理模式(Proxy Pattern)
- C#设计模式(14)——模板方法模式(Template Method)
- C#设计模式(15)——命令模式(Command Pattern)
- C#设计模式(16)——迭代器模式(Iterator Pattern)
- C#设计模式(17)——观察者模式(Observer Pattern)
- C#设计模式(18)——中介者模式(Mediator Pattern)
- C#设计模式(19)——状态者模式(State Pattern)
- C#设计模式(20)——策略者模式(Stragety Pattern)
- C#设计模式(21)——责任链模式
- C#设计模式(22)——访问者模式(Vistor Pattern)
- C#设计模式(23)——备忘录模式(Memento Pattern)
- C#设计模式总结
- WPF快速入门系列
- WPF快速入门系列(1)——WPF布局概览
- WPF快速入门系列(2)——深入解析依赖属性
- WPF快速入门系列(3)——深入解析WPF事件机制
- WPF快速入门系列(4)——深入解析WPF绑定
- WPF快速入门系列(5)——深入解析WPF命令
- WPF快速入门系列(6)——WPF资源和样式
- WPF快速入门系列(7)——深入解析WPF模板
- WPF快速入门系列(8)——MVVM快速入门
- WPF快速入门系列(9)——WPF任务管理工具实现
- ASP.NET 开发
- ASP.NET 开发必备知识点(1):如何让Asp.net网站运行在自定义的Web服务器上
- ASP.NET 开发必备知识点(2):那些年追过的ASP.NET权限管理
- ASP.NET中实现回调
- 跟我一起学WCF
- 跟我一起学WCF(1)——MSMQ消息队列
- 跟我一起学WCF(2)——利用.NET Remoting技术开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(3)——利用Web Services开发分布式应用
- 跟我一起学WCF(4)——第一个WCF程序
- 跟我一起学WCF(5)——深入解析服务契约 上篇
- 跟我一起学WCF(6)——深入解析服务契约 下篇
- 跟我一起学WCF(7)——WCF数据契约与序列化详解
- 跟我一起学WCF(8)——WCF中Session、实例管理详解
- 跟我一起学WCF(9)——WCF回调操作的实现
- 跟我一起学WCF(10)——WCF中事务处理
- 跟我一起学WCF(11)——WCF中队列服务详解
- 跟我一起学WCF(12)——WCF中Rest服务入门
- 跟我一起学WCF(13)——WCF系列总结
- .NET领域驱动设计实战系列
- .NET领域驱动设计实战系列 专题一:前期准备之EF CodeFirst
- .NET领域驱动设计实战系列 专题二:结合领域驱动设计的面向服务架构来搭建网上书店
- .NET领域驱动设计实战系列 专题三:前期准备之规约模式(Specification Pattern)
- .NET领域驱动设计实战系列 专题四:前期准备之工作单元模式(Unit Of Work)
- .NET领域驱动设计实战系列 专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
- .NET领域驱动设计实战系列 专题六:DDD实践案例:网上书店订单功能的实现
- .NET领域驱动设计实战系列 专题七:DDD实践案例:引入事件驱动与中间件机制来实现后台管理功能
- .NET领域驱动设计实战系列 专题八:DDD案例:网上书店分布式消息队列和分布式缓存的实现
- .NET领域驱动设计实战系列 专题九:DDD案例:网上书店AOP和站点地图的实现
- .NET领域驱动设计实战系列 专题十:DDD扩展内容:全面剖析CQRS模式实现
- .NET领域驱动设计实战系列 专题十一:.NET 领域驱动设计实战系列总结