协变逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。 在引用类型系统时,协变、逆变和不变性具有如下定义。 这些示例假定一个名为 Base 的基类和一个名为 Derived的派生类。

  • Covariance

    使你能够使用比原始指定的类型派生程度更大的类型。

    你可以向 IEnumerable<Derived> 类型的变量分配IEnumerable(Of Derived) (在 Visual Basic 中为 IEnumerable<Base>)的实例。

  • Contravariance

    使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。

    你可以向 Action<Base> 类型的变量分配Action(Of Base) (在 Visual Basic 中为 Action<Derived>)的实例。

  • Invariance

    这意味着,你只能使用原始指定的类型;固定泛型类型参数既不是协变类型,也不是逆变类型。

    你无法向 List<Base> 类型的变量分配 List(Of Base)(在 Visual Basic 中为 List<Derived>)的实例,反之亦然。

利用协变类型参数,你可以执行非常类似于普通的多态性的分配,如以下代码中所示。

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

List<T> 类实现 IEnumerable<T> 接口,因此 List<Derived> (在 Visual Basic 中为List(Of Derived) )实现 IEnumerable<Derived>。 协变类型参数将执行其余的工作。

相反,逆变看起来却不够直观。 下面的示例创建类型 Action<Base> (在 Visual Basic 中为Action(Of Base) )的委托,然后将此委托分配给类型 Action<Derived>的变量。

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b; // 逆变
d(new Derived());

此示例看起来是倒退了,但它是可编译和运行的类型安全代码。 由于 lambda 表达式与其自身所分配到的委托相匹配,因此它会定义一个方法,此方法采用一个类型 Base 的参数且没有返回值。 可以将结果委托分配给类型类型 Action<Derived> 的变量,因为 T 委托的类型参数 Action<T> 是逆变类型参数。 由于 T 指定了一个参数类型,因此该代码是类型安全代码。 在调用类型 Action<Base> 的委托(就像它是类型 Action<Derived>的委托一样)时,其参数必须属于类型 Derived。 始终可以将此实参安全地传递给基础方法,因为该方法的形参属于类型 Base

通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。 对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。

协变和逆变统称为“变体” 。 未标记为协变或逆变的泛型类型参数称为“固定参数” 。 有关公共语言运行时中变体的事项的简短摘要:

  • 在 .NET Framework 4 中,Variant 类型参数仅限于泛型接口和泛型委托类型。

  • 泛型接口或泛型委托类型可以同时具有协变和逆变类型参数。

  • 变体仅适用于引用类型;如果为 Variant 类型参数指定值类型,则该类型参数对于生成的构造类型是不变的。

  • 变体不适用于委托组合。 也就是说,在给定类型 Action<Derived> 和 Action<Base>(在 Visual Basic 中为Action(Of Derived) 和 Action(Of Base) )的两个委托的情况下,无法将第二个委托与第一个委托结合起来,尽管结果将是类型安全的。 变体允许将第二个委托分配给类型 Action<Derived>的变量,但只能在这两个委托的类型完全匹配的情况下对它们进行组合。

具有协变类型参数的泛型接口

从 .NET Framework 4 开始,某些泛型接口具有协变类型参数;例如:IEnumerable<T>IEnumerator<T>IQueryable<T> 和 IGrouping<TKey,TElement>。 由于这些接口的所有类型参数都是协变类型参数,因此这些类型参数只用于成员的返回类型。

下面的示例阐释了协变类型参数。 此示例定义了两个类型: Base 具有一个名为 PrintBases 的静态方法,该方法采用 IEnumerable<Base> (在 Visual Basic 中为IEnumerable(Of Base) )并输出元素。 Derived 继承自 Base。 此示例创建一个空 List<Derived> (在 Visual Basic 中为List(Of Derived) ),并且说明可以将该类型传递给 PrintBases 且在不进行强制转换的情况下将该类型分配给类型 IEnumerable<Base> 的变量。 List<T> 实现 IEnumerable<T>,它具有一个协变类型参数。 协变类型参数是可使用 IEnumerable<Derived> 的实例而非 IEnumerable<Base>的原因。

using System;
using System.Collections.Generic; class Base
{
public static void PrintBases(IEnumerable<Base> bases)
{
foreach(Base b in bases)
{
Console.WriteLine(b);
}
}
} class Derived : Base
{
public static void Main()
{
List<Derived> dlist = new List<Derived>(); Derived.PrintBases(dlist);
IEnumerable<Base> bIEnum = dlist;
}
}
具有逆变泛型类型参数的泛型接口

