脑图概览
泛型声明和使用
协变和逆变
《C#权威指南》上在委托篇中这样定义:
-
协变
:委托方法的返回值类型直接或者间接地继承自委托前面的返回值类型; -
逆变
:委托签名中的参数类型继承自委托方法的参数类型;
在泛型篇中这样定义:
-
协变
:泛型参数定义的类型只能作为方法的返回类型,不能作为方法的参数类型,且该类型直接或者间接地继承自接口方法的返回值类型;可以使用out关键字声明协变参数。 -
逆变
:泛型参数定义的类型只能作为方法参数的类型,不能作为返回值类型,且该类型是接口方法的参数类型的基类型;可以使用in关键字声明逆变参数
一直没弄懂,或者一时弄懂了也没记住,一定不怪我!看到一个博主这样解释,秒懂:
-
协变性
:如:string->object (子类到父类的转换) -
逆变性
:如:object->string (父类到子类的转换) -
不变性
:
《CLR via C#》 第三版这样定义:
-
协变量
:意味着泛型类型参数可以从一个派生类更改为它的基类(子类可以转为父类); -
逆变量
:意味着泛型类型参数可以从一个基类改为该类的派生类(父类可以转为子类); -
不变量
:意味着泛型参数不能更改;
总结:协变逆变中的协逆是相对于继承关系的继承链方向而言的
《CLR via C#》 底部有注解说:
协变性指定返回类型的兼容性,而逆变性指定参数的兼容性。
基类和子类的转换
面向对象中有一个规则是:子类向上可以转为基类,但是基类不能向下转为子类。
比如子类Student可以通过以下方式转为基类Person:
Person p=new Student();
或者
Student s=new Student();
Person p=s;
但是基类转为子类这样转编译器就会出错:
Person p=new Person();
Student s=p;
或者
Object obj=new Object();
string str=obj;
Func泛型委托的出参只有协变
既然子类可以转为父类,父类不能转为子类,那我这样做行不行?新定义了一个委托,委托返回一个类型
delegate T MyFunc<T>();
void Main()
{
Student s = new Student() { Name = "张三" };
Person p = s;
MyFunc<Student> func1 = () => new Student() { Name = "李四" };
MyFunc<Person> func2 = func1;
Func<Student> func3 = () => new Student() { Name = "王五" };
Func<Person> func4 = func3;
}
public class Person
{
public string Name { get; set; }
}
public class Student : Person
{
public string Number { get; set; }
}
public class Teacher
{
public string TeacherBook { get; set; }
}
这样不行,MyFunc<Person> func2 = func1;
编译器不认编译失败,这句代码却Func<Person> func4 = func3;
正常。
我们知道Student转Person是合法的转换,但是编译器不知道,需要告诉编译器这段转换时合法的,没有必要做强制的类型安全转换。
怎么做?声明类型时指定out关键字,如下:
delegate T MyFunc<out T>();
可以看到编译器通过。out关键字会告诉编译器Student到Person是有效转换。
其实Func委托的定义也是如此:
public delegate TResult Func<out TResult>();//无输入参数,有返回值。
但要是加如下代码呢?
MyFunc<Teacher> func5 = func1;
编译还是会不给过的!因为转换不合法。
那要是 out关键字加在委托的入参类型呢
delegate void MyFunc<out T>(T t);
会告诉你无效
那出参可以协变,入参是否可以协变呢?
完全不可以,就像父类转为子类一样是不合法的
泛型委托Action的入参只有逆变
以下代码可以正常编译为什呢?明明object类型不可以转为string类型
Action<object> action1 = t => { Console.WriteLine(t.GetType()); };
Action<string> action2 = action1;
而反过来却是错的
Action<string> action3 = t => { Console.WriteLine(t.GetType()); };
Action<object> action4 = action3;
看看Action是如何定义的?
public delegate void Action<in T>(T obj);//泛型委托,无返回值
in关键会告诉编译器,要么传递T作为委托的参数类型,要么传递T的派生类型。string是oject的派生类,所有是正常的.
所以这里不能单纯理解为object转为string类型了,而要理解为string可以安全的替换掉object,因为string是object的子类呀,
object有的,string都有,这个转换肯定是安全的。
泛型中的协变和逆变
泛型中的协变和逆变原理与泛型委托一样
out和in总结
out: 输出(作为结果),in:输入(作为参数)
所以如果有一个泛型参数标记为out,则代表它是用来输出的,只能作为结果返回,而如果有一个泛型参数标记为in,则代表它是用来输入的,也就是它只能作为参数。
为什么in只能作为输入参数的逆变?
void Main()
{
var p = new Person();
var s = new Student();
var gs = new GoodStudent();
this.Method(p);//编译出错
this.Method(s);
this.Method(gs);
}
public void Method(Student stu)
{
}
public class Person
{
public string Name { get; set; }
}
public class Student : Person
{
public string Number { get; set; }
}
public class GoodStudent : Student
{
}
方法体形参可以把子类当成父类来用(传递Student还是GoodStudent对象都无所谓),但是不能把父类当成子类来用(传递Person对象就出错了)。【里氏替换原则】
为什么out只能作为返回值的协变?
通常定义一个变量来接受返回值,父类可以接收子类的数据(这里可以理解为object obj=str),子类能接收父类的数据吗(这里理解为string str = (string)objcet)?肯定不是不能。所以只能是协变
相关单词:
- Covariant:协变量
- Contravariant:逆变量
- Covariance:协变性
- Contravariance:逆变性
使用注意
C#4.0之前 IEnumerable<T> 、 IComparable<T> 、 IQueryable<T>
等接口都不支持可变性,在4.0及之后才支持。因为4.0之前定义的泛型接口没有添加out、in关键字,有兴趣可以切换版本看看。
参考
- 景春雷,协变(Covariance)和逆变(Contravariance)的十万个为什么
- 那些年搞不懂的术语、概念:协变、逆变、不变体
- 深入理解 C# 协变和逆变
- Func和Action学习
- 《Visual C# 从入门到精通》 第八版
- 《CLR via C#》 第三版
- 《C# 本质论》 第四版