Java 中泛型的协变

在专门的学业中碰着二个题目,用代码描述如下:

用作二个从接触 Unity 3D 才起来读书 C#
的人,笔者一向只询问部分最基本、最简便的言语特征。近期看了《C# in
Depth》那本书,发现那其间东西还真不菲,即便除去和 Windows
编制程序相关的源委,只是宽容 Unity
的有的就够雅观消食一阵子的。当中,令笔者格外头大的一组概念,就是协变性和逆变性(统称可变性)。

官网:

package test;
import java.util.LinkedList;
import java.util.List;

public class ListTest {
    public void func(List<Base> list) {
    }
    public static void main(String args[]) {
        ListTest lt = new ListTest();
        List<Derived> list = new LinkedList<Derived>();
        lt.func(list); // 编译报错
    }
}

class Base {
}

class Derived extends Base {
}

一、可变性的概念

C# 一上马就帮助数组的协变,传闻是为着和 Java 竞争于是就把 Java
的这几个平凡的表征给贯彻了。看如下代码,

object[] myArray = new string[] { "abc", "def", "ghi", // ... };

new 了一个 string 类型的数组,却把它看作一个 object
类型数组的发轫化式。那足以编写翻译通过,也正是说,使用 object
数组之处,都得以传入 string
数组。推广一下,正是行使基类数组之处,都得以流传派生类数组。那样看来,协变的定义没什么深奥的,所谓协变性指的便是,在贰个采用相同项目标地方,能够流传一个卓殊类型的指标。那正是说,最中央的协变就是面向对象中的多态——使用基类型对象的地点,都足以流传派生类对象。

而是,上面那么些数组的协变性依旧很极度的。首先,在 C#
的体系系统中,派生类型的数组并不三番五次自基类型的数组(其他语言也是那般呢)。所以,它实乃一种新的协变性。其次,假若您做二个之类操作,你就能吸收接纳运营时不当,告诉您数组类型不相配:

myArray[0] = 3;

约等于说,CL途观 依旧精通 myArray
到底是何许项指标,并且不能够退换。那呈现略略别扭,可是最少自个儿经过它领会协变是什么样了。于是逆变正是扭曲的定义——在三个行使极度类型的地点,能够流传平日项指标对象。

原稿链接:

此间须求写二个函数func,能够以Base的list作为参数。原感觉传二个Derived的list也足以,因为Derived是Base的派生类,那Derived的list也相应是Base的list的派生类,结果编译器报错。

二、委托中的可变性

在 C# 1中,借使我们定义二个寄托项目,那么用于它的秘技将必需在参数表和再次来到值方面严俊相称。但是C# 2
里事情改造了,它支持对参数的逆变性和对再次来到值的协变性。假定有品种
Base 和 Derived,当中后面一个派生自前面多少个,那么上面代码是法定的。

delegate Base VariantDelegate(Derived d);

public Derived MyFunc(Base b) 
{ 
    // ...
}

VariantDelegate d = new VariantDelegate(MyFunc);

寄托 VariantDelegate 供给五个 Base 类型的再次来到值,可是大家得以给它三个Derived
类型的重临值,即那重返值是协变的。雷同的,那委托的参数是逆变的。为何那样是入情入理的吧?

虚构选取那委托之处,它最终会调用那几个委托的实例,传入参数,管理重返值。由于使用者必需给那些委托实例传叁个Derived 类型(或然它的子类型)的参数,那么,倘使那委托所调用的点子
MyFunc 本人要求三个 Base 类型的参数,不会有任何难点。因为具有的 Derived
对象足以被用作是 Base 类型的指标来接收。具体的说,假设 Base 是
object,而 Derived 是 string,那么本例中,委托的使用者会给委托一个string 类型的参数,而 MyFunc 会把这 string 充当是 object
管理,当然是优游卒岁的。另一面,对于那委托的再次来到值,使用者当它是个 Base
来拍卖,那么,MyFunc 实际重临的是 Derived
也就从未别的难题。这件事实上正是花费代码必得把传播对象充当是贰个尤为泛化(经常)的对象来拍卖。因而,假若让委托参数补助协变,再次来到值扶持逆变,那么自然会死得超丑。

