C# 协变和逆变

 

  伴随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返回属于某一类具有集成关系的族的对象。所以只能用于只读属性。

  泛型从某种程度上说,是对面向对象设计中多态和继承最有力的诠释。而协变和逆变将面向服务,契约的思想带到了程序设计中的细颗粒层面。我们可以通过泛型接口和泛型委托构造更为灵活的服务。 

转载:https://kb.cnblogs.com/page/114331/
 
 

C# 协变和逆变

上一篇:C# BYTE[] 与16进制字符串互相转换


下一篇:C# 拼接字符串的几种方式和性能