谈谈.NET的协变和逆变
伴随 Visual Studio2010 的发布, C# 这门语言提供一些新的特性,包含协变( Covariant )和逆变( Contravariant )、动态( Dynamic )和 DLR 、命名参数和可选参数、索引属性、 COM 调用优化和嵌入 COM 互操作类型。写本文的目的主要是探讨下泛型类型的协变和逆变,按照以往版本 .NET 新特性的增加,一般是由新的关键字、 Attribute 来标注,继而编译器或者 .NET Runtime 负责解析执行。这两个新特性也是如此,两个关键字 in/out 。
目录
1. 协变逆变的追本溯源
2. 协变逆变的 深入分析
3. 协变逆变的场景应用
4. 总结
一. 追本溯源
协变和逆变需 .NET Runtime 的支持,但是在以往的几个 .NET 版本中 , 都是提供这种支持。可能大家都纳闷了, C# 只是支持 .NET 平台的一门语言,这两者关系表明 C# 语言不支持,我们只有干瞪眼,还好沉寂了好几个版本之后,千呼万唤始出来。简明分析下应用场景变迁:以前都是面向对象编程,现在逐渐面向服务编程,泛型类型大量在程序设计结构中的使用,以及 .NET3.0 LINQ 等等应用活跃。还是举一个 code 场景,对于泛型接口 IEnumerable<T> 和 IEnumerator<T>, 我们经常在 foreach 循环进行操作,为了保证服务接口的共用性、稳定性、代码重用,需要对类型 T 进行“安全”的隐性转换或者强制转换。以往 .NET 版本都需允许泛型接口的协变和逆变的。因为这会造成类型安全问题:类 Dogs 继承于类 Mammals ,对于 List<T> , 我们是不可能用 List< Mammals> 来代替 List<Dogs> 的。有人会提问了,为什么 IEnumerable< Mammals > 可以代替 IEnumerable<Dogs> 呢? 下文会给出解答。
尽管以前的 C# 不支持协变和逆变,我们还是可以找到一些影子的。
(1) 例如方法的参数(引用类型)都是可以协变的
static void Main( string [] args)
{
DoSomething( new Dogs());s
}
static void DoSomething(Mammals mammals)
{
Console.WriteLine(mammals.ToString());
}
(2) 再如方法的返回值(引用类型)也是可以协变的,但是不可以逆变
static void Main( string [] args)
{
Mammals mammals = GetMammals();
}
static Dogs GetMammals()
{
return new Dogs();
}
(3) 数组也是支持协变的
Mammals[] dogss = new Mammals[] { new Dogs(), new Mammals() };
PS :数组支持协变,只能用于引用类型。如果把 Mammals 数组赋予 object 数组, Mammals 数据就可以使用派生自 object 的任何元素。但是有一个特例,编译器是允许把字符串传递给数组元素,但是因为 object 数组引用 Mammals 数组,所以就会出现一个运行时的异常 。
二. 深入分析
首先来看下协变和逆变的概念。
(1) 什么是协变?
修饰类型参数 T 的被定义为 out 关键字。当编译器遇到此关键字时,会将 T 标记为协变,并检查接口定义中使用的 T 是否符合规则(即,它们是否仅在输出位置使用,这也就是 out 关键字的由来)。为什么将这种特性称为协变呢?我们可以通过绘制箭头,更加形象的看出,由于 Dogs 和 Mammals 这两个类之间存在继承关系,从 Dogs 到 Mammals 存在隐式引用转换。
IEnumerable<Dogs> ds= GetDogs();
IEnumerable<Mammals> ms = ds;
Dogs → Mammals
由于 .NET4.0 中 IEnumerable<T> 支持协变,可以表示为 IEnumerable<out T> ,因此 IEnumerable<Dogs> 到 IEnumerable<Mammals> 之间也存在隐式引用转换。
IEnumerable< Dogs > → IEnumerable< Mammals >
我们可以很清晰的看出 Dogs – >Mammals 的转换 和 IEnumerable< Dogs > → IEnumerable< Mammals > 的转换完全是同步的,一致的,我们可以很方便简单的判断为协变。
当类型 T 由子类型转化为父类型时,也就是存在隐性转换时,我们称之为协变。
(2) 什么是逆变
NET4.0 IComparable<T> 接口是支持逆变的。可以形象解释为 IComparable<in T> 。大家可以看以下场景
IComparable<Mammals> mc= GetMammals();
IComparable<Dogs> dc= mc;
而转换关系则是这样的
Dogs → Mammals
IComparable< Dogs > ← IComparable<Mammals>
当类型 T 由父类型转化为子类型时,也就是强制转换时,我们称之为逆变。
(3) 总结分析
众所周知, .NET 世界中类型分为值类型和引用类型,值类型存放在堆栈上, 而引用类型存放在托管堆。 .NET 中的每个实例对象都有属于它的类型 type , type 包含类型本身一些属性如 field 等等,同时也包含该类型的行为动作。在当前 AppDomain 中,每个 type 都是独一无二的。这就涉及到了类型和类型的绑定。正如我们在引用类型的初始化时,首先在当前 load heap 中查找该 type ,假如该 type 被加载到当前 appDomain 中,而不是 SystemDomain 和 SharedDomain , CLR 计算当前即将被创建的 Instance 所需内存的大小,在当前 managed heap 中申请一块连续的内存空间,这其中涉及该 Instance 两个额外的成员 TypeHandle 和 SyncBlockIndex 。 Managed heap 是存档托管对象,而 loader heap 确实存放对象的类型的。之所以谈这些就是为了更好的理解为何协变和逆变是针对引用类型的。
我们操作引用对象实际上,操作的也是引用对象存储位置的指针。当两个具有继承和被继承关系的对象相互转换的时候,其实也就是对象地址变更(当然还有其他操作机制),子类型和父类型在存储内容上也就是自身属性的一些不同,子类或扩展属性,或扩展行为动作。也就是说子类型和父类型在存储内容的结构上有相同,相通之处。相反值类型由于存放在堆栈上,生成专属的封闭构造类型,自身的设计布局决定了协变和逆变无法用于值类型。
三. 场景应用
现实业务需求往往是引发技术变更的前因,面向服务 SOA 崛起,使得泛型在程序设计中大量的使用。用面向对象设计方式进行细颗粒功能的实现,继而用粗颗粒的服务契约方式进行服务功能的发布,泛型接口和泛型委托的用处显而易见。其实委托也可看成是单独一个方法实现的接口。 .NET4.0 种接口 IEnumerable<T> 、 IComparable<T> 、 Func<T> 、 Action<T> 等等都可协变或逆变。
回过头说下为何 IList<T> 无法进行协变。首先在 IEnumerable<T> 内部有个 GetEnumerator 属性, IEnumerator<T> 有个 Current 只读属性。这两个均返回当前的 T 的实例,也就是当前 T 指针地址。 IList<T> 内部维护的是一个强类型列表, C# 语言使用 this 关键字来定义索引器而不是通过实现 IList<T>.Item 属性,该属性支持读写操作,返回的是 object ,并且 IList<T>.Add 方法参数是一个强类型的 T 。任何非 T 类型的插入都回抛出 ArgumentException 。
协变在接口中的使用,我们先定义如下的接口:
interface ICovariant < out T >
{
T Method();
}
interface IContravariant < in T >
{
void Method(T t);
}
out 是用来描述作为返回值的类型参数, in 是用来描述仅能作为方法参数的类型参数。我们也不能单纯的说一个接口是支持协变或者逆变,因为存在以下的情况:
interface ICC<in T1, out T2>
如果泛型接口发生嵌套了类似如下:
interface ICovariant < out T >
{
}
interface IContravariant < in T >
{
void Method(ICovariant < T > param);
}
上述是正确,我们不可进行如下操作
interface ICovariant < in T >
{
}
interface IContravariant < in T >
{
void Method(ICovariant < T > param);
}
Why ?从协变和逆变本质上说,两个具有继承关系的对象之间相互隐性或者强制转换,我们在一个接口 IContravariant 对 T 进行了逆变,是对该对象的强制转换为他的子类,如果在该接口的方法中我们再次对 T 进行逆变,其一该方法的输入参数要求强类型 T ,而不是他的父类,其二该子类对象能再次转换为什麽呢?协变也是同样的道理。我们把协变和逆变看成是两个原子性的操作,换成物理概念也就是两个物体相互作用,相互影响。
如果我们在一个接口中对 T 进行了协变,那么在该接口的方法中,我们不可再对其进行协变;如果我们在一个接口中对 T 进行了逆变,那么在该接口的方法中,我们不可再对其进行逆变。
四 . 总结
(1) 协变和逆变只能用于引用类型,就算 IFoo<int> à IFoo<object> 也是不不可行的。
(2) in 和 out 修饰类型参数时,只能用于泛型接口或泛型委托,泛型类或者泛型方法却不行。
(3)in 和 out 修饰属性时, in 属于强制输入符合某种类型的对象,所以只能用于只写属性。 Out 返回属于某一类具有集成关系的族的对象。所以只能用于只读属性。
泛型从某种程度上说,是对面向对象设计中多态和继承最有力的诠释。而协变和逆变将面向服务,契约的思想带到了程序设计中的细颗粒层面。我们可以通过泛型接口和泛型委托构造更为灵活的服务。
文章首发站点: www.agilesharp.com IT创业产品互推平台
SQL性能调优在线视频讲座,大家有兴趣去看看。 http://www.agilesharp.com/c/sqlprofiler.aspx
作者: Leo_wl
出处: http://www.cnblogs.com/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息