从 .NET Framework 4 开始,某些泛型接口具有逆变类型参数;例如:IComparer<T>IComparable<T> 和 IEqualityComparer<T>。 由于这些接口只具有逆变类型参数,因此这些类型参数只用作接口成员中的参数类型。

下面的示例阐释了逆变类型参数。 该示例定义具有MustInherit 属性的抽象(在 Visual Basic 中为 Shape ) Area 类。 该示例还定义一个实现 ShapeAreaComparer (在 Visual Basic 中为 IComparer<Shape> )的IComparer(Of Shape) 类。 IComparer<T>.Compare 方法的实现基于 Area 属性的值,所以 ShapeAreaComparer 可用于按区域对 Shape 对象排序。

Circle 类继承 Shape 并重写 Area。 该示例创建 SortedSet<T> 对象的 Circle ,使用采用 IComparer<Circle> (在 Visual Basic 中为IComparer(Of Circle) )的构造函数。 但是,该对象不传递 IComparer<Circle>,而是传递一个用于实现 ShapeAreaComparer 的 IComparer<Shape>对象。 当代码需要派生程度较大的类型的比较器 (Shape) 时,该示例可以传递派生程度较小的类型的比较器 (Circle),因为 IComparer<T> 泛型接口的类型参数是逆变参数。

向 Circle 中添加新 SortedSet<Circle>对象时,每次将新元素与现有元素进行比较时,都会调用 IComparer<Shape>.Compare 对象的IComparer(Of Shape).Compare 方法(在 Visual Basic 中为 ShapeAreaComparer 方法)。 方法 (Shape) 的参数类型比被传递的类型 (Circle) 的派生程度小,所以调用是类型安全的。 逆变使 ShapeAreaComparer 可以对派生自 Shape的任意单个类型的集合以及混合类型的集合排序。

using System;
using System.Collections.Generic; abstract class Shape
{
public virtual double Area { get { return ; }}
} class Circle : Shape
{
private double r;
public Circle(double radius) { r = radius; }
public double Radius { get { return r; }}
public override double Area { get { return Math.PI * r * r; }}
} class ShapeAreaComparer : System.Collections.Generic.IComparer<Shape>
{
int IComparer<Shape>.Compare(Shape a, Shape b)
{
if (a == null) return b == null ? : -;
return b == null ? : a.Area.CompareTo(b.Area);
}
} class Program
{
static void Main()
{
// 可以传递实现IComparer<shape>的shapeareComparer,
// 即使sortedset<circle>的构造函数需要IComparer<circle>,
// 因为IComparer<t>的类型参数T是反向的。
SortedSet<Circle> circlesByArea = new SortedSet<Circle>(new ShapeAreaComparer())
{ new Circle(7.2),
new Circle(),
null,
new Circle(.)
}; foreach (Circle c in circlesByArea)
{
Console.WriteLine(c == null ? "null" : "Circle with area " + c.Area);
}
}
} /* 输出结果:
null
Circle with area 0.000314159265358979
Circle with area 162.860163162095
Circle with area 31415.9265358979
*/
具有 Variant 类型参数的泛型委托
在 .NET Framework 4 中,Func 泛型委托(如 Func<T,TResult>)具有协变返回类型和逆变参数类型。 Action 泛型委托(如 Action<T1,T2>)具有逆变参数类型。 这意味着,可以将委托指派给具有派生程度较高的参数类型和(对于 Func 泛型委托)派生程度较低的返回类型的变量。

Func 泛型委托的最后一个泛型类型参数指定委托签名中返回值的类型。 该参数是协变的(out 关键字),而其他泛型类型参数是逆变的(in 关键字)。

下面的代码阐释这一点。 第一段代码定义了一个名为 Base的类、一个名为 Derived 的类(此类继承 Base)和另一个具有名为 static 的Shared 方法(在 Visual Basic 中为 MyMethod)的类。 该方法采用 Base 的实例,并返回 Derived 的实例。 (如果参数是 Derived 的实例,则 MyMethod 将返回该实例;如果参数是 Base 的实例,则 MyMethod 将返回 Derived 的新实例。)在 Main() 中,该示例创建一个表示 Func<Base, Derived> 的 Func(Of Base, Derived)(在 Visual Basic 中为 MyMethod)的实例,并将此实例存储在变量 f1 中。
 public class Base {}