 仿照效法链接:

究其原因,在互连网查了有的素材:Java的泛型并非协变的。

三、泛型中的可变性

一直到 C#
3,泛型类型、接口、委托的参数都以不可变的。基于和上述相仿的逻辑,C# 4
终于决定在泛型接口和信托中帮忙项目参数的可变性。借使您想使用可变性,必得在项目参数前用
in 或者 out
修饰符来显式钦命。和如今相似,如若三个档期的顺序参数仅看成接口方法大概委托中的(普通)参数,那么它能够被钦赐为逆变的(使用
in 来修饰);纵然它只看做再次回到值,那么它可以被钦赐为协变的(使用 out
来修饰)。最分布的例证是下面五个委托:

delegate void Action<T>(T t);
delegate TResult Func<TResult>();

在 C# 4 中的定义形成了

delegate void Action<in T>(T t);
delegate TResult Func<out TResult>();

假设驾驭了上面介绍的有关(非泛型)委托参数和再次回到值的可变性,那么对如此的泛型委托也足以很容易的明亮。然而,假如事态复杂了咋办呢?考虑上边这些情状:

delegate void Action2<T>(Action<T> action);

这几个委托要是要受可变性的雨滴,应该在 T 后边加什么修饰符呢?答案是
out,而书上的解释是歪曲的:“作为贰个便利的规则,可以以为内嵌的逆变性反转了事情发生前的可变性。”那话什么人看得懂呢?依然来分析一下好了。

本条委托 Action2 的参数是 Action<T>
类型。基于委托参数的逆变性,我们得以传递叁个比 Action<T>
更新鲜(假设不是 Action<T> 本身)的门类。那么什么样的项目是比
Action<T> 特殊呢?由于委托参数的逆变性,Action<T> 须求多少个比 T
特殊(如若不是 T 本人的话)的参数,也等于说,使用 Action<object>
的地点可以传叁个
Action<string>。所以,大家得以将其通晓为,Action<string> 是比
Action<object> 更泛化的品类。那好了,Action<object> 就是比
Action<string> 越来越窄化。那样,前边说需求 Action<T>
参数的地方,能够传二个 Action<T 的派生类>。也便是说,在 Action2<T>
中,供给类型 T 的时候能够传入 T 的派生类,所以 T 是协变的,应加
out 修饰符。从这一个例子,也可以通晓前边援用的《C# in
depth》上的表明:object 本身是比 string 更泛化的门类,但逆变性使得
Action<string> 成了比 Action<object> 更泛化的体系。

值得注意的有个别,正是 out 的意思。out
除了象征项目参数的协变性之外,还应该有二个作用,正是作为函数参数的修饰符,表示输出参数。即便泛型接口可能委托的品种参数用于出口参数,那么它本身是不可变的,也就不能用
out 来修饰了。为何吧?大家假设 CLTiggo扶植这种可变性(从语义上来看自然应该是协变性),看看会发生哪些。考虑如下委托:

delegate void WrongDelegate<out T>(out T t); // Won't really compile

它有叁个输出参数,是 T 类型的。大家标识它是协变的,那么须要
WrongDelegate<string> 之处能够传入
WrongDelegate<object>。约等于说它能够从下边那样的主意实例化:

void MyFuncWithOutParam(out object o)
{
    // Something will be assigned to o here
}

可是,使用 WrongDelegate<string> 的地点会传贰个 string
类型的变量给它作为出口参数,而 MyFuncWithOutParam 以为传进来的东西是
object,不定会赋什么样的对象给它,后果不堪虚构。至于假诺 T
是逆变的会发生什么难点,作者还不曾想清楚,很也许是因为,输出参数是无法做类型调换的(比如编写翻译器会告知你
out object 无法更动为 out string
大概反过来)。但为啥输出参数无法做类型转变呢?

好歹,输出参数和重回值照旧十分不一致等的。输出参数终归依旧参数。对 CLPAJERO来讲,输出参数是包罗非常属性的援用参数,小编理解和援用参数的界别并十分的小。而对此引用参数,大家大概更便于理解它为啥不可能是可变的。

只顾:这里说的可变性,是朝鲜语 Variance,协变和逆变分别是 Covariance 和
Contravariance,要和 mutability 区分。


旧文搬运,二〇一一-12-15 首发于搜狐。

 

泛型的协变和逆变都以术语,后边多个指能够利用比原本内定的派生类型的派生程度更加小(不太现实的卡塔尔的门类,后面一个指能够利用比原本钦点的派生类型的派生程度越来越大(更切实的)的系列。

1.3.4  泛型类型的协变(covariant)和逆变(contravariant)

例如C#中的泛型正是支撑协变的:

在.NET
4.0事情未发生前的本子中,泛型类型是不辅助协变和逆变的,不过委托项目标参数是支撑协变和逆变的。什么是协变和逆变呢?在编制程序语言中,“协变”是指能够利用与原有钦赐的派生类型比较派生程度越来越大的门类;“逆变”则是指能够利用派生程度更加小的品种。

IEnumerable<Derived> d = new List<Derived>();
IEnumerable<Base> b = d;

下边包车型地铁代码很好地示范了寄托项目标协变。假定有八个类Animals,从其派生出三个子类Dogs,那么当定义叁个寄托,该信托再次来到Animals。客户也足以将一个赶回Dogs的寄托赋值给该信托,称之为协变,见代码1.4。

唯独Java的泛型却是不帮忙协变的,相仿下边包车型地铁代码在Java中不可能透过编译。

代码1.4  委托的协变

但风趣的是,Java中的数值却是扶植协变,比如:

  1. class Program  
  2.     {  
  3.         public delegate Animals HandlerMethod();    //返回Animals的委托
     
  4.         public static Animals FirstHandler(卡塔尔(قطر‎        //重回Animals的办法完成 
  5.         {  
  6.             Console.WriteLine(“返回Animals的委托”);  
  7.             return null;  
  8.         }  
  9.         public static Dogs Secondhandler(State of Qatar          //重临Dogs的艺术完结 
  10.         {  
  11.             Console.WriteLine(“返回Dogs的委托”);  
  12.             return null;  
  13.         }  
  14.         static void Main(string[] args)  
  15.         {  
  16.             HandlerMethod handler1 = FirstHandler;  //标准委托  
  17.             HandlerMethod handler2 = Secondhandler; //委托协变  
  18.         }  
  19.     }  
  20.     // 定义四个Animals的类  
  21.     public class Animals  
  22.     {  
  23.         public string  Location { get; set; }  
  24.     }  
  25.     // 定义三个派生自Animals的Dogs类  
  26.     public class Dogs : Animals  
  27.     {  
  28.         public string Cry { get; set; }  
  29.     } 
Integer[] intArray = new Integer[10]; 
Number[] numberArray = intArray;

在上头的代码中,首先定义了Animals类和Dogs类,然后定义了贰个名称为HandlerMethod的信托,该信托重返Animals类型的
值。在Main(卡塔尔国方法中,分别赋给一个赶回Animals类型的值和一个回到Dogs类型值的方式。能够看来,由于委托的协变个性,使得本来再次回到三个Animals的信托能够接纳三个回到Dogs的寄托。

小结:Java的泛型不扶持协变,越来越多的是从类型安全的角度考虑。这种规划不是无可否认必得的,举个例子C#就不曾动用这种设计。只能说Java的设计者在易用性和类别安全之间做了接收。

.NET
4.0引入了in/out参数,使泛型类型的协变和逆变得以得以完毕。比方定义一个泛型接口只怕是泛型委托,能够行使out关键字,将泛型类型参数申明为协变。协变类型必得满足条件:类型仅作为接口方法的回到类型,不用作方法参数的花色。

谈起底回来最早的百般标题,要落实叁个那么的方法func,能够改正为:

能够行使in关键字,将泛型类型参数注明为逆变。逆变类型只好当做方法参数的品类,不可能用作接口方法的归来类型。逆变类型还可用以泛型约束。上边包车型大巴示范演示了什么使用in/out参数来设置泛型类型的协变和逆变。协变的接收见代码1.5。

public void func(List list) {
}

代码1.5  泛型的协变

抑或利用参数化类型:

  1. interface ITest<out T>                  //定义二个支撑协变的接口  
  2. {  
  3.     T X { get; }                            //属性  
  4.     T M(卡塔尔国;                                  //再次回到T类型的艺术  
  5. }  
  6. //定义二个落到实处接口的泛型类  
  7. class TestClass<T> : ITest<T> 
  8.   where T : Base, new(State of Qatar                 //约束T要派生自Base,具备构造函数
     
  9. {  
  10.     public T X { get; set; }  
  11.     //达成泛型方法  
  12.     public T M()  
  13.     {  
  14.         return new T();  
  15.     }  
  16. }  
  17. //定义五个类  
  18. class Base { }  
  19. class Derived : Base { }  
  20. class Program  
  21. {  
  22.     static void Main(string[] args)  
  23.     {  
  24.         ITest<Derived> _derived =   
  25.             new TestClass<Derived> { X = new Derived(卡塔尔 };                                       //使用对象早先化语法赋初值
     
  26.         ITest<Base> _base = _derived;   //泛型协变  
  27.         Base x = _base.X;   
  28.         Base m = _base.M();  
  29.     }  
public <T> void func(List<T> list) {
}

在上头的代码中,定义了叁个泛型接口ITest,注意使用了out参数以协助协变。然后TestClass泛型类达成了接口,並且定义了泛型约束钦定T类型必得是派生自Base类的子类。能够看出在Main主窗体中,定义了一个ITest的接口,然后选用泛型的协变性格来举行泛型类型之间的转变。

但是如此也不寻常,会搅乱了func的参数类型。越来越好的方式是不改func,在传参时就传三个Base类型的List,那就供给在将成分参预那几个List时就要转型成Base类型。

与协变相反的是,逆变是将基类转变为派生类,泛型逆变犹如下两条法则:

泛型参数受in关键字约束,只好用来属性设置或委托(方法)参数。

隐式调换指标的泛型参数类型必得是现阶段项目的“世襲类”。

譬喻,代码1.6定义了三个接口,演示了如何是允许协变,哪些是同意逆变的。

代码1.6  接口的逆变

  1. interface ITest<in T> 
  2. {  
  3.     T X  
  4.     {  
  5.         get;    //获取属性不许逆变  
  6.         set;    //设置属性允许逆变!  
  7.     }  
  8.     T M(T o卡塔尔;   //只允许方法参数,无法成效于方法重返值  

与协变相反,逆变切合多态性的原理,逆变有些令人费解,但是逆变主如若为泛型委托寻思的。逆变的使用如代码1.7所示。

代码1.7  委托的逆变

  1. class Program  
  2. {  
  3.     static void Main(string[] args)  
  4.     {  
  5.         Action<Base> _base = (o卡塔尔国 => Console.WriteLine(o卡塔尔;//定义二个Base基类  
  6.         Action<Derived> _derived = _base;       //使用协变将基类转变为派生类
     
  7.         _derived(new Derived(卡塔尔卡塔尔;                    //逆变的机能  
  8.     }  

如上代码中成立了一个寄托,是基于Base类,可是在末端的赋值语句中,将基类赋给派生类,形成了逆变。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图