.NET Framework 4 中的新 C# 功能---协变与逆变

转载于:http://msdn.microsoft.com/zh-cn/magazine/ff796223.aspx

Chris Burrows          

C# 编程语言自 2002 年初次发布以来已经有了极大的改善,可以帮助程序员编写更清晰易懂、更容易维护的代码。这种改善来自于不断加入的新功能,例如泛型类型、可为空的值类型、lambda 表达式、迭代器方法、分部类以及其他大量有用的语言结构。而且,这些改变还经常伴随着为 Microsoft .NET Framework 库提供相应的支持。

C# 4.0 延续了这种不断提高易用性的趋势。这款产品大大简化了许多常见任务,包括泛型类型、传统的互操作以及处理动态对象模型。本文旨在为深入调查探讨这些新功能。我将先介绍泛型方差,然后探讨传统互操作特性和动态互操作特性。

协变与逆变

协变与逆变最好通过示例来介绍,而最好的示例就在框架中。在 System.Collections.Generic 中,IEnumerable<T> 和 IEnumerator<T> 分别表示一个包含 T 的序列的对象和一个用来遍历该序列的枚举器(或迭代器)。这些接口长期以来承担了大量繁重的任务,因为它们允许实现 foreach 循环构造。在 C# 3.0 中,它们变得更加突出,因为它们在 LINQ 和 LINQ 到对象中起着重要作用:它们是表示序列的 .NET 接口。

因此,假如您有一个类层次结构,其中包含一个 Employee 类型以及从这个 Employee 类型派生而来的 Manager 类型(毕竟经理也是员工),那么您认为以下代码会产生什么效果?

IEnumerable<Manager> ms = GetManagers();
IEnumerable<Employee> es = ms;

        

看起来好像应该能够将 Manager 序列当作 Employee 序列。但是在 C# 3.0 中,赋值操作将失败;编译器将提示您,没有相应的转换功能。毕竟,该版本根本不能理解 IEnumerable<T> 的语义。这可以是任何接口,因此对于任意接口 IFoo<T>,为什么就能说 IFoo<Manager> 基本上能替代 IFoo<Employee> 呢?

而在 C# 4.0 中,赋值操作是有效的,因为 IEnumerable<T> 以及其他几种接口均发生了变化,这种变化是由 C# 中新增的类型参数协变支持实现的。

IEnumerable<T> 比任意 IFoo<T> 更加特殊,因为尽管初看起来并不明显,但是使用类型参数 T 的成员(IEnumerable<T> 中的 GetEnumerator 和 IEnumerator<T> 中的 Current 属性)实际上仅在返回值的位置使用 T。因此,您只能从序列中获取 Manager,而绝不能向其中放入 Manager。

相比之下,让我们看看 List<T>。由于以下原因,用 List<Manager> 来替代 List<Employee> 将是一场灾难:

List<Manager> ms = GetManagers();
List<Employee> es = ms; // Suppose this were possible
es.Add(new EmployeeWhoIsNotAManager()); // Uh oh

        

正如这段代码所示,如果您认为您是在查看 List<Employee>,那您就可以插入任何员工。但实际上正在操作的列表是 List<Manager>,因此插入非 Manager 的员工一定会失败。如果允许这种操作,就会失去类型安全性。List<T> 不能对 T 进行协变。

而 C# 4.0 中有一项新的语言功能,允许定义一些类型(例如新的 IEnumerable<T>),只要正在处理的类型参数之间存在一定的关系,就允许在这些类型参数之间进行转换。.NET Framework 开发人员编写 IEnumerable<T> 时就使用了这项特性,他们的代码类似于下面的代码(当然经过了简化):

public interface IEnumerable<out T> { /* ... */ }

        

请注意修饰类型参数 T 的定义的 out 关键字。当编译器遇到此关键字时,会将 T 标记为协变,并检查接口定义中使用的 T 是否均正常(即,它们是否仅在输出位置使用,这也就是 out 关键字的由来)。

