[C#]浅谈协变与逆变
看过几篇说协变与逆变的博客,虽然都是正确无误的,但是感觉都没有说得清晰明了,没有切中要害。
那么我也试着从我的理解角度来谈一谈协变与逆变吧。
什么是协变与逆变
MSDN的解释:
https://msdn.microsoft.com/zh-cn/library/dd799517.aspx
协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型。
泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。
一开始我总是分不清协变和逆变,因为MSDN的解释实在是严谨有余而易读不足。
其实从中文的字面上来理解这两个概念就挺容易的了:
"协变"即"协调的转变","逆变"即"逆向的转变"。
为什么说"能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型"是协调的,而"能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型"是逆向的呢,看这两行代码:
object o = "";
string s = (string) o;
string类型到object类型,也就是派生类到基类,是可以隐式转换的,因为任何类型向基类的转换都是类型安全的,所以认为这一转变是协调的。
object类型到string类型,也就是基类到派生类,就只能是显式转换,因为对象o的实际类型不一定是string,强制转换不是类型安全的,所以认为这一转变是逆向的。
再看协变与逆变的常见场合:
IEnumerable<object> o = new List<string>();//协变
Action<string> s = new Action<object>((arg)=>{...});//逆变
上例的泛型参数就是分别发生了协调的与逆向的转变。
协变与逆变的作用对象
从定义中可以看到,协变与逆变都是针对的泛型参数,而且
在.NET Framework 4中,Variant类型参数仅限于泛型接口和泛型委托类型。
为什么是接口和委托?先看IEnumerable<T>和Action<T>的声明:
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
} public delegate void Action<in T>(T obj);
IEnumerable中的out关键字给泛型参数提供了协变的能力,Action中的in关键字给泛型参数提供了逆变的能力。
这里的out和in是相对于谁的入和出?不是相对于接口和委托,而是相对于方法体!
看它们的实现:
class MyEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator()
{
yield return default(T);
} IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
} Action<string> myAction = new Action<object>(
(o) =>
{
Console.WriteLine(o.ToString());
});
这样是不是能看出来泛型参数是怎么入和出的了?
那么接口和委托,它们和方法是什么关系呢,它们两个之间又是什么关系,以下纯属个人理解:
接口类型定义了一组方法签名,委托类型定义了一个方法结构(方法签名刨除方法名)。
接口实例和委托实例都包含了一组方法入口。
综上所述,协变与逆变的作用对象是方法体中的泛型参数。
为什么允许协变与逆变
协变和逆变都是类型发生了转换,一旦涉及到类型转换当然就要想类型安全的问题。
协变和逆变之所以可以正常的运转,就是因为这里所涉及到的所有类型转换都是类型安全的!
回头看最开始的四行代码:
object o1 = "";//类型安全
string s1 = (string) o1;//非类型安全
IEnumerable<object> o2 = new List<string>();//协变
Action<string> s2 = new Action<object>((arg)=>{...});//逆变
显然第二行的object到string是非类型安全的,那为什么第四行的object到string就是类型安全的呢?
结合上一个方法体的示例,来看这段代码:
Action<List<int>> myAction = new Action<IList<int>>(
(list) =>
{
Console.WriteLine(list.Count);
});
myAction(new List<int> {, , });
第一行貌似是把IList转换成了List,但是实际上是这样的:
第六行传入的实参是一个List,进入方法体,List被转换成了IList,然后使用了IList的Count属性。
所以传参的时候其实发生的是派生类到基类的转换,自然也就是类型安全的了。
List<string>到IEnumerable<object>的协变其实也是类似的过程:
IEnumerable<Delegate> myEnumerable = new List<Action>
{
new Action(()=>Console.WriteLine()),
new Action(()=>Console.WriteLine()),
new Action(()=>Console.WriteLine()),
};
foreach (Delegate dlgt in myEnumerable)
{
dlgt.DynamicInvoke();
}
实参是三个Action,调用的是Delegate的DynamicInvoke方法,完全的类型安全转换。
最后想说的是,所有死记硬背来的知识,都远远不如充分理解的知识来得可靠。
[C#]浅谈协变与逆变的更多相关文章
- 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变
在net中json序列化与反序列化 准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...
- 再谈对协变和逆变的理解(Updated)
去年写过一篇博客谈了下我自己对协变和逆变的理解,现在回头看发现当时还是太过“肤浅”,根本没理解.不久前还写过一篇“黑”Java泛型的博客,猛一回头又是“肤浅”,今天学习Java泛型的时候又看到了协变和 ...
- Friday Q&A 2015-11-20:协变与逆变
作者:Mike Ash,原文链接,原文日期:2015-11-20译者:Cee:校对:千叶知风:定稿:numbbbbb 在现代的编程语言中,子类型(Subtypes)和超类型(Supertypes)已经 ...
- Java中的协变与逆变
Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...
- C#4.0泛型的协变,逆变深入剖析
C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...
- C#协变和逆变
我们知道在C#中,是可以将派生类的实例赋值给基类对象的.
- C# 泛型的协变和逆变
1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...
- 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事
阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约 阿袁,早!开始工作吧. 阿袁在笔记上写下今天工作清单: 实现一个scala类 ...
- 【转】c# 协变和逆变
本文转自:http://www.cnblogs.com/rr163/p/4047404.html C#的协变和逆变 由子类向父类方向转变是协变,用out关键字标识,由父类向子类方向转变是逆变,用in关 ...
随机推荐
- JAVA的学习内容
http://www.weixueyuan.net/java/rumen/ 安卓学习文档 http://www.runoob.com/w3cnote/android-tutorial-linearla ...
- gitlab中修改项目名称客户端修改方法
如果gitlab项目名称已经修改,对于本地已经克隆下来的仓库,可以使用如下命令进行修改: git remote set-url origin 新的项目路径
- 2018-2019 20165219 网络对抗 Exp5 MSF基础
实验内容 本实践目标是掌握metasploit的基本应用方式,重点常用的三种攻击方式的思路.具体需要完成: 1.1一个主动攻击实践,如ms08_067; (1分) 1.2 一个针对浏览器的攻击,如ms ...
- RHEL 7 下内存管理小记
RHEL 7 下内存管理小记 一.Overview 众所周知,在 Linux 操作系统中有三个目录非常有趣好玩. /dev /run /proc 一个个解释下,/dev 用于对特殊设备(BTW:特殊设 ...
- Android 线刷小白教程
Android 线刷小白教程 再说一遍,绝不使用刷机精灵等软件. 一.概念 安卓系统一般把rom芯片分成7个区,如果再加上内置sd卡这个分区,就是8个: hboot分区----------负责启动. ...
- day 13 课后作业
# -*- coding: utf-8 -*-# @Time : 2019/1/7 18:00# @Author : Endless-cloud# @Site : # @File : day 13 课 ...
- IntelliJ IDEA中可以用Tab键跳出括号和引号吗?
在 Settings->Editor->General->Smart Keys 中勾选 Jump outside closing bracket/quote with Tab 即可.
- AssertJ断言系列<一>
1 - Get AssertJ Core assertions Maven的pom.xml加入如下配置: <dependency> <groupId>org.assertj&l ...
- 初学struts2框架
昨天初学strts2,折腾了一晚上才正好一个简单的应用程序,在这个过程中遇到了很多的问题. 1. struts2的更新太快,所以从图书馆借的书常常会跟不上变化,所以有时执行起来会很麻烦. 2. 如果你 ...
- NPOI设置水平、垂直居中
C#语法: string fs = "@report.xls";//文件路径 FileStream excelPath = File.Open(@fs, FileMode.Open ...