# 原则29:让接口支持协变和逆变
**By D.S.Qiu**
**尊重他人的劳动,支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
类型可变性,具体地,协变和逆变,定义了一个类型变化为另一个类型的两种情况。如果可能,你应该让泛型接口和委托支持泛型的协变和逆变。这样做可以让你的 APIs 能安全地不同方式使用。如果你不能将一个类型替换为另一个,那么就是不可变。
类型可变性是很多开发者遇到的却又不真正理解的很多问题之一。协变和逆变是类型替换的两种不同形式。如果你用声明类型的子类返回那么就是协变的。如果你用声明类型的基类作为参数传入那么就是逆变。面向对象原因普遍支持参数类型的协变。你可以传递子类对象到任何期望是基类参数的方法。例如, Console.WriteLine() 函数有一个使用 System.Object 参数的版本。你可以传入任何 System.Object 的子类对象。如果你重载实例方法返回 System.Object ,你可以返回任何继承自 System.Object 的对象。
普遍的行为让很多开发者认为泛型也遵循这个规则。你可以使用 IEnumerable<MyDerived> 传给参数为 IEnumerable<Object> 的方法。你会期望返回的 IEnumerable<MyDerivedType> 可以赋值给 IEnumerable<Object> 变量。不是这样的。在 C# 4.0之前,所有泛型类型都是不可变的。这意味着,很多次你都自以为泛型也有协变和逆变时,编译器却告诉你的代码是有问题的。数组是被看做协变的。然而,数组不支持安全的协变。随着 C# 4.0 ,新关键字可以让你的泛型支持协变和逆变。这使得泛型更有用,特别是在泛型接口和委托上你应该尽可能使用 in 和 out 参数。
我们开始通过数组理解协变的问题。考虑下面简单的类继承结构:
```
abstract public class CelestialBody
{
public double Mass { get; set; }
public string Name { get; set; }
// elided
}
public class Planet : CelestialBody
{
// elided
}
public class Moon : CelestialBody
{
// elided
}
public class Asteroid : CelestialBody
{
// elided
}
```
下面这个方法把 CelestialBody 对象数组当做协变,而且那样做事安全的:
```
public static void CoVariantArray(CelestialBody[] baseItems)
{
foreach (var thing in baseItems)
Console.WriteLine("{0} has a mass of {1} Kg", thing.Name, thing.Mass);
}
```
下面这个方法也把 CelestialBody 对象数组当做协变,但这是不安排的。赋值语句会抛出异常。
```
public static void UnsafeVariantArray(
CelestialBody[] baseItems)
{
baseItems[0] = new Asteroid
{ Name = "Hygiea", Mass = 8.85e19 };
}
```
如果你将子类赋给基类的数组元素一样会有相同的问题:
```
CelestialBody[] spaceJunk = new Asteroid[5];
spaceJunk[0] = new Planet();
```
把集合看着协变意味着当如果有两个类有继承关系是,你可以认为他们的关系和两个类型的数组是一样的。这不是一个严格的定义,但要记住它是很用的。 Planet 对可以传递给任何期望参数为 CelestialBody 的方法。这是因为 Planet 继承于 CelestialBody 。类似地,你可以将 Planet[] 传递给任何期望参数为 CelestianlBody[] 的方法。但是,正如上面的例子一样,它们总是不能如你期望一样工作。
当泛型被引入时,这个问题被十分严格的处理。泛型总是被当做不可变的。泛型类型不得不正确匹配。然而,在 C# 4.0,你可以将方向接口修饰变为协变或逆变。我们先讨论泛型协变,而后在讨论逆变。
下面这个方法调用参数为 List<Planet> :
```
public static void CoVariantGeneric(
IEnumerable<CelestialBody> baseItems)
{
foreach (var thing in baseItems)
Console.WriteLine("{0} has a mass of {1} Kg", thing.Name, thing.Mass);
}
```
这是因为 IEnumerable<T> 已经被扩展为限制 T 只能出现在输出位置:
```
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
public interface IEnumerator<out T> :IDisposable, IEnumerator
{
T Current { get; }
// MoveNext(), Reset() inherited from IEnumerator
}
```
我给出了 IEnumerable<T> 和 IEnumerator<T> 的定义,因为 IEnumerator<T> 会有比较重要的限制。注意到 IEnumerator<T> 现在的参数类型 T 已经被修饰符 out 修饰。这就强制编译器类型 T 只能在输出位置。输出位置仅限于函数返回值,属性 get 访问器和委托的参数。
因此,使用 IEnumerable<out T> ,编译器知道你会查看序列的每个 T ,但是不会修改序列的内容。这个例子中把 Planet 当做 CelestailBody 就是这样的。
IEnumerable<T> 可以协变是因为 IEnumerator<T> 也是协变的。如果 IEnumerable<T> 返回的接口不是协变的,编译器会产生一个错误。协变类型必须返回值类型的参数或这个接口是协变的。
然而,下面方法替换队列的第一个元素的泛型是不可变的:
```
public static void InvariantGeneric(
IList<CelestialBody> baseItems)
{
baseItems[0] = new Asteroid { Name = "Hygiea", Mass = 8.85e19 };
}
```
因为 IList<T> 的参数 T 既没有被 in 又没有被 out 修饰符,你必须使用正确的类型进行匹配。
当然,你也可以创建逆变泛型接口和委托。用 in 修饰符替换 out 。这个告诉编译器类型参数只能出现在输入位置。.NET 框架已经为 IComparable<T> 加上了 in 修饰符:
```
public interface IComparable<in T>
{
int CompareTo(T other);
}
```
这说明如果 CelestialBody 实现 IComparable<T> ,可以使用很多不同的对象。它可以比较两个 Planet ,一个 Planet 和一个 Moon ,一个 Moon 和一个 Asteroid ,或者其他组合。比较了多个不同的对象,但这是有效的比较。
你会注意到 IEquatable<T> 是不可变的。按照定义, Planet 对象不会和 Moon 对象相等。它们是不同的类型,所以没有意义。如果两个对象是相同类型的如果相等而且不充分的,它是必要的(查看原则6)。
类型参数是可逆变的只有作为方法参数或某些地方的委托参数。
现在,你应该已经注意到我已经用了词组“某些地方的委托参数”两次。委托的定义可以协变也可以逆变。这是相当简单:方法参数逆变( in ),方法的返回值是协变( out )。BCL 更新了包括下面变种的很多委托的定义:
```
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, T2, out TResult>(T1 arg1,T2 arg2);
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
public delegate void Action<in T1, in T2, T3>(T1 arg1, T2 arg2, T3 arg3);
```
在重复一次,这也许不太难。但是,如果你把它们混淆了,事情就得开动你的脑筋了。你已经看到你不能从协变接口返回不可变接口。你使用委托要么限制协变要么限制逆变。
如果你不仔细的话,委托在接口里会向协变和逆变偏移。这里有几个例子:
```
public interface ICovariantDelegates<out T>
{
T GetAnItem();
Func<T> GetAnItemLater();
void GiveAnItemLater(Action<T> whatToDo);
}
public interface IContravariantDelegate<in T>
{
void ActOnAnItem(T item);
void GetAnItemLater(Func<T> item);
Action<T> ActOnAnItemLater();
}
```
接口里的方法的命名展示了它们具体的工作。仔细看 ICovariantDelegate 接口的定义。 GetAnItemLater() 只是检索元素。方法中可以调用 Func<T> 返回检索的元素。 T 仍然出现在输出位置上。这可能是有意义。 GetAnItemLater() 很容易让人困扰。这里,你的委托方法只是接收 T 对象。所以,即使 Action<T> 是协议的,它出现的 ICovarinatDelegate 接口的位置其实是 T 由实现 ICovariantDelegate<T> 的对象返回的。它看起来是逆变的,但是相对于接口来说是协变的。
IContravariantDelegate<T> 和一般的接口一样但是展示如何使用逆变接口。再说一次, ActOnAnItemLater() 方法就很明显。 ActOnAnItemLater() 方法有些复杂。你返回一个接受 T 类型对象的方法。这个最后方法,一次又一次强调,会引起一些困扰。它和其他接口的概念是一样的。 GetAnItemLater() 方法接受一个方法并返回 T 对象。即使 Func<out T> 声明为协变,它的作用是为实现 IContravariantDelegate 对象引入输入。它相对于 IContravariantDelegate 的作用是逆变的。
描述协变和逆变如何正确的工作十分复杂。值得庆幸的是,语法现在支持使用 in (逆变) 和 out (协变)修饰接口。你应该尽可能使用 in 或 out 修饰符修复接口和委托。然后,编译器就会纠正和你定义的有差异的用法。编译器会捕获到接口和委托的定义,并且发现你创建的类型的任何误用。
小结:
这个原则作为第三章的最后一个,虽然介绍的是类型的可变性,有些类似类型转换,但是情况却复杂的多,理解起来难度很大,想要更彻底的理解协变和逆变的概念,可以参考①。
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/2086977](/blog/2086977)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
参考:
①[1-2-3.cnblogs.com](http://www.cnblogs.com/1-2-3/): [http://www.cnblogs.com/1-2-3/archive/2010/09/27/covariance-contravariance-csharp4.html](http://www.cnblogs.com/1-2-3/archive/2010/09/27/covariance-contravariance-csharp4.html)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数