让我们用心感受泛型接口的协变和抗变out和in

时间:2022-10-07 13:48:00

  关键字out和in相信大家都不陌生,系统定义的很多泛型类型大家F12都或多或少看见了。但是实际中又很少会用到,以前在红皮书里看到,两三页就介绍完了。有的概念感觉直接搬出来的,只是说这样写会怎样,并没有形象的将为什么这么设计,什么时候有用。再加上是翻译的语义很生硬,理解起来很费劲。自然又百度一通,看了一大堆大家各抒己见,这东西还是像一个低分辨率的图片一样,不够清晰。其实现在各种知识点基本都知道大概是怎么回事,怎么用,但是总感觉少点什么,不够高清。于是最近写了个控制台,把各种不够高清或者需要高清显示的知识点捋了一边。果然纸上得来终觉浅,欲知此事写代码。好多东西尝试一遍或者配合使用,的确感觉神清气爽,很多设计知道了缘由,世界都高清了。废话不多说,分享下自认为简单粗暴有效的愚见吧。

------------------------------------------------关于定义的验证-----------------------------------------------------------------------

  既然是泛型out in关键字(非泛型试过了),那就写一个吧。于是

让我们用心感受泛型接口的协变和抗变out和in

  大意就是只能用于接口和委托的泛型,于是这样写

让我们用心感受泛型接口的协变和抗变out和in

  果然OK,为什么只能接口和委托,一时半会想不通,先不管,再捋捋。红皮书里写了一堆,不理解没关系,但是说了一个关键点,in修饰的泛型只能是传入参数,out修饰的只能是返回类型。问题又来了,为什么这么设计,想不通,先不管,验证下

让我们用心感受泛型接口的协变和抗变out和in

  果然,接着捋。改正错误,现在一个泛型接口带in,out关键字的定义好了。接口能干什么,就是实现它啊。实现泛型接口需要指定泛型为具体类型,红皮书里讲了一堆,in,out反正是跟类型转换有关,那这里的in,out肯定是要作用于父类子类才有效果。于是定义一个父类和子类,再实现接口大概就是这样。

让我们用心感受泛型接口的协变和抗变out和in

  实现时in out的类型可以随意指定,只要满足in是入参,out是返回类型就行了。既然能这样写,那就这样吧。红皮书里写到用了in之后实现接口的类能怎么怎么转换,用out之后能怎么怎么转换,反正一大堆,很绕,重点就是实现后,父类子类分别作为泛型类型之间能倒腾了。管他的,我们自己来倒腾下吧。

-----------------------------------------------------------------关于转换的验证-------------------------------------------------------

  1.现在GT_ClassC 继承自 GT_InterfaceC<GT_Child, GT_Parent>,于是这样写是OK的。

让我们用心感受泛型接口的协变和抗变out和in

  既然是父类子类对泛型不同实现的转换,那我们就修改左边的父类或子类泛型,看看能不能转换。首先把左边的子类改为父类,于是是两个父类实现的泛型接口。

让我们用心感受泛型接口的协变和抗变out和in

  结果是不能自动转换,为什么左边的参数从子类变成父类,就报错了。

  来捋一捋,首先左边是in修饰,所以左边的泛型肯定只能是入参类型。我们看看GT_ClassC类Show方法的入参是什么,是子类GT_Child对吧,假设方法体里已经调用了子类的相关成员,这时候我们把入参类型改成他的父类,会有什么问题。子类的成员父类不一定有,这就是关键,方法体有异常风险。所以这里的入参类型的只能兼容GT_Child的同类或子类。

  是不是呼之欲出,in修饰的类型在泛型接口实现后相互转换时,左边的该类型只能是右边该类型的同类或子类。

  再看红皮书里对in抗变的描述,大意父类到子类的转换(这样写谁明白),其实意思应该是:in修饰的类型只能向下兼容,编译器允许转换右边的类型被转换成子类。

  2.那再来看看out吧,out是GT_InterfaceC第二个类型的修饰符,所以我们在刚才不报错的写法上修改下第二个参数,于是

让我们用心感受泛型接口的协变和抗变out和in

  果然,报错了。那就捋一捋吧。

  out关键字修饰的类型只能是返回类型,看看GT_ClassC中Show方法的返回类型是什么,是GT_Parent对吧。为什么把左边的out类型从父类改为子类就报错了呢。结合上面的理解,因为该类型肯定是返回类型,假设返回的是父类,现在要把父类转换成子类会怎样。看看实验结果

让我们用心感受泛型接口的协变和抗变out和in

  父类到子类的转换不能隐式转换,因为父类可以有多个子类,转换需要强制转换。所以Show方法的返回类型只能隐式转换成父类。

  是不是呼之欲出:out修饰的类型在泛型接口实现后相互转换时,左边的该类型只能是右边该类型的同类或父类。简单来说out修饰的类型只能向上兼容。和in真的是前呼后应啊。再回头一想,要是in和out不限制:in只能修饰入参类型,out只能修饰返回类型,是不是上面的理论都不能成立,现在知道为什么有这样的约束了吧。

------------------------------------------------------------分析-----------------------------------------------------------------------------------

  下面贴上写代码的时候的一点总结帮助理解

让我们用心感受泛型接口的协变和抗变out和in

  其实刚开始GT_ClassC实现接口的类型,in out对应的类型是和现在图中颠倒的,这样一来,应该会有人想到了,不管左边是什么类型,右边都能转换成功。所以刚开始我把左边子类父类不管怎么替换都没问题,我以为只要用了in out约束就可以任意写了,但是机智的我立马觉得此事必有蹊跷。要是没问题,还讲什么协变抗变,直接说入参in约束,返回类型out约束就行了。于是有了以上推论。

  看到这是不是觉得该完了。too young, simple is good.

  3.再回头看最开始的疑问,为什么只能是泛型接口或泛型委托可以使用in out。我们写两行代码看看

让我们用心感受泛型接口的协变和抗变out和in

  定义一个简单泛型,发现不管怎么转换都不行。

  泛型转换是基于接口,先不管具体实现类型是什么,两个同一泛型接口的成员当然可以转换。而具体的类型转换就要基于in和out的约束了(没有关键字约束不管具体实现类型之间有无继承关系都无法转换,已验证)。

  再看看委托,委托可以理解成一种类型,定义(指定参数类型和返回类型的方法)的类型。是不是和泛型很像,事件就相当于委托的实现,是不是有泛型接口的影子。

  泛型委托涉及到泛型,有入参和返回类型当然可以使用in out 约束泛型达到(泛型委托实现互相转换)的编译器检查。

  下面贴上比较完整的demo方便大家整体查看,好些报错的验证性代码删了,有兴趣大家可以自己验证一下。

让我们用心感受泛型接口的协变和抗变out和in