VS的推出会为我们带来新版本的C#了解C#中的新功能有助于我们利用编码它还能够帮助我们了解程序中正在出现而下一代的C#有可能会解决的错误最终这样的实践可以帮助我们在现有的知识结构上创建适应C#的业务
在本文中我们关注的是C# 中的协变性和逆变性
恆定性协变性和逆变性
在进一步研究问题之前我们先解释一下恆定性协变性逆变性参数以及返回类型这些概念的意思大家对这些概念应该是熟悉的即便那你可能并不能把握这些概念的正式定义
如果你必须使用完全匹配正式类型的名称那么返回的值或参数是不变的如果你能够使用更多的衍生类型作为正式参数类型的代替物那么参数是可变的如果你能够将返回的类型分配给拥有较少类型的变量那么返回的值是逆变的
在大多数情况下C#支持协变参数和逆变的返回类型这一特性也符合其他所有的对象指向型语言事实上多态性通常是建立在协变和逆变的概念之上的直观上我们发现是可以将衍生的类对象发送给任何期望基类对象的方法比较衍生的对象也是基类对象的实例本能地我们也清楚我们可以将方法的结果保存在拥有较少衍生对象类型的变量中例如你可能会需要对这段代码进行编译
public static void PrintOutput(object thing) {if (thing != null)ConsoleWriteLine(thing);}// elsewhere:PrintOutput();PrintOutput(This is a string);
这段代码之所以有效是因为参数类型在C#中具有协变性你可以将任意方法保存在类型对象的变量中因为C#中返回类型是逆变的
object value = SomeMethod();
如果在NET推出后你已经了解C#或VBNET那么你应该很熟悉以上的内容但是规则发生了一些改变在很多方法中你直觉上认为有效的其实不然随着你渐渐深入了解会发现你曾经认为是漏洞的东西很可能是该语言的说明现在是时候解释一下为什么集合以不同的方式工作以及未来将发生些什么变化
基于对象的集合
NET x集合(ArrayListHashTableQueue等)可以被视为具有协变性遗憾的是它们不具有安全的协变性事实上它们具有恆定性不过由于它们向SystemObject保存了参考它们看上去像是具有了协变性和逆变性举几个例子就可以说明这个问题
你可以认为这些集合是协变的因为你可以创建一个员工对象的数组列表然后使用这个列表作为任意方法的参数这些方法使用的是类型数组列表的对象通常这种方法很有效这个方法可能能够与数组列表连用
private void SafeCovariance(ArrayList bunchOfItems) {foreach(object o in bunchOfItems)ConsoleWriteLine(o);// reverse the items:int start = ;int end = bunchOfItemsCount ;while (start < end){object tmp = bunchOfItems[start];bunchOfItems[start] = bunchOfItems[end];bunchOfItems[end] = tmp;start++;end;}foreach(object o in bunchOfItems)ConsoleWriteLine(o);}
这个方法是安全的因为它没有改变集合中任何对象的类型它列举了集合并将集合中已有的项目移动到了不同索引不过并未改变任何类型因此这个方法适用于所有实例但是数组列表和其他传统的NET x集合不会被视为安全的协变看这一方法
private void UnsafeUse(ArrayList stuff) {for (int index = ; index < stuffCount; index++)stuff[index] = stuff[index]ToString();}
这是对保存在集合中的作出的更深一层的假设当方法存在时候集合包含了类型字符串的对象或许这不再是原始集合中的类型事实上如果原始集合包含这些字符串那么方法就不会产生效果
否则它会将集合转换为不同的类型下列使用实例显示了在调用方法的时候遇到的各种问题此处一列数字被发送到了UnsafeUse而数字正是在此处被转换成了字符串的数组列表调用以后呼叫代码会尝试再一次创建能够导致InvalidCastException的项目
// usage: public void DoTest(){ArrayList collection = new ArrayList(){ };SafeCovariance(collection);// create the sum:int sum = ;foreach (int num in collection)sum += num;ConsoleWriteLine(sum);UnsafeUse(collection);// create the sum:sum = ;try{foreach (int num in collection)sum += num;ConsoleWriteLine(sum);}catch (InvalidCastException){ConsoleWriteLine(Not safely covariant);}}
这个例子表明虽然典型的集合是不变的但是你可以视它们为可变或可逆变不过这些集合并非安全可变编译器难保不会出现失误
数组
作为参数使用的时候数组时而可变时而不可变和典型集合一样数组具有非安全的协变性首先只有包含了参考类型的数组可以被视为具有协变性或逆变性值类型的数组通常不可变即便是调用一个期望对象数组的方法时也是如此这一方法可以与其他任何参考类型的数组一起调用但是你不能向其发送整数数组或其他数值类型
private void PrintCollection(object[] collection) {foreach (object o in collection)ConsoleWriteLine(o);}
只要你限制引用类型数组就会具有协变性和逆变性但是仍然是不安全的你将数组视为可变或逆变的次数越多越会发现你需要处理ArrayTypeMismatchException让我们检查其中的一些方法数组参数是可变的但却是非安全协变检查下列不安全的方法
private class B {public override string ToString(){return This is a B;}}private class D : B{public override string ToString(){return This is a D;}}private class D : B{public override string ToString(){return This is a D;}}private void DestroyCollection(B[] storage){try{for (int index = ; index < storageLength; index++)storage[index] = new D();}catch (ArrayTypeMismatchException){ConsoleWriteLine(ArrayTypeMismatch);}}
下面的调用顺序会引发循环以抛出一个
ArrayTypeMismatch例外
D[] array = new D[]{ new D()new D()new D()new D()new D()new D()new D()new D()new D()new D()};DestroyCollection(array);
当我们将两个板块集合起来看时就一目了然了调用页面创建了一个D 对象数组然后调用了期望B对象数组的方法因为数组是可变的你可以将D[]发送到期望B[]的方法但是在DestroyCollection()里面可以修改数组在本例中它创建了用于集合的新对象类型D的对象这在该方法中是允许的D对象可以保存在B[]中因为D是由B衍生出来的但是其结合往往会引发错误当你引入一些返回数组储存的方法并视其为逆变值时同样的事情也会发生向这样的代码才能有效
B[] storage = GenerateCollection(); storage[] = new B();
但是如果GenerateCollection的内容向这样的话那么当storage[]要素被设置到B对象中它会引发ArrayTypeMismatch异常
泛型集合
数组被当作是可变和可逆变即便是不安全的NETx集合类型是不可变的但是将参考保存到了SystemsObjectNETx中的泛型集合并且被视为不可变这意味着你不能够替代包含有较多衍生对象的集合最好你试一试下面的代码
private void WriteItems(IEnumerable< object> sequence) {foreach (var item in sequence)ConsoleWriteLine(item);}
你要知道自己可能会和其他执行IEnumberable< T>集合一起对其进行调用因为任何T必须由对象衍生这或许是你的期望但是由于泛型是不变的下面的操作将无法进行编译
IEnumerable< int> items = EnumerableRange( );
WriteItems(items); // generates CS CS
你也不能将泛型集合类型视为可逆变这行代码之所以不能进行编译是因为分配返回数值的时候你不能将IEnumberable< T>转换成IEnumberable< object>
IEnumerable< object> moreItems =
EnumerableRange( );
你或许认为IEnumberable< int>衍生自IEnumberable< object>但是事实不然IEnumberable< int>是一个基于IEnumberable< T>泛型类定义的闭合泛型类
它们不会相互衍生因此没有关联性而且你也不能视其具有可变性即便在两个类型参数之间具备关联性使用类型参数的泛型类型不会对这种关联有响应
C#以不变的方式对待泛型显示出了该语言的强大优势最重要的是你不能在数组和x集合中出错一旦你编译了泛型代码你就能够很好地利用这些代码了这与C#的传统具有一致性因为它利用了编译器来删除代码中可能存在的漏洞
但是对于对于强效输入的依赖性显示出了一定的局限性上文显示的关于泛型转换的构造看上去是有效的但是你不会想将其转换为NETx集合和数组中使用的行为我们真正想要的是仅在它运行的时候将泛型类型视作是可变的或可逆变的而不是用运行时错误代替编译时错误的时候