1. 可变性的类型:协变性和逆变性


可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量。

协变和逆变是两个相互对立的概念:

  • 如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的
  • 如果某个参数类型可以由其基类替换,那么这个类型就是支持逆变的。

2. C# 4.0对泛型可变性的支持


在C# 4.0之前,所有的泛型类型都是不变量——即不支持将一个泛型类型替换为另一个泛型类型,即使它们之间拥有继承关系,简而言之,在C# 4.0之前的泛型都是不支持协变和逆变的。

C# 4.0通过两个关键字:out和in来分别支持以协变和逆变的方式使用泛型。

我们来看一段利用了协变类型参数的代码:

public class BaseClass
{
//...
} public class DerivedClass : BaseClass
{
//...
}

下面我们利用协变类型参数,可以执行类似于普通的多态性的分配:

IEnumerable<DerivedClass> d = new List<DerivedClass>();
IEnumerable<BaseClass> b = d;

在上面的实例中,在C# 4.0之前是不能正常编译的,除了对赋值给基类集合时将子类集合做一个强制转换,但是在运行时仍然会抛出一个类型转换的异常。

下面我们再看一个关于逆变的实例代码:

Action<BaseClass> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<DerivedClass> d = b;
d(new DerivedClass());

在上面的示例中我们 Action<BaseClass> 类型的委托分配给类型 Action<DerivedClass> 的变量,根据逆变的定义我们可以知道 Action<T> 类型是支持逆变的。

为什么IEnumerable<T> 和 Action<T> 可以分别支持类型的协变和逆变呢?我们查看这两个类型在 .NET 中的定义:

//IEnumerable<T> 接口的定义(支持协变)
public interface IEnumerable<out T> : IEnumerable //Action<T> 委托的定义(支持逆变)
public delegate void Action<in T>(T obj);

为了保证类型的安全,C#编译器对使用了 out 和 in 关键字的泛型参数添加了一些限制:

  • 支持协变(out)的类型参数只能用在输出位置:函数返回值、属性的get访问器以及委托参数的某些位置
  • 支持逆变(in)的类型参数只能用在输入位置:方法参数或委托参数的某些位置中出现。

3. C#中泛型可变性的限制


1. 不支持类的类型参数的可变性

只有接口和委托可以拥有可变的类型参数。in 和 out 修饰符只能用来修饰泛型接口和泛型委托。

2. 可变性只支持引用转换

可变性只能用于引用类型,禁止任何值类型和用户定义的转换,如下面的转换是无效的:

  • 将 IEnumerable<int> 转换为 IEnumerable<object> ——装箱转换
  • 将 IEnumerable<short> 转换为 IEnumerable<int> ——值类型转换
  • 将 IEnumerable<string> 转换为 IEnumerable<XName> ——用户定义的转换

3. 类型参数使用了 out 或者 ref 将禁止可变性

对于泛型类型参数来说,如果要将该类型的实参传给使用 out 或者 ref 关键字的方法,便不允许可变性,如:

delegate void someDelegate<in T>(ref T t)

这段代码编译器会报错。

4. 可变性必须显式指定

从实现上来说编译器完全可以自己判断哪些泛型参数能够逆变和协变,但实际却没有这么做,这是因为C#的开发团队认为:

必须由开发者明确的指定可变性,因为这会促使开发者考虑他们的行为将会带来什么后果,从而思考他们的设计是否合理。

5. 注意破坏性修改

在修改已有代码接口的可变性时,会有破坏当前代码的风险。例如,如果你依赖于不允许可变性的is或as操作符的结果,运行在.NET 4时,代码的行为将有所不同。同样,在某些情况下,因为有了更多可用的选项,重载决策也会选择不同的方法。所以在对已有代码引入可变性时要做好足够的单元测试以及防御措施。

6. 多播委托与可变性不能混用

下面的代码能够通过编译,但是在运行时会抛出 ArgumentException 异常:

Func<string> stringFunc = () => "";
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + stringFunc;

这是因为负责链接多个委托的 Delegate.Combine方法要求参数必须为相同的类型。上面的示例我们可以修改成如下正确的代码:

