逆变与协变

一、遇到问题

  这是基于.net3.5开发,实际工作中遇到一个问题。假设我们有一个 Base 类,一个 Derived 类,Derived 继承了 Base。如下:

1

2

3

4

5

6

7

8

class Base

{

}

class Derived : Base

{

}  

  当我用IEnumerable<Base> 作为形参,List<Derived> 作为实参时,发现编译出错了!原本父类作为形参,传递子类是再正常不过的,但在泛型中确编译不通过。

二、探究问题

  通常我们在设计参数和返回值都有一个原则,参数要尽可能“泛”(父:父类),返回值要尽可能的“细”(子:子类)。泛,指得是用接口或者父类作为参数,这样可以接收更多的参数类型;细,指的是返回具体类型,这样可以更好说明方法的作用。

  举个例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

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集合作为参数,那么基于上面的原则,我们会这样设计:

1

2

3

4

static void TestIn(IEnumerable<Base> bases)

{

}

  接着我们向下面这样调用,在3.5下就会发现编译不通过,提示无法将 List<Derived>转换为IEnumerable<Base>。

1

2

List<Derived> listIn = new List<Derived>();

TestIn(listIn);

  同样的代码,我们拿到4.0下,发现编译通过了。比较 IEnumerable泛型接口,我们发现4.0下的定义为:  

1

public interface IEnumerable<out T> : IEnumerable

  发现多了 out 关键字,这就是协变。msdn对于类型参数的解释是:out T 要枚举的对象的类型。该类型参数是协变的。即可以使用指定的类型或派生程度更高的类型。

  我们可以这样理解协变,参数的类型就是协变的,父类用子类代替,也就是子类当父类使用。

  理解协变后,抗变就好理解了。函数的返回值就是抗变的,子类用父类代替,也就是父类当子类使用。在非泛型的情况下,我们可以这样接收方法的返回值:  

1

2

3

4

5

6

object obj = Test_3();

static string Test_3()

{

  return "hello world";

}  

  当然,我们觉得这样调用也应该是可以的:  

1

2

3

4

5

IEnumerable<Base> listOut = TestOut();

static IEnumerable<Derived> TestOut()

{

  return new List<Derived>();

}

  在3.5下,这样同样会编译错误。4.0下就没有问题。

三、总结

   协变与抗变的概念其实我们经常遇到(参数协变、返回值抗变),而且我们也会习惯的这样设计。但对于泛型,.net 到了4.0才提供这样的支持,这为泛型的使用提供了更大的灵活性。

  ok,实际我们不怎么需要去理解概念性的东西,知道原理和理解怎么使用即可。以上是我的个人理解,如果有朋友想要更深入的理解,可以参见msdn。

发布了548 篇原创文章 · 获赞 52 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/sinolover/article/details/104254618