public class Derived : Base {} public class Program
{
public static Derived MyMethod(Base b)
{
return b as Derived ?? new Derived();
} static void Main()
{
Func<Base, Derived> f1 = MyMethod; /* 可以将委托分配给类型 Func<Base, Base> (在 Visual Basic 中为Func(Of Base, Base) )的变量,因为返回类型是协变的。*/
// 协变返回类型
Func<Base, Base> f2 = f1;
Base b2 = f2(new Base()); /* 可以将委托分配给类型 Func<Derived, Derived> (在 Visual Basic 中为Func(Of Derived, Derived) )的变量,因为参数类型是逆变的。*/
// 逆变参数类型
Func<Derived, Derived> f3 = f1;
Derived d3 = f3(new Derived()); /* 可以将委托分配给类型 Func<Derived, Base> (在 Visual Basic 中为Func(Of Derived, Base) )的变量,从而将逆变参数类型和协变返回类型的作用结合起来。*/
// 协变返回类型和逆变参数类型。
Func<Derived, Base> f4 = f1;
Base b4 = f4(new Derived());
}
}

泛型委托和非泛型委托中的变体

在上面的代码中, MyMethod 的签名与所构造的泛型委托 Func<Base, Derived> (在 Visual Basic 中为Func(Of Base, Derived) )的签名完全匹配。 此示例说明,只要所有委托类型都是从泛型委托类型 Func<T,TResult>构造的,就可以将此泛型委托存储在具有派生程度更大的参数类型和派生程度更小的返回类型的变量或方法参数中。

