10分钟浅谈泛型协变与逆变
首先声明,本文写的有点粗糙,只让你了解什么是协变和逆变,没有深入研究,根据这些年的工作经验,发现我们在开发过程中,很少会自己去写逆变和协变,因为自从net 4.0 (Framework 3.0) 以后,.net 就为我们提供了 定义好的逆变与协变。我们只要会使用就可以。协变和逆变都是在泛型中使用的。
-
什么是逆变与协变呢
可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量。协变和逆变是两个相互对立的概念:
- 如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的
- 如果某个参数类型可以由其基类替换,那么这个类型就是支持逆变的。
看起来你有点绕,我们先准备个“”鸟”类,在准备一个“麻雀”类,让麻雀继承鸟类,一起看代码研究
/// <summary> /// 鸟 /// </summary> public class Bird { public int Id { get; set; } } /// <summary> /// 麻雀 /// </summary> public class Sparrow : Bird { public string Name { get; set; } }
我们分别取实例化这个类,发现程序是能编译通过的。
Bird bird1 = new Bird();
Bird bird2 = new Sparrow();
Sparrow sparrow1 = new Sparrow();
//Sparrow sparrow2 = new Bird();//这个是编译不通过的,违反了继承性。
但是我们放在集合中,去实例化,是无法通过的
List<Bird> birdList1 = new List<Bird>();
//List<Bird> birdList2 = new List<Sparrow>();//不是父子关系,没有继承关系
//一群麻雀一定是一群鸟
那么我们如何去实现在泛型中的继承性呢??这就引入了协变和逆变得概念,为了保证类型的安全,C#编译器对使用了 out
和 in
关键字的泛型参数添加了一些限制:
- 支持协变(
out
)的类型参数只能用在输出位置:函数返回值、属性的get访问器以及委托参数的某些位置 - 支持逆变(
in
)的类型参数只能用在输入位置:方法参数或委托参数的某些位置中出现。
-
协变
我们来看下Net “System.Collections.Generic”命名空间下的IEnumerable泛型 接口,会发现他的泛型参数使用了out
现在我们使用下 IEnumerable 接口来进行一下上述实力,会发现,我们的泛型有了继承关系。
IEnumerable<Bird> birdList1 = new List<Bird>();
IEnumerable<Bird> birdList2 = new List<Sparrow>();//协变
//一群麻雀一定是一群鸟
下面我们来自己定义一个协变泛型接口ICustomerListOut<Out T>,让 CustomerListOut 泛型类继承CustomerListOut<Out T> 泛型接口。
代码如下
/// <summary> /// out 协变 只能是返回结果 /// </summary> /// <typeparam name="T"></typeparam> public interface ICustomerListOut<out T> { T Get(); // void Show(T t);//T不能作为传入参数 } /// <summary> /// 类没有协变逆变 /// </summary> /// <typeparam name="T"></typeparam> public class CustomerListOut<T> : ICustomerListOut<T> { public T Get() { return default(T); } public void Show(T t) { } }
我们会发现,在泛型斜变的时候,泛型不能作为方法的参数。我们用自己定义的泛型接口和泛型类进行实例化试试,我们会发现编译通过
ICustomerListOut<Bird> customerList1 = new CustomerListOut<Bird>();//这是能编译的
ICustomerListOut<Bird> customerList2 = new CustomerListOut<Sparrow>();//这也是能编译的,在泛型中,子类指向父类,我们称为协变
到这里协变我们就学完了,协变就是让我们的泛型有了子父级的关系。本文开始的时候,协变和逆变,是在C# 4.0 以后才有的,那C# 4.0以前我们是怎么写的呢,那个时候没有协变?
老版本的写法
List<Bird> birdList3 = new List<Sparrow>().Select(c => (Bird)c).ToList();//4.0以前的写法
等学完逆变,本文列出C# 4.0 以后的版本 中framework 已经定义好的协变、逆变 泛型接口,泛型类,泛型委托。
-
逆变
刚才我们学习了泛型参数用out 去修饰,饺子协变,现在来学习下逆变,逆变是使用in来修饰的
这里就是Net 4.0 给我们提供的逆变写法
我们自己写一个逆变的接口 ICustomerListIn<in T> ,在写一个逆变的 泛型类 CustomerListIn<T>:ICustomerListIn<T> ,代码如下
/// <summary> /// 逆变 /// </summary> /// <typeparam name="T"></typeparam> public interface ICustomerListIn<in T> { //T Get();//不能作为返回值 void Show(T t); } public class CustomerListIn<T> : ICustomerListIn<T> { public T Get() { return default(T); } public void Show(T t) { } }
逆变的泛型参数是不能作为泛型方法的返回值的,我们来看下实例化鸟类,和麻雀类,看好使不好使。
ICustomerListIn<Sparrow> customerList2 = new CustomerListIn<Sparrow>();
ICustomerListIn<Sparrow> customerList1 = new CustomerListIn<Bird>();//父类指向子类,我们称为逆变
ICustomerListIn<Bird> birdList1 = new CustomerListIn<Bird>();
birdList1.Show(new Sparrow());
birdList1.Show(new Bird());
Action<Sparrow> act = new Action<Bird>((Bird i) => { });
到此我们就完全学完了逆变与协变
-
总结
逆变与协变只能放在泛型接口和泛型委托的泛型参数里面,
在泛型中out修饰泛型称为协变,协变(covariant) 修饰返回值 ,协变的原理是把子类指向父类的关系,拿到泛型中。
在泛型中in 修饰泛型称为逆变, 逆变(contravariant )修饰传入参数,逆变的原理是把父类指向子类的关系,拿到泛型中。
-
NET 中自带的斜变逆变泛型
序号 | 类别 | 名称 |
1 | 接口 | IEnumerable<out T> |
2 | 委托 | Action<in T> |
3 | 委托 | Func<out TResult> |
4 | 接口 | IReadOnlyList<out T> |
5 | 接口 | IReadOnlyCollection<out T> |
各位朋友,如果谁还知道,请留言告知