脑图概览

泛型声明和使用

协变和逆变

《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关键字,有兴趣可以切换版本看看。

参考

C#-弄懂泛型和协变、逆变的更多相关文章

  1. 解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4

    前言 本篇继续讲解泛型.上一篇讲解了泛型类的定义细节.本篇继续讲解泛型接口. 泛型接口 使用泛型可定义接口,即在接口中定义的方法可以带泛型参数.然后由继承接口的类实现泛型方法.用法和继承泛型类基本没有 ...

  2. C#中泛型方法与泛型接口 C#泛型接口 List<IAll> arssr = new List<IAll>(); interface IPerson<T> c# List<接口>小技巧 泛型接口协变逆变的几个问题

    http://blog.csdn.net/aladdinty/article/details/3486532 using System; using System.Collections.Generi ...

  3. C#的in/out关键字与协变逆变

    C#提供了一组关键字in&out,在泛型接口和泛型委托中,若不使用关键字修饰类型参数T,则该类型参数是不可变的(即不允许协变/逆变转换),若使用in修饰类型参数T,保证"只将T用于输 ...

  4. 编写高质量代码改善C#程序的157个建议——建议45:为泛型类型参数指定逆变

    建议45:为泛型类型参数指定逆变 逆变是指方法的参数可以是委托或者泛型接口的参数类型的基类.FCL4.0中支持逆变的常用委托有: Func<int T,out TResult> Predi ...

  5. Programming In Scala笔记-第十九章、类型参数,协变逆变,上界下界

    本章主要讲Scala中的类型参数化.本章主要分成三个部分,第一部分实现一个函数式队列的数据结构,第二部分实现该结构的内部细节,最后一个部分解释其中的关键知识点.接下来的实例中将该函数式队列命名为Que ...

  6. java协变逆变,PECS

    public static void main(String[] args) { // Object <- Fruit <- Apple <- RedApple System.out ...

  7. 协变 & 逆变

    都跟里氏替换原则有关. 协变:你可以用一个子类对象去替换相应的一个父类对象,这是完全符合里氏替换原则的,和协(谐)的变.如:用Swan替换Bird. 逆变:你可以用一个父类对象去替换相应的一个子类对象 ...

  8. C#核心语法讲解-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)

    泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...

  9. C#核心语法-泛型(详细讲解泛型方法、泛型类、泛型接口、泛型约束,了解协变逆变)

    泛型(generic)是C#语言2.0和通用语言运行时(CLR)的一个新特性.泛型为.NET框架引入了类型参数(type parameters)的概念.类型参数使得设计类和方法时,不必确定一个或多个具 ...

随机推荐

  1. [CSP-S模拟测试]:轰炸行动(bomb)(塔尖+拓扑排序+语文)

    题目描述 战狂也在玩<魔方王国>.他只会征兵而不会建城市,因此他决定对小奇的城市进行轰炸.小奇有n座城市,城市之间建立了$m$条有向的地下通道.战狂会发起若干轮轰炸,每轮可以轰炸任意多个城 ...

  2. mac的jvm调优工具

    安装好JDK之后调优工具所在位置为: /System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/jvisualvm j ...

  3. git 时 出现 Permission denied (publickey).

    https://blog.csdn.net/awp0011/article/details/73368481 第一次使用github.com在本地 执行 git clone git@github.co ...

  4. CentOS 7命令行安装GNOME、KDE图形界面(成功安装验证)

    来源:cnblogs.com/Amedeo  作者:Amedeo 正文 CentOS 7 默认是没有图形化界面的,但我们很多人在习惯了 Windows 的图形化界面之后,总是希望有一个图形化界面从而方 ...

  5. 【ABAP系列】SAP 的逻辑数据库解析

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[ABAP系列]SAP 的逻辑数据库解析   前 ...

  6. 队列问题非STL解决方案

    队列问题非STL解决方案 常年使用STL解决队列问题,以至于严重生疏队列的根本原理... 直到今日 被老师被迫 使用算法原理解决问题,方才意识到我对队列一窍不通... ...直到 经过一系列的坑蒙拐骗 ...

  7. 2019/11/02 TZOJ

    1001 ShaoLin http://www.tzcoder.cn/acmhome/problemdetail.do?&method=showdetail&id=6003 标记一下i ...

  8. iintellij IDEA运行环境使用教程

    1.官网:https://www.jetbrains.com 链接: https://pan.baidu.com/s/10QKLn1bGEW9W0pXEp6WR1A 提取码: vt2b 看官觉得有用留 ...

  9. .net 学习官网

    https://docs.microsoft.com

  10. [Bzoj1014][JSOI2008]火星人prefix(无旋Treap&hash)

    题目链接:https://www.lydsy.com/JudgeOnline/problem.php?id=1014 因为涉及到增加和修改,所以后缀数组就被pass掉了,想到的就是平衡树维护hash值 ...