首页 > 代码库 > 协变和逆变
协变和逆变
首先, 假设有一下三个类的继承关系
class Person { public string Name { get; set; } } class Student: Person { public string Class { get; set; } //班级 } class HighSchoolStudent:Student { public string DormNo { get; set; } //假设HighSchool才住宿舍 }
然后编码
static void Main(string[] args) { IEnumerable<Student> studentList = new List<Student>(); IEnumerable<Person> personList = studentList; }
我们很自然的认为, 既然studentList里真实保存的对象是Student, 那么转换为IEnumerable<Person>应该是自然而然的事情
但是在.Net Framework 4之前, 这却是不允许的, 编译时会提示
无法将类型“System.Collections.Generic.IEnumerable<Test.Student>”隐式转换为“System.Collections.Generic.IEnumerable<Test.Person>”。存在一个显式转换(是否缺少强制转换?)
即使你变成这样也不可以
IEnumerable<Person> personList = (IEnumerable<Person>)studentList;
好在.Net Framework 4有了泛型协变和逆变后, 开始支持这种转换了, 下面我们开始学习一下协变和逆变
首先要了解 协变 和 逆变的定义: 对于泛型方法、泛型类等
如果某个返回的类型可以由其基类替换,那么这个类型就是支持协变的
IEnumerable<Student> d = new List<Student>();IEnumerable<Person> b = d;
如果某个输入参数类型可以由其派生类型替换,那么这个类型就是支持逆变的
Action<Person> b = (target) => { Console.WriteLine(target.Name); };Action<Student> d = b;
可以看出, 不管是协变还是逆变, 都遵从规则: 子类可以向父类隐式转换, 但是父类不能向子类隐式转换
需要注意的是, 协变、逆变都是针对泛型变量(这里的b和d), 而不是后面List<Student> 可不可以Add(new Person()) 或Add(new HighSchoolStudent())
下面这个泛型方法func1: 输入一个Person, 处理后, 输出一个新Student
func2可以安全的由func1转换过来: func1需要输入输入一个 Person的时候, func2给的是Student, 可以安全的隐式转换; func1输出Student的时候, func2只要Person就可以了, 可以安全的隐式转换
func1=Func<in Person, out Student>()func2=Func<Student, Person >()func1=func2;
in : Student作为输入参数, 转换(逆变)成 in Person
out: out Student转换(协变)成Person 作为输出结果
我们平常用到的IEnumerable<out T>就是带 out 的协变(但List却不是)
不对啊, 对于IEnumerable<Student>, 通常理解是, 要创建一个Student的列表, 赋值给它的肯定是一个Student列表, Student就是它的输入类型, 按理说 T 前面要加in 才对啊!?
其实呢, 是我们对IEnumerable<T>理解不够, IEnumerable<Student> list=new List<Student>(), 其实表达的意思是, 你从IEnumerable<Student> list取出(输出)的对象默认就是Student, 后面的List可以是new List<HighSchoolStudent>(), 但你从IEnumerable<Student> list 取得的对象仍然是Student; 如果你用IEnumerable<Person> list指向这个列表, 那么取出来的对象就是Person
同时编译器为确保类型安全, 总是强制要求赋值给list的必须是Student对象, 久而久之, 让人错觉赋值( 输入)给list的必须Student, 然后你搞混了, 这里的隐式转换其实是列表的里的对象, 但我们现在讨论的是泛型变量
我们平时用到的Action<in T>, 就是带in 的逆变
输入T参数, 最终输出void 的方法, 输入参数可以用派生类替代, 还是很好理解的
这时候你可以理解为啥List<T> 不带out了, 因为List<T> 同时具有读取和写入功能;
如果只是读取功能, List<Student> 转换成 List<Person> 当然没问题
但是写入的时候, List<Student>即使能转换为List<Person>, 却不可能Add(new Person())
IEnumerable<Student> 只有读功能,将里面的对象转换为IEnumerable<Person> 没有任何问题
总结: 对于只需要输入或者只需要输出的泛型接口来说, 尽量加上 in 或者 out, 以支持泛型转换
public delegate TResult Func<in T1, in T2, in T3, out TResult>( (T1 arg1, T2 arg2,…) 就是很好的例子
平时我们撸码的时候, 一般都用不到这些协变、逆变, 但是编写高度抽象的泛型方法时候, 却要知道这些知识
end
协变和逆变