.NET 4.0中的泛型协变和反变
转自:http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html
随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的新增功能。祝大家使用愉快!
.NET 4.0中的泛型协变和反变的更多相关文章
- .NET 4.0中的泛型协变和逆变
随Visual Studio 2010 CTP亮相的C#4和VB10,虽然在支持语言新特性方面走了相当不一样的两条路:C#着重增加后期绑定和与动态语言相容的若干特性,VB10着重简化语言和提高抽象能力 ...
- .NET 4.0中的泛型逆变和协变
转载自:http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html:自己加了一些理解 随Visual Studi ...
- .NET 4.0中的泛型的协变和逆变
转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...
- 转载.NET 4.0中的泛型的协变和逆变
先做点准备工作,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义如下: public class Animal { } public ...
- 小改动,大作为——C# 4.0中的微小改动
1.可选参数和命名实参 可选参数和命名实参就如同一对好基友,因为它们经常一起使用. 1.1 可选参数 可选参数重在“可选”,即在调用方法时,该参数可以明确指定实参,也可以不指定.如下代码所示,下面代码 ...
- [读书笔记]C#学习笔记七: C#4.0中微小改动-可选参数,泛型的可变性
前言 下面就开始总结C#4.0的一些变化了, 也是这本书中最后的一点内容了, 这一部分终于要更新完了. 同时感觉再来读第二遍也有不一样的收获. 今天很嗨的是武汉下雪了,明天周六,一切都是这么美好.哈哈 ...
- 编写高质量代码改善C#程序的157个建议——建议43:让接口中的泛型参数支持协变
建议43:让接口中的泛型参数支持协变 除了上一建议中提到的使用泛型参数兼容接口不可变性外,还有一种办法是为接口中的泛型声明加上out关键字来支持协变,如下所示: interface ISalary&l ...
- 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变
在net中json序列化与反序列化 准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...
- ActionScript3.0(AS3)中的泛型数组Vector
Adobe官方并没有"泛型数组"的叫法,这是我自己对Vector的叫法(有点标题党),不过Vector在使用上确实跟c#中的泛型数组有些相似之处. 原作者:菩提树下的杨过出处:ht ...
随机推荐
- delphi 转换sql server 中的 bit类型
FieldByName('e').AsBoolean = false 其中e为 sql server 中的bit类型.
- Canvas处理头像上传
未分类 最近社区系统需要支持移动端,其中涉及到用户头像上传,头像有大中小三种尺寸,在PC端,社区用Flash来处理头像编辑和生成,但该Flash控件的界面不友好而且移动端对Flash的支持不好,考虑到 ...
- hadoop2.0中无法启动datanode的问题
问题描述:在启动datanode进程时,能成功的启动:但用jps查看进程时,发现进程不存在,下面是在datanode日记文件的错误信息 如下图的截屏所示: 主要原因:发生错误的原因:由于把data放在 ...
- web自动化框架之四测试报告的搭建
现状: 看过前面的文章,楼主用的是python,所以在搭建测试报告这块的时候使用的是unittest+htmlTestRunner:然后发现生成出来的报告,总是有那么不完美的地方,比如想增加图片,比如 ...
- 在ubuntu下安装chrome
To add PPA in Ubuntu 14.04 / 13.10 / 13.04 / 12.10 / 12.04 First download and install the key from G ...
- DOM笔记(三):Element接口和HTMLElement接口
一.Element接口 Element接口表示一个元素,该接口扩展自Node接口,自然继承了Node接口的属性和方法,也有一套针对元素的属性和方法. Element接口常见的属性比较少,常用的就是一个 ...
- 1.2……初识Android开发
Android体系结构 Dalvik VM(Android下的java虚拟机)与传统的JVM的区别 传统JVM 基于堆栈的架构 编写.java文件--->编译为.class文件--->打包 ...
- java易混淆概念之类变量、成员变量、局部变量
类变量.成员变量.局部变量 类变量(也叫静态变量)是类中独立于方法之外的变量,用static 修饰.(static表示“全局的”.“静态的”,用来修饰成员变量和成员方法,或静态代码块(静态代码块独 ...
- JSF 2 graphicImage example
In JSF, you can use <h:graphicImage /> tag to render a HTML "img" element. For examp ...
- Web安全--使用Salt + Hash将密码加密后再存储进数据库
转载原地址 http://www.bozhiyue.com/mianshiti/_net/2016/0728/314239.html (一) 为什么要用哈希函数来加密密码 如果你需要保存密码(比如网站 ...