[翻译]协变性与逆变性FAQ
原文来自Alexandra Rusina在CSharpFAQ的Covariance and Contravariance FAQ
在这篇文章我尝试回答我在论坛和文档反馈里找到的最常见的关于C#协变性和逆变性的问题。对于一篇博文来讲,这是个大话题,所以你可能会看到大量“更多信息”的链接。
什么是协变性(covariance)和逆变性(contravariance)[又译为“反变性”]?
下面的代码演示分配兼容性、协变性和逆变性三者间的区别。
// 分配兼容性。
string str = "test";
// 派生程度较大的对象分配到派生程度较小的对象引用
// 协变性。
IEnumerable<string> strings = new List<string>();
// 参数类型派生程度较大的实例化对象
IEnumerable<object> objects = strings;
// 逆变性。
// 假定我们有这么个方法:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// 参数类型派生程度较小的实例化对象
Action<string> actString = actObject;
在C#里,下面的情况下支持可变性:
数组中的协变性被认为是“不安全”的,因为你可以这样做:
此代码可以编译通过,但它在运行时会抛出一个异常,因为obj事实上是string类型的数组,不能包含int类型。
什么是委托或方法组的可变性?
此特性是在C#2.0中添加的。当你实例化一个委托,你给它分配的方法,其返回值类型可以比此委托的返回值类型派生程度更大(协变性)。你同样可以让其参数类型比委托的参数类型派生程度更小(逆变性)。
这里是代码样例,用来说明此特性及其限制:
static object GetObject() { return null; }
static void SetObject(object obj) { }
static string GetString() { return ""; }
static void SetString(string str) { }
static void Main()
{
// 协变性。 委托指定了返回值是object类型,
// 但是我可以为其赋一个返回string类型的方法。
Func<object> del = GetString;
// 逆变性。委托指定一个string类型的参数,
// 但是我可以为其赋一个object类型参数的方法。
Action<string> del2 = SetObject;
// 但是,到C#4.0为止,都还不支持泛型委托之间的隐式转换
Func<string> del3 = GetString;
Func<object> del4 = del3; // C#4.0为止,这里都是编译器错误
}
什么是泛型参数的可变性?
这是C#4.0的新特性。现在,当你创建一个泛型接口,你可以指定在不同参数类型的接口实例间是否可以有隐式转换。例如,你可以使用一个返回值类型比原本指定类型派生程度更大的方法(协变性)或参数类型派生程度更小的方法(逆变性)的接口实例。同样的规则适用于泛型委托。
虽然你自己可以创建一个可变的接口和委托,但这不是这个特性的主要目的。更重要的是,.NET Framework 4中接口和委托的集合已经更新成可变的了。
- IEnumerable<T> (T协变)
- IEnumerator<T> (T协变)
- IQueryable<T> (T协变)
- IGrouping<TKey, TElement> (TKey和TElement协变)
- IComparer<T> (T逆变)
- IEqualityComparer<T> (T逆变)
- IComparable<T> (T逆变)
这里是委托更新列表:
- System命名空间的Action委托,例如, Action<T> and Action<T1, T2> (T, T1, T2, 等等是逆变)
- System命名空间的Func委托, 例如, Func<TResult> 和 Func<T, TResult> (TResult 是协变; T, T1, T2, 等等是逆变)
- Predicate<T> (T是逆变)
- Comparison<T> (T是逆变)
- Converter<TInput, TOutput> (TInput是逆变; TOutput是协变.)
估计大部分用户的多数情景都是像这样:
IEnumerable<Object> objects = new List<String>();
虽然这段代码看上去并不令人印象深刻,但它让你可以重新使用很多接收IEnumerable objects的方法。
{
// 方法有个IEnumerable<Person>类型的参数
public static void PrintFullName(IEnumerable<Person> persons)
{
// 方法遍历集合并打印一些信息。
}
public static void Main()
{
List<Employee> employees = new List<Employee>();
// 我可以传递List<Employee>,实际上是IEnumerable<Employee>,
// 虽然方法预期的是IEnumerable<Person>。
PrintFullName(employees);
}
}
有两个重要的规则需要记住:
-
这个特性仅仅针对泛型接口和委托。如果你实现一个可变泛型接口,实现类仍然是不可变的。在C#4.0,类和结构不支持可变性。// List<T>实现协变接口 IEnumerable<out T>。但类是不可变的。
List<Person> list = new List<Employee>(); // 编译器错误。 - 可变性仅仅支持引用类型的参数,可变性不支持值类型。下面的代码也不会编译通过。
// int是值类型,所以代码不能编译通过。
IEnumerable<Object> objects = new List<int>(); // 编译器错误。
我在哪里能找到使用协变性和逆变性的更多例子?
我写了两篇MSDN主题用来展示你能怎么受益于这个新特性。他们也许能帮你更好的理解协变性和逆变性的原理。
- Using Variance in Interfaces for Generic Collections
- Using Variance for Func and Action Generic Delegates
同样,可以看看这个视频,Eric Lippert的 How Do I: Use Covariance and Contravariance in VS 2010 Part I?
我自己要怎么来创建可变的泛型接口和委托?
关键字out标记一个类型参数是协变的,关键字in标记它是逆变的。两个非常重要的规则需要记住:
- 如果一个泛型类型参数仅用来作为方法返回值,而没有用来作为方法参数,那么你可以标记它为协变的。
- 反之亦然,如果一个泛型类型参数仅用来作为方法参数,而没有用来作为方法返回值,你可以标记它为逆变的。
关于可变性是否有效的更多信息,读读MSDN里的 Creating Variant Generic Interfaces 和 Variance in Delegates ,还有 Eric Lippert’s 的 Exact rules for variance validity。
这个例子展示怎么创建一个可变泛型接口:
{
// 这些方法符合规则。
R GetR();
void SetA(A sampleArg);
R GetRSetA(A sampleArg);
// 这些方法不满足规则。
// A GetA();
// void SetR(R sampleArg);
// A GetASetR(R sampleArg);
}
如果你扩展一个可变的接口,派生接口默认是不可变的。你必须用out或in关键字显示指定类型参数是协变的或逆变的。这里是个来自MSDN的简单例子。
interface ICovariant<out T> { }
// 这个接口是不可变的,因为我没有使用“out”关键字。
interface IInvariant<T> : ICovariant<T> { }
// 这个则是协变的,因为我明确指定了“out”关键字。
interface IExtCovariant<out T> : ICovariant<T> { }
再次强调,这个特性只支持泛型接口和委托。那么下面的代码不能编译:
我能在哪里找到更多关于协变性和逆变性的深入信息?
这是MSDN根主题:Covariance and Contravariance.
当然,可以读读 Eric Lippert’s blog。他为C#4.0设计了这个特性,那么谁会比他知道得更多?
[译注:中文相关讨论里,有篇不错的推荐:装配脑袋的《.NET 4.0中的泛型协变和反变》]
转载需注明出处:http://www.cnblogs.com/tianfan/