为什么将这种特性称为协变呢?通过绘制箭头,最容易看出原因。为了更形象,还是让我们使用 Manager 和 Employee 类型。由于这两个类之间存在继承关系,从 Manager 到 Employee 之间存在隐式引用转换。

Manager → Employee        

现在,由于 IEnumerable<out T> 中 T 的注释,从 IEnumerable<Manager> 到 IEnumerable<Employee> 之间也存在隐式引用转换。这就是 T 注释的目的:

IEnumerable<Manager> → IEnumerable<Employee>        

这被称为协变,因为两个示例中的箭头都指向相同的方向。我们首先定义两个类型 Manager 和 Employee。然后利用这两个类型来定义新类型 IEnumerable<Manager> 和 IEnumerable<Employee>。新类型的转换方式与旧类型一样。

当这两种转换的方向相反时,即为逆变。您可能已经想到这种情况将发生在仅将类型参数 T 用作输入时,那您就对了。例如,System 命名空间包含一个名为 IComparable<T> 的接口,该接口有一个名为 CompareTo 的方法:

public interface IComparable<in T> { 
  bool CompareTo(T other); 
}

        

如果您有 IComparable<Employee>,则应该能够将其视为 IComparable<Manager>,因为您能做的唯一操作就是将 Employee 添加到该接口中。由于经理本身就是员工,加入经理应该是可行的,实际上也确实加入成功了。本例使用 in 关键字来修饰 T,并且以下方案能够正确执行:

IComparable<Employee> ec = GetEmployeeComparer();
IComparable<Manager> mc = ec;

        

这称为逆变,因为这次的两个箭头方向相反:

Manager → Employee IComparable<Manager> ← IComparable<Employee>        

到现在为止,可以很容易地总结语言特性:在您定义类型参数时可以添加 in 或 out 关键字,从而为您提供额外的自由转换。不过,还是有一些限制。

首先,这种方式仅适用于泛型接口和委托。您不能以这种方式为类或结构声明泛型参数。造成这种限制的一个简单原因是:委托很像只拥有一个方法的接口,而由于字段的存在,无论如何都不能将类看作某种形式的接口。您可以将泛型类的任何字段当作既是输入又是输出,具体取决于您是对它执行写入还是读取操作。如果这些字段涉及类型参数,则这些参数既不能协变也不能逆变。

其次,如果某个接口或委托具有协变或逆变类型参数,则只有在该接口使用(而不是其定义)中,类型参数是引用类型时,才允许对该类型执行新的转换。例如,由于 int 是值类型,因此即使看起来应该能行,但是 IEnumerator<int> 事实上不能转换为 IEnumerator <object>:

IEnumerator <int>              IEnumerator <object>

出现这种行为的原因是转换必须保留类型的表现形式。如果允许执行 int 到 object 的转换,就不可能调用结果的 Current 属性,因为值类型 int 与对象引用在堆栈中所具有的表现形式是不一样的。但所有引用类型在堆栈中都具有相同的表现形式,因此只有在类型参数为引用类型时,才能实现这些额外的转换。

大多数 C# 开发人员很可能会开心地使用这项新的语言功能,因为在使用 .NET Framework 提供的某些类型(IEnumerable<T>、IComparable<T>、Func<T>、Action<T> 等等)时,他们会获得更多框架类型的转换,而且编译器错误也会更少。事实上,只要设计的库包含泛型接口和委托,设计人员就可以根据需要自由使用新的 in 和 out 类型参数,使其用户能够更轻松地使用他们设计的库。

另外,此功能需要运行时的支持,但这项支持早就存在了。可是由于没有什么语言用到这项支持,它已经沉寂了好几个版本。同样,前几个版本的 C# 允许一些有限的逆变转换。特别是,它们允许您使用具有兼容返回类型的方法来生成委托。此外,数组类型始终都是协变的。这些既有的功能与 C# 4.0 中的新功能截然不同,后者实际上是允许您定义自己的类型,使类型中的部分类型参数支持协变和逆变。

posted on 2013-11-13 15:42  决定走下去  阅读(130)  评论(0)    收藏  举报

导航