Func<string> stringFunc = () => "";
Func<object> defensiveCopy = new Func<object>(stringFunc);
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + defensiveCopy;

4. C#中协变和逆变的继承关系


1. C#中父接口(interface)的泛型参数是协变(out)或逆变(in)时,子接口的泛型参数可以不是协变或逆变:

/// <summary>
/// IDemoA是父接口
/// </summary>
public interface IDemoA<in T1, out T2>
{
} // <summary>
/// IDemoB是子接口,继承父接口IDemoA
/// </summary>
public interface IDemoB<T1, T2> : IDemoA<T1, T2>
{
}

我们可以看到上面的代码中父接口IDemoA<in T1, out T2>的泛型参数T1是逆变、T2是协变,子接口IDemoB<T1, T2>的泛型参数T1可以不是逆变、T2也可以不是协变。

2. C#中父接口(interface)的泛型参数是协变(out)或逆变(in)时,子接口的泛型参数也可以是协变或逆变:

/// <summary>
/// IDemoA是父接口
/// </summary>
public interface IDemoA<in T1, out T2>
{
} // <summary>
/// IDemoB是子接口,继承父接口IDemoA
/// </summary>
public interface IDemoB<in T1, out T2> : IDemoA<T1, T2>
{
}

这种情况就不多说了,上面的代码一看就明白。

3. C#中父接口(interface)的泛型参数是协变(out)时,子接口的泛型参数不可以是逆变(in):

/// <summary>
/// IDemoA是父接口
/// </summary>
public interface IDemoA<out T1>
{
} // <summary>
/// IDemoB是子接口,继承父接口IDemoA
/// </summary>
public interface IDemoB<in T1> : IDemoA<T1>
{
}

可以看到当父接口IDemoA<out T1>的泛型参数T1是协变,而子接口IDemoB<in T1>的泛型参数T1是逆变时,C#代码编译时会报错,这是不允许的。

4. C#中父接口(interface)的泛型参数是逆变(in)时,子接口的泛型参数不可以是协变(out):

/// <summary>
/// IDemoA是父接口
/// </summary>
public interface IDemoA<in T1>
{
} // <summary>
/// IDemoB是子接口,继承父接口IDemoA
/// </summary>
public interface IDemoB<out T1> : IDemoA<T1>
{
}

可以看到当父接口IDemoA<in T1>的泛型参数T1是逆变,而子接口IDemoB<out T1>的泛型参数T1是协变时,C#代码编译时会报错,这是不允许的。

5. C#中父接口(interface)的泛型参数没有协变(out)时,子接口的泛型参数也不可以是协变(out):

/// <summary>
/// IDemoA是父接口
/// </summary>
public interface IDemoA<T1>
{
} // <summary>
/// IDemoB是子接口,继承父接口IDemoA
/// </summary>
public interface IDemoB<out T1> : IDemoA<T1>
{
}

可以看到当父接口IDemoA<T1>的泛型参数T1没有协变,而子接口IDemoB<out T1>的泛型参数T1是协变时,C#代码编译时会报错,这是不允许的。

6. C#中父接口(interface)的泛型参数没有逆变(in)时,子接口的泛型参数也不可以是逆变(in):

/// <summary>
/// IDemoA是父接口
/// </summary>
public interface IDemoA<T1>
{
} // <summary>
/// IDemoB是子接口,继承父接口IDemoA
/// </summary>
public interface IDemoB<in T1> : IDemoA<T1>
{
}

可以看到当父接口IDemoA<T1>的泛型参数T1没有逆变,而子接口IDemoB<in T1>的泛型参数T1是逆变时,C#代码编译时会报错,这是不允许的。

参考&扩展阅读

协变和逆变

泛型中的协变和逆变

委托中的协变和逆变

《深入理解C#》:13.3 接口和委托的泛型可变性

《Effective C#》:条目29:支持泛型协变和逆变

《CLR via C#》:12.5 委托和接口的逆变和协变泛型类型实参

原文链接

