那些年搞不懂的术语概念:协变逆变不变体

因为菜,所以要好好学习、天天向上!——农码一生 因为菜,所以要好好学习、天天向上!——农码一生     2022-08-01     115

关键词:

简述什么是协变性、逆变性、不变性

  • 协变性,如:string->object (子类到父类的转换)
  • 逆变性,如:object->string (父类到子类的转换)
  • 不变性,基于上面两种情况,不可变。具体下面再做分析。

泛型委托的可变性

先使用框架定义的泛型委托Func和Action做例子(不了解的请戳

协变:(string->object)

Func<string> func1 = () => "农码一生";
Func<object> func2 = func1;

逆变:(object->string)

Action<object> func3 = t => { };
Action<string> func4 = func3;

上面代码没有任何问题。

接着我们自己定义委托试试:

我X,看人不来哦。为什么自定义的委托却不能协变呢。

我看看系统定义的Func到底和我们自定义的有什么不同:

public delegate TResult Func<out TResult>();

多了一个out,什么鬼:

  • out:对于泛型类型参数,out 关键字指定该类型参数是协变的。 可以在泛型接口和委托中使用 out 关键字。来源
  • in:对于泛型类型参数,in 关键字指定该类型参数是逆变的。 可以在泛型接口和委托中使用 in 关键字。来源

那么我们可以修改自定义委托:

完美!

那如果我们要实现逆变性呢:

直接逆变是不可行的,我们需要修改泛型类型参数:

我们发现整个委托参数都变了。本来的返回值,改成输入参数才行。

结论:

  • in->输入参数->可逆变(父类到子类的转变[如 object->string])
  • out->返回值->可协变(子类到父类的转变[如 string->object])

 

假设:如果泛型参数中既存在in又存在out改如何:

delegate Tout MyFunc<in Tin, out Tout>(Tin obj);
MyFunc<object, string> str1 = t => "农码一生";
MyFunc<string, string> str2 = str1;//第一个泛型的逆变(object->string)
MyFunc<object, object> str3 = str1;//第二个泛型的协变(string->object)
MyFunc<string, object> str4 = str1;//第一个泛型的逆变和第二个泛型的协变

以上都是没有问题的。 

然后我们看看编译后的C#代码:

结论:

  • 所谓的逆变其实只是编译后进行了强制类型转换而已。

以上代码也可以直接写成:

//delegate Tout MyFunc<in Tin, out Tout>(Tin obj);
MyFunc<string, string> str5 = t => "农码一生";
MyFunc<object, object> str6 = t => "农码一生";
MyFunc<string, object> str7 = t => "农码一生";

泛型接口的可变性

接着看框架默认接口:

协变:(子类->父类)

IEnumerable<string> list = new List<string>();
IEnumerable<object> list2 = list;

逆变:(父类-> 子类)

IComparable<object> list3 = null;
IComparable<string> list4 = list3;

接下来我们试试自定泛型接口:

首先定义测试类型、接口:

//
public class People
{ }
//老师(继承People[人])
public class Teacher : People
{ }
//运动
public interface IMotion<T>
{ }
//跑步
public class Run<T> : IMotion<T>
{ }

然后我们测试协变性:

同样我们需要把接口 interface IMotion<T> 定义为 interface IMotion<out T> 

//运动
public interface IMotion<out T>{}
IMotion<Teacher> x = new Run<Teacher>();
IMotion<People> y = x;

如果我们要测试逆变性,则需要把 interface IMotion<T>  定义为 interface IMotion<in T> 

//运动
public interface IMotion<in T>{}
IMotion<People> x2 = new Run<People>();
IMotion<Teacher> y2 = x2;

泛型接口的逆变,编译后同样进行了强制转换:

当然,我们也可以直接写成:

IMotion<Teacher> y3 = new Run<People>();

不变性

从上面我们知道逆变性的代码编译后都会进行强制转换。假设:那我们不用out、in直接手动强制转换是否可以?:

//
public class People { }
//老师(继承People[人])
public class Teacher : People { }
//运动
public interface IMotion<T> { }
//跑步
public class Run<T> : IMotion<T> { }
//协变
IMotion<Teacher> x = new Run<Teacher>();
IMotion<People> y = (IMotion<People>)x;

//逆变
IMotion<People> x2 = new Run<People>();
IMotion<Teacher> y2 = (IMotion<Teacher>)x2;
IMotion<Teacher> y3 = (IMotion<Teacher>)new Run<People>();

天才的我发现编译成功了,没有任何问题!且还可以同时协变、逆变??不对,真的天才了吗?我们运行试试:

看来我还是太单纯了,如果真的这么容易绕过去,Microsoft又何必去搞个out、in关键字。

对于同一个泛型参数,我们既想有协变性又想逆变性,咋办?答案是不可行。这就会出现第三种情况,既不可以协变又不可以逆变。称为不变性。

(我们在IMotion定义两个方法)

//运动
public interface IMotion<T>
{
    T Show();
    void Match(T t);
}

上面我们测试过,代码直接强制转换是不能实现协变、逆变的。那么我们只能通过out、in来实现。如果现在我们在泛型参数添加out或in属性会如何?:

我们发现out和in都不能用。在用out时,有个传入参数为泛型 void Match(T t) 的方法。使用in时,有个返回参数为泛型 T Show() 的方法。现在就出现了是矛更锋利还是盾更坚硬的问题了。

最后结果是:都不能用,既不能协变,也不能逆变。此为不变体

小知识:

C#4.0之前 IEnumerable<T> 、 IComparable<T> 、 IQueryable<T> 等接口都不支持可变性,在4.0及之后才支持。因为4.0之前定义的泛型接口没有添加out、in关键字,有兴趣可以切换版本看看。

延伸思考

为什么in[输入参数]就只能逆变?分析如下:

//
public class People { }
//老师(继承People[人])
public class Teacher : People
{
    //薪水
    public decimal Salary { get; set; }
}

//运动
public interface IMotion<in T>
{
    void Match(T t);
}
//跑步
public class Run<T> : IMotion<T>
{
    public void Match(T t)
    {
        //假设中间有很多逻辑.....       
    }
}

为什么out[返回值]只能协变?分析如下:

//
public class People { }
//老师(继承People[人])
public class Teacher : People
{
    //薪水
    public decimal Salary { get; set; }
}

//运动
public interface IMotion<out T>
{
    T Show();
    //void Match(T t);
}
//跑步
public class Run<T> : IMotion<T>
{
    public T Show()
    {
        return default(T);
    }
    //public void Match(T t)
    //{
    //    //假设中间有很多逻辑.....         
    //}
}

这里有两个关键点:

  • 传入参数(in)是把参数当成父类来用,显然可以逆变(子类当成父类来用[里氏替换原则]),但是却不可以把父类当子类来用(如:子类存在有而父类没有的方法或属性)
  • 返回值(out)返回值类型用父类来接收,显然可以协变(父类可以接收一切子类),但却不可用子类接收父类数据(如:父类代表的对象不能强制转给子类[string str = (string)objcet])

。。。是不是有点越想越头晕,想不明白就慢慢想。自己动动手。

如果实在想的头大,就把它当成是乌龟的屁股(龟腚\规定)吧,知道是C#做的一种安全限制!

总结

关于泛型接口、泛型委托的可变性:

  • 协变 -> 比较和谐正常的变化 -> 子类转父类 [如 string转object] -> 必须有out标识 [返回值]
  • 逆变 -> 逆天的变化 -> 父类转子类 [如object转string] -> 必须有in标识 [传入参数]  (父亲变儿子,越活越年轻,还不够逆天吗?)
  • 所谓的逆变,会在编译后的C#代码中进行强制类型转换。
  • 示例:
    • IEnumerable<string> list = new List<string>();  
      IEnumerable<object> list2 = list; //协变
      IEnumerable<object> list2 = new List<string>();  //(也可以直接写成这样)

    • IComparable<object> list3 = null;
      IComparable<string> list4 = list3; //逆变  编译后 [ IComparable<string> list4 = (IComparable<string>) list3;]

注意:

  • 不支持类的类型参数的可变性
  • 只有泛型接口和泛型委托可以拥有可变的类型参数(out、in)
  • 可变性只支持引用转换。(不能用于值类型)
  • 类型参数使用了 out 或者 ref 将禁止可变性

 

好了,今天就到这里。没啥高深的技术知识,主要为理解协变、逆变、不变体等术语和概念。

本文已同步至索引目录:《C#基础知识巩固

 

同类文章推荐:

http://www.cnblogs.com/haoyifei/p/5760959.html

http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

 

那些年搞不懂的"协变"和"逆变"

  博主之前也不是很清楚协变与逆变,今天在书上看到了有关于协变还是逆变的介绍感觉还是不太懂,后来看了一篇园子里面一位朋友的文章,顿时茅塞顿开。本文里面会有自己的一些见解也会引用博友的一些正文,希望通过本篇,... 查看详情

上周热点回顾(8.29-9.4)

...实战案例-------架构优化之清爽一夏(Double_K)· 那些年搞不懂的术语、概念:协变、逆变、不变体(农码一生)· 【无私分享:ASP.NETCORE项目实战(第十二章)】添加对SqlServer、MySql、Oracle的支持(果 查看详情

那些年搞不懂的多线程同步异步及阻塞和非阻塞---多线程简介

1、进程和线程的概念进程:运行中的应用程序称为进程,拥有系统资源(cpu、内存)线程:进程中的一段代码,一个进程中可以有多段代码。本身不拥有资源(共享所在进程的资源);在java中,程序入口被自动创建为主线程,... 查看详情

java进阶知识点2:看不懂的代码-协变与逆变

要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了。我们知道,在Java的世界中,存在继承机制。比如MochaCoffee类是Coffee类的派生类,那么我们可以在任何时候使用MochaCoffee类的引用去替换... 查看详情

协变逆变与不变:数组泛型与返回类型

...。2.定义这里讨论的协变、逆变与不变都是编程语言中的概念。下面介绍定义: 若类A是类B的子类,则记作A≦B。设有变换f(), 查看详情

java中的协变与逆变

...的协变与逆变首先说一下关于Java中协变,逆变与不变的概念比较官方的说法是逆变与协变描述的是类型转换后的继承关系。定义A,B两个类型,A是由B派生出来的子类(A<=B),f()表示类型转换如newList();协变:当A<=B时,f(A)<=... 查看详情

厘清泛型参数的协变与逆变

...不过,如果你想提高水平,想大概看懂.NetFramework类库中那些泛型接口与泛型类,想大概弄清楚Linq,这个概念还是需要搞清楚。话又说回来,想弄清楚,其实还是挺费劲的。如果你还糊涂着这两个概念,相信我,认真看完下面的... 查看详情

kotlin泛型③(泛型out协变|泛型in逆变|泛型invariant不变|泛型逆变协变代码示例|使用reified关键字检查泛型参数类型)(代码片段)

文章目录一、泛型out协变二、泛型in逆变三、泛型invariant不变四、泛型逆变协变代码示例五、使用reified关键字检查泛型参数类型本章总结:使用了泛型out协变和泛型in逆变极大的提高了程序的扩展性;泛型in逆变:使用in关键字,可以... 查看详情

协变和逆变之疑问

...绕道!既然是标题是协变和逆变,还是先给个公认的msdn概念吧。说完概念直接进入问题区。概念协变:是指能够使用与原始指定的派生类型相比,派生程度更大的类型。逆变:则是指能够使用派生程度更小的类型。问题请看代码650... 查看详情

详解c#的协变和逆变(代码片段)

  一、使用协变(Covariance)和逆变(Contravariance)能够实现数组之间、委托实例和方法之间、泛型委托实例之间、泛型接口的变量和泛型类型的对象之间、泛型接口的变量之间的隐式转换;使用协变将允许使用比原指定类型派... 查看详情

逆变(contravariant)与协变(covariant)

逆变(contravariant)与协变(covariant)是C#4新增的概念,许多书籍和博客都有讲解,我觉得都没有把它们讲清楚,搞明白了它们,可以更准确地去定义泛型委托和接口,这里我尝试画图详细解析逆变与协变。变的概念我们都知道.N... 查看详情

.netc#杂谈:变体

0.文章目的:介绍变体的概念,并介绍其对C#的意义1.阅读基础了解C#进阶语言功能的使用(尤其是泛型、委托、接口)2.从示例入手,理解变体变体这一概念用于描述存在继承关系的类型间的转化,这一概念并非只适用于C#,在许... 查看详情

搞不懂的github

我也是在网上看了不少的教程,但还是一脸懵逼。首先还是先写出自己的github的地址吧。我的github地址为:https://github.com/UchinoMENG.这个网址里面还是有一些东西的,但都是瞎折鼓得,自己也不太懂。可能自己下的功夫不够,所以... 查看详情

oop中的逆变和协变

...用了C#、Scala的语法作为演示样例,事实上逆变和协变的概念跟语言本身关系不大,事实也是如此。一、定义逆变的參数能够由指定的类型的子类型取代,协变的參数能够由指定类型的父类型取代。Scala中的逆变声明:Function[-A,+B]... 查看详情

typescript中的协变和逆变(代码片段)

最近用TS时碰到协变和逆变的一些概念,发现有篇外国人写的文章比较容易理解的,这里记录下。1.协变和逆变简单理解先简单说下协变和逆变的理解。首先,无论协变还是逆变,必然是存在于有继承关系的类当中... 查看详情

scala型变

参考技术A「型变(Variance)」是一个令人费解的概念,但它却是理解类型系统的重要基石。本文首先讨论型变的基本概念,深入理解型变的基本形态。然后以List,Option为例讲解型变在Scala中的应用;最后通过ScalaHamcrest的实战,加深... 查看详情

java泛型中的协变和逆变

...t;之间是不可变的。但当我们在Java泛型中引入通配符这个概念的时候,Java其实是支持协变和逆变的。看下面几行代码: //不可变List<Fruit>fruits =newArrayList<Apple 查看详情

c#中的协变与逆变(代码片段)

一:什么是协变与逆变官方解释:协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 更易理解... 查看详情