抗变与协变
前言
工作一年了,平时也喜欢看看书,逛逛园子;但说到写博,还真的没有,说到底,只有一个字:懒!现在想改掉这个“毛病”了,希望多把平时工作学习到的知识和遇到的问题记录下来,一是可以梳理自己的思路,加深理解;二是可以向更多的朋友学习和分享;三是可以锻炼自己的写作水平;可谓百利而无一害!
平时偶尔会遇到一些小问题,很多时候都是查了记住,或者简单写写笔记,当时理解就过了,没有形成文档,等过段时间又遇到同样的问题,又要重新去查去理解,甚是麻烦。希望以后把这些东西写成文章,尽管可能是很小的问题,也当做笔记记录。关于c#的一些概念、语法或者规范,就记录在【c#笔记】。由于不是初学边学边记,所以没有一定的时间和学习顺序,只是平时遇到觉得有必要,就记录下来。
一、遇到问题
工作是基于.net3.5开发,实际过程遇到一个问题。假设我们有一个 Base 类,一个 Derived 类,Derived 继承了 Base。如下:
class Base { } class Derived : Base { }
当我用IEnumerable<Base> 作为形参,List<Derived> 作为实参时,发现编译出错了!原本父类作为形参,传递子类是再正常不过的,但在泛型中确编译不通过。
二、探究问题
通常我们在设计参数和返回值都有一个原则,参数要尽可能“泛”,返回值要尽可能的“细”。泛,指得是用接口或者父类作为参数,这样可以接收更多的参数类型;细,指的是返回具体类型,这样可以更好说明方法的作用。
举个例子:
string[] strs = new string[] { "hello", "word" }; //这样的缺点是数组就传递不了了,还要调用一次 ToList() static void Test_1(List<string> list) { } //正确的做法,应该用IEnumerable<T> static void Test_2(IEnumerable<string> list) { }
可见,参数的“泛”可以提供更大的灵活性。
接着就进入本次的主题:抗变与协变。需要说明的是,抗变与协变是在4.0开始支持的。假设有一个方法需要Derived集合作为参数,那么基于上面的原则,我们会这样设计:
static void TestIn(IEnumerable<Base> bases) { }
接着我们向下面这样调用,在3.5下就会发现编译不通过,提示无法将 List<Derived>转换为IEnumerable<Base>。
List<Derived> listIn = new List<Derived>(); TestIn(listIn);
同样的代码,我们拿到4.0下,发现编译通过了。比较 IEnumerable泛型接口,我们发现4.0下的定义为:
public interface IEnumerable<out T> : IEnumerable
发现多了 out 关键字,这就是协变。msdn对于类型参数的解释是:out T 要枚举的对象的类型。该类型参数是协变的。即可以使用指定的类型或派生程度更高的类型。
我们可以这样理解协变,参数的类型就是协变的,父类用子类代替,也就是子类当父类使用。
理解协变后,抗变就好理解了。函数的返回值就是抗变的,子类用父类代替,也就是父类当子类使用。在非泛型的情况下,我们可以这样接收方法的返回值:
object obj = Test_3(); static string Test_3() { return "hello world"; }
当然,我们觉得这样调用也应该是可以的:
IEnumerable<Base> listOut = TestOut(); static IEnumerable<Derived> TestOut() { return new List<Derived>(); }
在3.5下,这样同样会编译错误。4.0下就没有问题。
三、总结
协变与抗变的概念其实我们经常遇到(参数协变、返回值抗变),而且我们也会习惯的这样设计。但对于泛型,.net 到了4.0才提供这样的支持,这为泛型的使用提供了更大的灵活性。
ok,实际我们不怎么需要去理解概念性的东西,知道原理和理解怎么使用即可。以上是我的个人理解,如果有朋友想要更深入的理解,可以参见msdn。