C# 泛型的协变和逆变 (转载)的更多相关文章

  1. 转载.NET 4.0中的泛型的协变和逆变

    先做点准备工作,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义如下:   public class Animal { } public ...

  2. .NET 4.0中的泛型的协变和逆变

    转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...

  3. Java用通配符 获得泛型的协变和逆变

    Java对应泛型的协变和逆变

  4. Kotlin泛型与协变及逆变原理剖析

    在上一次https://www.cnblogs.com/webor2006/p/11234941.html中学习了数据类[data class]相关的知识,这次会学习关于泛型相关的东东,其中有关于泛型 ...

  5. Java泛型的协变与逆变

    泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods  have same erasure ...

  6. C#4.0泛型的协变,逆变深入剖析

    C#4.0中有一个新特性:协变与逆变.可能很多人在开发过程中不常用到,但是深入的了解他们,肯定是有好处的. 协变和逆变体现在泛型的接口和委托上面,也就是对泛型参数的声明,可以声明为协变,或者逆变.什么 ...

  7. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  8. C#-弄懂泛型和协变、逆变

    脑图概览 泛型声明和使用 协变和逆变 <C#权威指南>上在委托篇中这样定义: 协变:委托方法的返回值类型直接或者间接地继承自委托前面的返回值类型; 逆变:委托签名中的参数类型继承自委托方法 ...

  9. 深入理解 C# 协变和逆变 (转载)

      深入理解 C# 协变和逆变 msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型. “逆变”则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样,不过不 ...

随机推荐

  1. https、公钥、私钥白话解说

    原文 摘要:本文尝试一步步还原HTTPS的设计过程,以理解为什么HTTPS最终会是这副模样.但是这并不代表HTTPS的真实设计过程.在阅读本文时,你可以尝试放下已有的对HTTPS的理解,这样更利于“还 ...

  2. IIS 网站发布——遇到的问题,以及解决方案

    在 解决方案——Web 右键  发布  如图:   之后,在其他文件夹或盘  放入发布之后 生成的文件 如图所示,之所以这样是为了好整理查找发布的项目.   之后,在IIS 上发布出去     首先 ...

  3. ccf-201809-2 买菜

    问题描述 小H和小W来到了一条街上,两人分开买菜,他们买菜的过程可以描述为,去店里买一些菜然后去旁边的一个广场把菜装上车,两人都要买n种菜,所以也都要装n次车.具体的,对于小H来说有n个不相交的时间段 ...

  4. C# CRC16算法实现【转】

    /// <summary> /// CRC校验 /// </summary> public class CRC { #region CRC16 public static by ...

  5. Thrift-RPC client in Flume

    Get RpcClient from RpcClientFactory with Reflection programming Message or Event definition in Flum ...

  6. 如何实现Docker应用的自定义弹性伸缩

    简介 现在有很多客户很关心应用的自动弹性伸缩,有些客户也有自己的监控框架,并希望能跟阿里云容器服务进行集成.阿里云容器服务提供了服务弹性伸缩触发器,并能够跟监控框架集成来实现自定义的服务自动弹性伸缩. ...

  7. Java中生成帮助文档

    如何在Java中使用注释 在编写程序时,经常需要添加一些注释,用以描述某段代码的作用. 一般来说,对于一份规范的程序源代码而言,注释应该占到源代码的 1/3 以上.因此,注释是程序源代码的重要组成部分 ...

  8. idea maven 依赖问题

    今天发现,原来idea引用maven依赖的时候,优先依赖的是本地.但是有个问题,如果我删除了一个packet,如果刚刚好本地另外一个项目里面也有这个对于的packet,idea的智能提示 是 将其导入 ...

  9. css display属性详解

    css display属性在对css做layout设计时非常重要,它的值有以下几种: Value Description Play it inline Default value. Displays ...

  10. Data Flow ->> Source ->> Error Output ->> Error & Truncation: Ignore Failure, Redirect Now, Fail Component

    Ignore Failure: 当该字段遇到错误时,字段值被设为NULL Redirect Now: 把该行输出到SSIS的Source组件的红色输出线,这时红色输出线应该连接一个可以接受结果集的组件 ...