.NET 4.0中的泛型协变和反变
随Visual Studio 2010 CTP亮相的C#4和VB10,虽然在支持语言新特性方面走了相当不一样的两条路:C#着重增加后期绑定和与动态语言相容的若干特性,VB10着重简化语言和提高抽象能力;但是两者都增加了一项功能:泛型类型的协变(covariant)和反变(contravariant)。许多人对其了解可能仅限于增加的in/out关键字,而对其诸多特性有所不知。下面我们就对此进行一些详细的解释,帮助大家正确使用该特性。
背景知识:协变和反变
很多人可能不不能很好地理解这些来自于物理和数学的名词。我们无需去了解他们的数学定义,但是至少应该能分清协变和反变。实际上这个词来源于类型和类型之间的绑定。我们从数组开始理解。数组其实就是一种和具体类型之间发生绑定的类型。数组类型Int32[]就对应于Int32这个原本的类型。任何类型T都有其对应的数组类型T[]。那么我们的问题就来了,如果两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。在.NET世界中,唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,数组这种与原始类型转换方向相同的可变性就称作协变(covariant)。
由于数组不支持反变性,我们无法用数组的例子来解释反变性,所以我们现在就来看看泛型接口和泛型委托的可变性。假设有这样两个类型:TSub是TParent的子类,显然TSub型引用是可以安全转换为TParent型引用的。如果一个泛型接口IFoo<T>,IFoo<TSub>可以转换为IFoo<TParent>的话,我们称这个过程为协变,而且说这个泛型接口支持对T的协变。而如果一个泛型接口IBar<T>,IBar<TParent>可以转换为T<TSub>的话,我们称这个过程为反变(contravariant),而且说这个接口支持对T的反变。因此很好理解,如果一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫反变性。你记住了吗?
.NET 4.0引入的泛型协变、反变性
刚才我们讲解概念的时候已经用了泛型接口的协变和反变,但在.NET 4.0之前,无论C#还是VB里都不支持泛型的这种可变性。不过它们都支持委托参数类型的协变和反变。由于委托参数类型的可变性理解起来抽象度较高,所以我们这里不准备讨论。已经完全能够理解这些概念的读者自己想必能够自己去理解委托参数类型的可变性。在.NET 4.0之前为什么不允许IFoo<T>进行协变或反变呢?因为对接口来讲,T这个类型参数既可以用于方法参数,也可以用于方法返回值。设想这样的接口
Interface IFoo(Of T) Sub Method1(ByVal param As T) Function Method2() As T End Interface |
interface IFoo<T> { void Method1(T param); T Method2(); } |
如果我们允许协变,从IFoo<TSub>到IFoo<TParent>转换,那么IFoo.Method1(TSub)就会变成IFoo.Method1(TParent)。我们都知道TParent是不能安全转换成TSub的,所以Method1这个方法就会变得不安全。同样,如果我们允许反变IFoo<TParent>到IFoo<TSub>,则TParent IFoo.Method2()方法就会变成TSub IFoo.Method2(),原本返回的TParent引用未必能够转换成TSub的引用,Method2的调用将是不安全的。有此可见,在没有额外机制的限制下,接口进行协变或反变都是类型不安全的。.NET 4.0改进了什么呢?它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围。我们看到,如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对反变相容。如下所示:
Interface ICo(Of Out T) Function Method() As T End Interface Interface IContra(Of In T) Sub Method(ByVal param As T) End Interface |
interface ICo<out T> { T Method(); } interface IContra<in T> { void Method(T param); } |
可以看到C#4和VB10都提供了大同小异的语法,用Out来描述仅能作为返回值的类型参数,用In来描述仅能作为方法参数的类型参数。一个接口可以带多个类型参数,这些参数可以既有In也有Out,因此我们不能简单地说一个接口支持协变还是反变,只能说一个接口对某个具体的类型参数支持协变或反变。比如若有IBar<in T1, out T2>这样的接口,则它对T1支持反变而对T2支持协变。举个例子来说,IBar<object, string>能够转换成IBar<string, object>,这里既有协变又有反变。
在.NET Framework中,许多接口都仅仅将类型参数用于参数或返回值。为了使用方便,在.NET Framework 4.0里这些接口将重新声明为允许协变或反变的版本。例如IComparable<T>就可以重新声明成IComparable<in T>,而IEnumerable<T>则可以重新声明为IEnumerable<out T>。不过某些接口IList<T>是不能声明为in或out的,因此也就无法支持协变或反变。
下面提起几个泛型协变和反变容易忽略的注意事项:
1. 仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2. 值类型不参与协变或反变,IFoo<int>永远无法变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3. 声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。
协变和反变的相互作用
这是一个相当有趣的话题,我们先来看一个例子:
Interface IFoo(Of In T) End Interface Interface IBar(Of In T) Sub Test(ByVal foo As IFoo(Of T)) '对吗? End Interface |
interface IFoo<in T> { } interface IBar<in T> { void Test(IFoo<T> foo); //对吗? } |
你能看出上述代码有什么问题吗?我声明了in T,然后将他用于方法的参数了,一切正常。但出乎你意料的是,这段代码是无法编译通过的!反而是这样的代码通过了编译:
Interface IFoo(Of In T) End Interface Interface IBar(Of Out T) Sub Test(ByVal foo As IFoo(Of T)) End Interface |
interface IFoo<in T> { } interface IBar<out T> { void Test(IFoo<T> foo); } |
什么?明明是out参数,我们却要将其用于方法的参数才合法?初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。现在我们考虑IBar<string>,它应该能够协变成IBar<object>,因为string是object的子类。因此IBar.Test(IFoo<string>)也就协变成了IBar.Test(IFoo<object>)。当我们调用这个协变后方法时,将会传入一个IFoo<object>作为参数。想一想,这个方法是从IBar.Test(IFoo<string>)协变来的,所以参数IFoo<object>必须能够变成IFoo<string>才能满足原函数的需要。这里对IFoo<object>的要求是它能够反变成IFoo<string>!而不是协变。也就是说,如果一个接口需要对T协变,那么这个接口所有方法的参数类型必须支持对T的反变。同理我们也可以看出,如果接口要支持对T反变,那么接口中方法的参数类型都必须支持对T协变才行。这就是方法参数的协变-反变互换原则。所以,我们并不能简单地说out参数只能用于返回值,它确实只能直接用于声明返回值类型,但是只要一个支持反变的类型协助,out类型参数就也可以用于参数类型!换句话说,in参数除了直接声明方法参数之外,也仅能借助支持协变的类型才能用于方法参数,仅支持对T反变的类型作为方法参数也是不允许的。要想深刻理解这一概念,第一次看可能会有点绕,建议有条件的情况下多进行一些实验。
刚才提到了方法参数上协变和反变的相互影响。那么方法的返回值会不会有同样的问题呢?我们看如下代码:
Interface IFooCo(Of Out T) End Interface Interface IFooContra(Of In T) End Interface Interface IBar(Of Out T1, In T2) Function Test1() As IFooCo(Of T1) Function Test2() As IFooContra(Of T2) End Interface |
interface IFooCo<out T> { } interface IFooContra<in T> { } interface IBar<out T1, in T2> { IFooCo<T1> Test1(); IFooContra<T2> Test2(); } |
我们看到和刚刚正好相反,如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变。这就是方法返回值的协变-反变一致原则。也就是说,即使in参数也可以用于方法的返回值类型,只要借助一个可以反变的类型作为桥梁即可。如果对这个过程还不是特别清楚,建议也是写一些代码来进行实验。至此我们发现协变和反变有许多有趣的特性,以至于在代码里in和out都不像他们字面意思那么好理解。当你看到in参数出现在返回值类型,out参数出现在参数类型时,千万别晕倒,用本文的知识即可破解其中奥妙。
总结
经过本文的讲解,大家应该已经初步了解的协变和反变的含义,能够分清协变、反变的过程。我们还讨论了.NET 4.0支持泛型接口、委托的协变和反变的新功能和新语法。最后我们还套了论的协变、反变与函数参数、返回值的相互作用原理,以及由此产生的奇妙写法。我希望大家看了我的文章后,能够将这些知识用于泛型程序设计当中,正确运用.NET 4.0的新增功能。祝大家使用愉快!
http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html
转载于:https://www.cnblogs.com/anorthwolf/archive/2010/04/02/1703365.html
.NET 4.0中的泛型协变和反变相关推荐
- C#2.0中的泛型学
之前对于泛型一直没有个系统的学习,只是懂得如何调用而已,今天就结合MSDN和网上的一些文章对C#2.0的泛型进行深入研究. 1. 概述 泛型是 2.0 版 C# 语言和公共语言运行库 (CLR) 中的 ...
- ASP.NET Core 3.1 Web API和EF Core 5.0 中具有泛型存储库和UoW模式的域驱动设计实现方法
目录 介绍 背景 领域驱动设计 存储库模式 工作单元模式 使用代码 创建空白解决方案和解决方案架构 添加和实现应用程序共享内核库 PageParam.cs 在Entity Framework Core ...
- [读书笔记]C#学习笔记七: C#4.0中微小改动-可选参数,泛型的可变性
前言 下面就开始总结C#4.0的一些变化了, 也是这本书中最后的一点内容了, 这一部分终于要更新完了. 同时感觉再来读第二遍也有不一样的收获. 今天很嗨的是武汉下雪了,明天周六,一切都是这么美好.哈哈 ...
- [翻译]C#中的泛型 (From dotNet SDK 2.0 Beta1)
来源:Mircrosoft.NET 2.0 Beta1 SDK 翻译:Jim Xu 日期: 2004-11-2 泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.N ...
- 泛型--协变与逆变(转)
对于泛型的知识,一直比较模糊,现在有机会整理一下,突发发现C#还有很多你不知道的东东,继续.NET FrameWork中泛型的协变与逆变: 1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的 ...
- JDK 5.0 中的泛型类型学习
JDK 5.0 中增加的泛型类型,是 Java 语言中类型安全的一次重要改进.但是,对于初次使用泛型类型的用户来说,泛型的某些方面看起来可能不容易明白,甚至非常奇怪.在本月的"Java 理论 ...
- Kotlin语言中的泛型设计哲学
Kotlin语言的泛型设计很有意思,但并不容易看懂.关于这个部分的官方文档,我反复看了好几次,终于弄明白Kotlin语言泛型设计的背后哲学.这篇文章将讲述Kotlin泛型设计的整个思考过程及其背后的哲 ...
- 聊聊Java中的泛型
聊聊Java中的泛型 文章目录 聊聊Java中的泛型 参考资料 一.概述 1. 泛型的定义 1.1 定义 1.2 常见形式 2. 为什么需要泛型 3. 泛型的优点 4. 泛型的使用 4.1 泛型类 4 ...
- Scala系列-5、scala中的泛型、actor、akka
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 传送门:大数据系列文章目录 目录 scala的 泛型 给方法定义泛型 给类定义泛型 泛型的上下 ...
最新文章
- 谈谈Python那些不为人知的冷知识(一)
- 网络名称空间 实例研究 veth处于不同网络的路由问题
- 拜托,别再问我什么是堆了!
- java怎么把随机数放入数组_Java学习:集合的使用与数组的区别
- 009Linux密码故障排除
- LintCode-A + B 问题
- 点名册_骑士新书《万界点名册》十万收藏火爆气势不减当年修真聊天群
- Calendar的api方法
- Mac中Python版本随意切换终极指南,简单到爆,什么都不用设置
- 拓端tecdat|R语言分段线性回归分析预测车辆的制动距离
- euraka有哪些组件_SpringCloud及其五大常用组件之Eureka和Zuul
- 项目采集自动刷新 php,PHP168 CMS自动采集-PHP168 CMS自动更新-齐博CMS站群管理系统...
- RetinaFace+ArcFace人脸识别测试
- Asp.net页面跳转的方法
- 云班课python测试答案_智慧职教云课堂APPPython程序设计题目答案
- 2018第二十二届软博会将于6月在京拉开帷幕
- 多层感知机预测电池SOH值
- windows下通过关键字批量删除远程k8s下deployment及pods
- 超大跨度桥梁建设如何突破特异风致灾风险的困局?
- 【大数据技术】centos7下配置hive连接mysql,mysql-connector-java-8.0.26-1.el7.noarch.
热门文章
- ssm read time out的原因_为什么得肝病的男人越来越多?爱喝酒不是原因,或跟老婆有关系!...
- php 数组 闭包,PHP如何用array_filter加闭包函数过滤数组?
- Java打印出所有的水仙花数
- Java判断100到200之间所有的素数,并且输出这些素数
- 不属于处理数据的计算机应用,计算机应用基础6
- mysql通用mapper_通用Mapper(Mybatis)
- 课堂考试作弊检测系统,情绪识别、表情识别和人脸识别结合
- NLP最新趋势,7个主流业务场景!
- 基于谷歌街景多位数字识别技术:TensorFlow的车牌号识别系统
- python中怎样使用re模块_python如何导入re模块