[翻译]协变性与逆变性FAQ

原文来自Alexandra Rusina在CSharpFAQCovariance and Contravariance FAQ

在这篇文章我尝试回答我在论坛和文档反馈里找到的最常见的关于C#协变性和逆变性的问题。对于一篇博文来讲,这是个大话题,所以你可能会看到大量“更多信息”的链接。

特别感谢 Eric LippertChris Burrows 的检阅和提供很有帮助的意见。


什么是协变性(covariance)和逆变性(contravariance)[又译为“反变性”]?

在C#里,协变性和逆变性使数组类型、委托类型和泛型类型参数能够隐式的引用转换。协变性保持分配兼容性(assignment compatibility)而逆变性反转它。


下面的代码演示分配兼容性、协变性和逆变性三者间的区别。

// 分配兼容性。
string str = "test";
// 派生程度较大的对象分配到派生程度较小的对象引用

object obj = str;

// 协变性。
IEnumerable<string> strings = new List<string>();
// 参数类型派生程度较大实例化对象
// 分配给参数类型派生程度较小的对象引用。
// 分配兼容性被保留。
IEnumerable<object> objects = strings;

// 逆变性。          
// 假定我们有这么个方法:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// 参数类型派生程度较小的实例化对象
// 分配给参数类型派生程度较大的对象引用。
// 分配兼容性被反转。
Action<string> actString = actObject;

在C#里,下面的情况下支持可变性:

 

1.数组中的协变性(从C#1.0起)
2.委托中的协变性和逆变性,也称作“方法组可变性”(从C#2.0起)
3.接口和委托中的泛型参数的可变性(从C#4.0起)

 

什么是数组协变性?

 

从C#1.0起数组就是协变的。你一直都可以这样:

 

object[] obj = new String[10];

 

在上面的代码,我将一个string的数组分配到object的数组变量中。所以,使用比原本类型派生程度较大的类型,就是协变性。

数组中的协变性被认为是“不安全”的,因为你可以这样做:

obj[0] = 5;

此代码可以编译通过,但它在运行时会抛出一个异常,因为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<Object> objects = new List<String>();

虽然这段代码看上去并不令人印象深刻,但它让你可以重新使用很多接收IEnumerable objects的方法。

class Program
{
    // 方法有个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主题用来展示你能怎么受益于这个新特性。他们也许能帮你更好的理解协变性和逆变性的原理。


同样,可以看看这个视频,Eric Lippert的 How Do I: Use Covariance and Contravariance in VS 2010 Part I? 

我自己要怎么来创建可变的泛型接口和委托?

关键字out标记一个类型参数是协变的,关键字in标记它是逆变的。两个非常重要的规则需要记住:

  • 如果一个泛型类型参数仅用来作为方法返回值,而没有用来作为方法参数,那么你可以标记它为协变的。
  • 反之亦然,如果一个泛型类型参数仅用来作为方法参数,而没有用来作为方法返回值,你可以标记它为逆变的。


关于可变性是否有效的更多信息,读读MSDN里的 Creating Variant Generic InterfacesVariance in Delegates ,还有 Eric Lippert’s 的 Exact rules for variance validity

这个例子展示怎么创建一个可变泛型接口:

interface IVariant<out R, in A>
{
    // 这些方法符合规则。
    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> { }

再次强调,这个特性只支持泛型接口和委托。那么下面的代码不能编译:

class Sample<out T> { }  // 编译器错误。

更多示例,看看Eric Lippert的视频 How Do I: Use Covariance and Contravariance in VS 2010 Part II? 

我能在哪里找到更多关于协变性和逆变性的深入信息?

这是MSDN根主题:Covariance and Contravariance.

当然,可以读读 Eric Lippert’s blog。他为C#4.0设计了这个特性,那么谁会比他知道得更多?

[译注:中文相关讨论里,有篇不错的推荐:装配脑袋的《.NET 4.0中的泛型协变和反变]

posted @ 2010-05-17 21:48 甜番薯 阅读(...) 评论(...) 编辑 收藏