这一点非常重要。 泛型委托的类型参数中的协方差和逆变的效果类似于普通委托绑定中的协方差和逆变的效果(请参阅委托中的差异 (C#) 和委托中的差异 (Visual Basic))。 但是,委托绑定中的变化适用于所有委托类型,而不仅仅适用于具有 Variant 类型参数的泛型委托类型。 此外,通过委托绑定中的变化,可以将方法绑定到具有限制较多的参数类型和限制较少的返回类型的任何委托,而对于泛型委托的指派,只有在委托类型是基于同一个泛型类型定义构造的时才可以进行。

下面的示例演示委托绑定中的变化和泛型类型参数中的变化的组合效果。 该示例定义了一个类型层次结构,其中包含三个按派生程度从低到高排列的类型,即Type1的派生程度最低,Type3的派生程度最高。 普通委托绑定中的变化用于将参数类型为 Type1 、返回类型为 Type3 的方法绑定到参数类型为 Type2 、返回类型为 Type2的泛型委托。 然后,使用泛型类型参数的协变和逆变,将得到的泛型委托指派给另一个变量,此变量的泛型委托类型的参数类型为 Type3 ,返回类型为 Type1。 第二个指派要求变量类型和委托类型是基于同一个泛型类型定义(在本例中为 Func<T,TResult>)构造的。

using System;

public class Type1 {}
public class Type2 : Type1 {}
public class Type3 : Type2 {} public class Program
{
public static Type3 MyMethod(Type1 t)
{
return t as Type3 ?? new Type3();
} static void Main()
{
Func<Type2, Type2> f1 = MyMethod; // 协变返回类型和逆变参数类型
Func<Type3, Type1> f2 = f1;
Type1 t1 = f2(new Type3());
}
}
定义 Variant 泛型接口和委托
从 .NET Framework 4 开始,Visual Basic 和 C# 提供了一些关键字,利用这些关键字,可以将接口和委托的泛型类型参数标记为协变或逆变。

从 .NET Framework 2.0 版开始,公共语言运行时支持泛型类型参数上的变化批注。 在 .NET Framework 4 之前,定义包含这些批注的泛型类的唯一方法就是利用 Ilasm.exe(IL 汇编程序) 编译该类或在动态程序集中发出该类,从而使用 Microsoft 中间语言 (MSIL)。

协变类型参数用 out 关键字(在 Visual Basic 中为Out 关键字,在 + MSIL 汇编程序 中为)标记。 可以将协变类型参数用作属于接口的方法的返回值,或用作委托的返回类型。 但不能将协变类型参数用作接口方法的泛型类型约束。

如果接口的方法具有泛型委托类型的参数,则接口类型的协变类型参数可用于指定委托类型的逆变类型参数。

逆变类型参数用 in 关键字(在 Visual Basic 中为In 关键字,在 - MSIL 汇编程序 中为)标记。 可以将逆变类型参数用作属于接口的方法的参数类型,或用作委托的参数类型。 也可以将逆变类型参数用作接口方法的泛型类型约束。

只有接口类型和委托类型才能具有 Variant 类型参数。 接口或委托类型可以同时具有协变和逆变类型参数。

Visual Basic 和 C# 不允许违反协变和逆变类型参数的使用规则,也不允许将协变和逆变批注添加到接口和委托类型之外的类型参数中。 MSIL 汇编程序 不执行此类检查,但如果你尝试加载违反规则的类型,则会引发 TypeLoadException 。

有关信息和示例代码,请参阅泛型接口中的差异 (C#) 和泛型接口中的差异 (Visual Basic)

Variant 泛型接口和委托类型的列表

在 .NET Framework 4 中,下面的接口和委托类型具有协变和/或逆变类型参数。

 

C#4.0新增功能03 泛型中的协变和逆变的更多相关文章

  1. Java泛型中的协变和逆变

    Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛 ...

  2. .NET泛型中的协变与逆变

    泛型的可变性:协变性和逆变性 实质上,可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用. 我们已经习惯了普通继承中的可变性:例如,若某方法声明返回类型为Stream,在实现时可以返回一个M ...

  3. .NET泛型03,泛型类型的转换,协变和逆变

    协变(Convariant)和逆变(Contravariant)的出现,使数组.委托.泛型类型的隐式转换变得可能. 子类转换成基类,称之为协变:基类转换成子类,称之为逆变..NET4.0以来,支持了泛 ...

  4. C#4.0中的协变和逆变

    原文地址 谈谈.Net中的协变和逆变 关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Anima ...

  5. Scala中的协变,逆变,上界,下界等

    Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...

  6. .net中的协变和逆变

    百度:委托中的协变和逆变. 百度:.net中的协变和逆变. 协变是从子类转为父类. 逆变是从父类到子类. 这样理解不一定严谨或者正确.需要具体看代码研究.

  7. [改善Java代码]警惕泛型是不能协变和逆变的

    什么叫做协变(covariance)和逆变(contravariance)? 在变成语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数,泛型,返回值)替换或交换的特性,简单的说,协变是 ...

  8. Java语言中的协变和逆变(zz)

    转载声明: 本文转载至:http://swiftlet.net/archives/1950 协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性.简单的说,协变就是用一个窄类型替代宽类型,而逆 ...

  9. Java中的协变与逆变

    Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...

随机推荐

  1. QT在linux环境下读取和设置系统时间(通过system来直接调用Linux命令,注意权限问题)

    QT在Linux环境下读取和设置系统时间 本文博客链接:http://blog.csdn.NET/jdh99,作者:jdh,转载请注明. 环境: 主机:Fedora12 开发软件:QT 读取系统时间 ...

  2. 利用js参数,保持客户端文件的新鲜度

    不知道你是否碰到过如下情况,在服务端更新了一个重要的js文件后,由于浏览器的缓存机制,导致用户始终不能获取到最新的文件,此时的你恨不得有孙悟空吹毛化身的法术,帮用户清除浏览器的缓存.缓存既是程序员的好 ...

  3. python中的set集合和深浅拷贝

    一.基础数据类型的补充 1.str中的join算法,将列表转换成字符串,并用'_'(或其他) li=['李嘉诚','马化腾','刘嘉玲','黄海峰',] s='_'.join(li) print(s) ...

  4. vue+TS(CLI3)

    1.用CLI3创建项目 查看当前CLI的版本,如果没有安装CLI3的  使用npm install --global vue-cli来安装CLI 安装好CLI 可以创建项目了 使用vue create ...

  5. Spring之bean生命始末

    可以为Bean定制初始化后的生命行为,也可以为Bean定制销毁前的生命行为.举例:ba06包.首先,这些方法需要在Bean类中事先定义好:是方法名随意的public void方法. 其次,在配置文件的 ...

  6. Python连载12-shutil模块

    一.shutil模块 1.函数:copy() (1)用法:复制文件0 (2)格式:copy(来源路径,目标路径) (3)返回值:返回目标路径 (4)注意:拷贝的同时可以给文件重命名 source_pa ...

  7. 推荐三个学习git的网站或教程

    廖雪峰官方教程:https://www.liaoxuefeng.com/wiki/896043488029600/900388704535136 ProGit中文版:https://git-scm.c ...

  8. iOS开发(4):录音AVAudioRecorder

    录音,声音的采集,一般有两种实现办法,一是使用AVAudioRecorder,一是使用AudioUnit.如果只是简单的录音,使用AVAudioRecorder就可以了,如果想更灵活地处理刚录到的声音 ...

  9. 智能小程序关于Filter过滤器的简单使用

    <filter module="swan"> export default { imgurl: (imgUrl) => { var imgurlprefix = ...

  10. 【简易bat脚本】启动java程序

    前置条件:path中添加了JAVAHOME配置了java环境变量 1.新建txt文本文件 2.粘贴以下内容 @echo off set path=%path%;.;java -classpath &q ...