一、遇到问题
这是基于.net3.5开发,实际工作中遇到一个问题。假设我们有一个 Base 类,一个 Derived 类,Derived 继承了 Base。如下:
1 2 3 4 5 6 7 8 |
|
当我用IEnumerable<Base> 作为形参,List<Derived> 作为实参时,发现编译出错了!原本父类作为形参,传递子类是再正常不过的,但在泛型中确编译不通过。
二、探究问题
通常我们在设计参数和返回值都有一个原则,参数要尽可能“泛”(父:父类),返回值要尽可能的“细”(子:子类)。泛,指得是用接口或者父类作为参数,这样可以接收更多的参数类型;细,指的是返回具体类型,这样可以更好说明方法的作用。
举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
可见,参数的“泛”可以提供更大的灵活性。
接着就进入本次的主题:抗变与协变。需要说明的是,抗变与协变是在4.0开始支持的。假设有一个方法需要Derived集合作为参数,那么基于上面的原则,我们会这样设计:
1 2 3 4 |
|
接着我们向下面这样调用,在3.5下就会发现编译不通过,提示无法将 List<Derived>转换为IEnumerable<Base>。
1 2 |
|
同样的代码,我们拿到4.0下,发现编译通过了。比较 IEnumerable泛型接口,我们发现4.0下的定义为:
1 |
|
发现多了 out 关键字,这就是协变。msdn对于类型参数的解释是:out T 要枚举的对象的类型。该类型参数是协变的。即可以使用指定的类型或派生程度更高的类型。
我们可以这样理解协变,参数的类型就是协变的,父类用子类代替,也就是子类当父类使用。
理解协变后,抗变就好理解了。函数的返回值就是抗变的,子类用父类代替,也就是父类当子类使用。在非泛型的情况下,我们可以这样接收方法的返回值:
1 2 3 4 5 6 |
|
当然,我们觉得这样调用也应该是可以的:
1 2 3 4 5 |
|
在3.5下,这样同样会编译错误。4.0下就没有问题。
三、总结
协变与抗变的概念其实我们经常遇到(参数协变、返回值抗变),而且我们也会习惯的这样设计。但对于泛型,.net 到了4.0才提供这样的支持,这为泛型的使用提供了更大的灵活性。
ok,实际我们不怎么需要去理解概念性的东西,知道原理和理解怎么使用即可。以上是我的个人理解,如果有朋友想要更深入的理解,可以参见msdn。