一、可变性的概念

C# 一开始就支持数组的协变,听说是为了和 Java 竞争于是就把 Java 的这个不怎么样的特性给实现了。

1
object[] myArray = new string[] { "abc", "def", "ghi", // ... };

所谓协变性指的就是,在一个使用宽泛类型的地方,可以传入一个具体类型的对象。那么,最基本的协变就是面向对象中的多态——使用基类型对象的地方,都可以传入派生类对象。

但是,上面这个数组的协变性还是很特别的。首先,在 C# 的类型系统中,派生类型的数组并不继承自基类型的数组(别的语言也是如此吧)。所以,它的确是一种新的协变性。其次,如果你做一个如下操作,你就会收到运行时错误,告诉你数组类型不匹配:

1
myArray[0] = 3;

也就是说,CLR 还是知道 myArray 到底是什么类型的,并且不许改变。

逆变就是反过来的概念——在一个使用具体类型的地方,可以传入宽泛类型的对象。

二、委托中的可变性

在 C# 1 中,如果我们定义一个委托类型,那么用于它的方法将必须在参数表和返回值方面严格匹配。但是 C# 2 里事情改变了,它支持对参数的逆变性和对返回值的协变性

假定

1
2
class Base {}
class Derived : Base {}

那么下面的代码是合法的

1
2
3
4
5
6
7
8
delegate Base VariantDelegate(Derived d);

public Derived MyFunc(Base b)
{
// ...
}

VariantDelegate d = new VariantDelegate(MyFunc);

在委托的标准中,需要Base类型的返回值,Derived类型的参数,而我们传入的MyFunc是Derived类型的返回值,Base类型的参数,这样是合理的

考虑使用这个委托的地方,会调用委托的实例d(new Derived()),而MyFunc本身需要一个Base类型的参数,不会有问题;委托的实例d(new Derived).(From Base…)应用返回值一直是按Base来处理,所以MyFunc返回Derived也没有任何问题。

这其实就是消费代码必须把传入对象当做是一个更加泛化(宽泛)的对象来处理

三、泛型中的可变性

一直到 C# 3,泛型类型、接口、委托的参数都是不可变的。基于和上述类似的逻辑,C# 4 终于决定在泛型接口和泛型委托中支持类型参数的可变性。如果你想使用可变性,必须在类型参数前用 in 或者 out 修饰符来显式指定。和前面类似,如果一个类型参数仅用作接口方法或者委托中的(普通)参数,那么它可以被指定为逆变的(使用 in 来修饰);如果它只作为返回值,那么它可以被指定为协变的(使用 out 来修饰)。

1
2
delegate void Action<in T>(T t);
delegate TResult Func<out TResult>();

如果理解了上面介绍的关于(非泛型)委托参数和返回值的可变性,那么对这样的泛型委托也可以很容易的理解。但是,如果情况复杂了怎么办呢?考虑下面这个情况:

1
delegate void MyAction<T>(Action<T> action);

这个委托如果要受可变性的恩泽,应该在 T 前面加什么修饰符呢?答案是 out,而书上的解释是模糊的:“作为一个便捷的规则,可以认为内嵌的逆变性反转了之前的可变性。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

delegate void MyAction<out T>(Action<T> action);//不用写out也依然能够通过,写in的话IDE会报错

class Program
{
public MyAction<Object> myAction;

void MyMainMethod
{
myAction += MyActionMethod;
}

void MyActionMethod(Action<string> action){}
}

引用的《C# in depth》上的解释:object 本身是比 string 更泛化的类型,但逆变性使得 Action<string> 成了比 Action<object> 更泛化的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;

delegate void MyAction<in T>(T action);//不用写in也依然能够通过,写out的话IDE会报错

class Program
{
public MyAction<string> myAction;

void MyMainMethod
{
myAction += MyActionMethod;
}

void MyActionMethod(Object action){}
}

可变类型参数的使用还具有以下限制

  • 仅可在泛型接口和泛型委托的声明中使用
  • 仅适用于引用类型,如果使用值类型填充类型参数,则得到的封闭类型是不可变的
  • 可变性不能用于委托链,即不能组合两个封闭类型不完全一致的委托
1
2
3
4
5
6
7
8
9
10
11
12
13
//编译时错误
//无法将类型“System.Func<int>”隐式转换为“System.Func<object>”
Func<object> foo = new Func<int>(()=>3);


Func<Derived> bar = ()=>new Derived();
Func<Base> foo = bar;
Console.WriteLine(foo().ToString()); //Derived

foo = ()=>new Base();
foo += bar;
Console.WriteLine(foo().ToString());
//System.ArgumentException: Delegates must be of the same type

四、内置的可变类型

.NET类库中有很多泛型接口或委托都使用了可变的类型参数,下面列举一些这类接口和委托

具有协变类型参数的泛型接口

1
2
3
4
5
6
7
public interface IEnumerable<out T> : IEnumerable
{
// Returns an IEnumerator for this enumerable Object. The enumerator provides
// a simple way to access all the contents of a collection.
/// <include file='doc\IEnumerable.uex' path='docs/doc[@for="IEnumerable.GetEnumerator"]/*' />
new IEnumerator<T> GetEnumerator();
}

具有逆变类型参数的泛型接口

1
2
3
4
5
6
7
8
public interface IComparer<in T>
{
// Compares two objects. An implementation of this method must return a
// value less than zero if x is less than y, zero if x is equal to y, or a
// value greater than zero if x is greater than y.
//
int Compare(T x, T y);
}

具有协变类型参数的委托类型

1
public delegate TResult Func<out TResult>();

具有逆变类型参数的委托类型

1
public delegate void Action<in T>(T obj); 

同时具有协变类型参数和逆变类型参数的委托类型

1
public delegate TResult Func<in T, out TResult>(T arg);