ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

泛型的协变与抗变(反变)(转载)

2021-11-16 13:35:23  阅读:181  来源: 互联网

标签:IFoo 类型 参数 协变 泛型 反变 抗变


原文地址:https://www.cnblogs.com/linybo/p/13340343.html

随Visual Studio 2010 CTP亮相的 C#4 和 VB10,虽然在支持语言新特性方面走了相当不一样的两条路:C#着重增加后期绑定和与动态语言相容的若干特性,VB10着重简化语言和提高抽象能力;但是两者都增加了一项功能:泛型类型的协变(covariant)和抗变(contravariant)。许多人对其了解可能仅限于增加的in/out关键字,而对其诸多特性有所不知。下面我们就对此进行一些详细的解释,帮助大家正确使用该特性。

背景知识:协变和抗变

很多人可能不不能很好地理解这些来自于物理和数学的名词。我们无需去了解他们的数学定义,但是至少应该能分清协变和反变。实际上这个词来源于类型和类型之间的绑定。

我们从数组开始理解。数组其实就是一种和具体类型之间发生绑定的类型。数组类型Int32[]就对应于Int32这个原本的类型。任何类型T都有其对应的数组类型T[]。

那么我们的问题就来了,如果两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?

这就牵扯到了将原本类型上存在的类型转换映射到他们的数组类型上的能力,这种能力就称为“可变性(Variance)”。

在.NET世界中,原始类型唯一允许可变性的类型转换就是由继承关系带来的“子类引用->父类引用”转换。

举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,数组这种与原始类型转换方向相同的可变性就称作协变(covariant)。

由于数组不支持反变性,我们无法用数组的例子来解释反变性,所以我们现在就来看看泛型接口和泛型委托的可变性。

假设有这样两个类型:TSub是TParent的子类,显然TSub型引用是可以安全转换为TParent型引用的。
如果一个泛型接口IFoo,IFoo可以转换为IFoo的话,我们称这个过程为协变,而且说这个泛型接口支持对T的协变。
而如果一个泛型接口IBar,IBar可以转换为T的话,我们称这个过程为抗变(contravariant),而且说这个接口支持对T的抗变。

因此很好理解,如果一个可变性和子类到父类转换的方向一样,就称作协变;而如果和子类到父类的转换方向相反,就叫抗变。

简而言之:协变和抗变是专门针对泛型接口和泛型委托可变性的扩展,协变是指接口的泛型子类向泛型父类方向的类型转换,抗变是指接口的泛型父类向泛型子类方向的类型转换,如果不使用out和in标注协变和抗变,那么这个泛型类型就是不变的。

.NET 4.0引入的泛型协变、反变性

刚才我们讲解概念的时候已经用了泛型接口的协变和反变,但在.NET 4.0之前,无论C#还是VB里都不支持泛型的这种可变性。不过它们都支持委托参数类型的协变和反变。由于委托参数类型的可变性理解起来抽象度较高,所以我们这里不准备讨论。已经完全能够理解这些概念的读者自己想必能够自己去理解委托参数类型的可变性。

在.NET 4.0之前为什么不允许IFoo进行协变或反变呢?因为对接口来讲,T 这个类型参数既可以用于方法参数,也可以用于方法返回值。设想这样的接口

interface IFoo<T>
{
    void Method1(T param);

    T Method2();
}

如果我们允许协变,从IFoo到IFoo转换,那么IFoo.Method1(TSub)就会变成IFoo.Method1(TParent)。

我们都知道TParent是不能安全转换成TSub的,所以Method1这个方法就会变得不安全。
同样,如果我们允许反变IFoo到IFoo,则TParent IFoo.Method2()方法就会变成TSub IFoo.Method2(),原本返回的TParent引用未必能够转换成TSub的引用,Method2的调用将是不安全的。

有此可见,在没有额外机制的限制下,泛型接口进行协变或反变都是类型不安全的。

.NET 4.0改进了什么呢?

它允许在类型参数的声明时增加一个额外的描述,以确定这个类型参数的使用范围。

我们看到,如果一个类型参数仅仅能用于函数的返回值,那么这个类型参数就对协变相容。而相反,一个类型参数如果仅能用于方法参数,那么这个类型参数就对反变相容。

如下所示:

interface ICo<out T>
{
    T Method();
}

interface IContra<in T>
{
    void Method(T param);
}

可以看到C#4和VB10都提供了大同小异的语法,用Out来描述仅能作为返回值的类型参数,用In来描述仅能作为方法参数的类型参数。

一个接口可以带多个类型参数,这些参数可以既有In也有Out,因此我们不能简单地说一个接口支持协变还是反变,只能说一个接口对某个具体的类型参数支持协变或反变。
比如若有IBar<in T1, out T2>这样的接口,则它对T1支持反变而对T2支持协变。
举个例子来说,IBar<object, string>能够转换成IBar<string, object>,这里既有协变又有反变。

在.NET Framework中,许多接口都仅仅将类型参数用于参数或返回值。为了使用方便,在.NET Framework 4.0里这些接口将重新声明为允许协变或反变的版本。
例如IComparable就可以重新声明成IComparable,而IEnumerable则可以重新声明为IEnumerable。不过某些接口IList是不能声明为in或out的,因此也就无法支持协变或反变。

下面提起几个泛型协变和反变容易忽略的注意事项:

  1. 仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。

  2. 值类型不参与协变或反变,IFoo永远无法变成IFoo

  3. 声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。

协变和反变的相互作用

这是一个相当有趣的话题,我们先来看一个例子:

interface IFoo<in T>
{
}

interface IBar<in T>
{
    void Test(IFoo<T> foo); //对吗?
}

你能看出上述代码有什么问题吗?

我声明了in T,然后将他用于方法的参数了,一切正常。但出乎你意料的是,这段代码是无法编译通过的!反而是这样的代码通过了编译:

interface IFoo<in T>
{
}

interface IBar<out T>
{
    void Test(IFoo<T> foo);
}

什么?明明是out参数,我们却要将其用于方法的参数才合法?
初看起来的确会有一些惊奇。我们需要费一些周折来理解这个问题。
现在我们考虑IBar,它应该能够协变成IBar

标签:IFoo,类型,参数,协变,泛型,反变,抗变
来源: https://www.cnblogs.com/bleds/p/15560749.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有