C# 泛型的协变和逆变

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

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

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

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

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

C# 4.0经过两个关键字:outin来分别支持以协变和逆变的方式使用泛型。单元测试

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

public class BaseClass
{
    //...
}

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

下面咱们利用协变类型参数,能够执行相似于普通的多态性的分配:设计

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

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

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

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#编译器对使用了 outin 关键字的泛型参数添加了一些限制:开发

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

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

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

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

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;

参考&扩展阅读

协变和逆变
泛型中的协变和逆变
委托中的协变和逆变
《深刻理解C#》:13.3 接口和委托的泛型可变性
《Effective C#》:条目29:支持泛型协变和逆变
《CLR via C#》:12.5 委托和接口的逆变和协变泛型类型实参

相关文章
相关标签/搜索