1. 协变性:从API返回的值
协变性用于向调用者返回某项操作的值。例如一个简单的表示工厂模式的泛型接口,它只包含一个方法 CreateInstanse ,返回适当类型的实例。代码如下
1 public interface IFactory<T> 2 { 3 T CreateInstance(); 4 }
现在, T 在接口中只出现了一次(除了在签名中),它仅作为返回值使用,即方法的输出。
这意味着可以将特定类型的工厂视为更一般类型的工厂。如在现实世界里,你可以将比萨工厂视为食品工厂。
2. 逆变性:传入API的值
逆变性则相反。它指的是调用者向API传入值,即API是在消费值,而不是产生值。
我们来想象另一个简单的接口——它可以向控制台打印特定的文档类型。
同样,它也只有一个方法Print :
1 public interface IPrettyPrinter<T> 2 { 3 void Print(T document); 4 }
这次 T 只作为参数出现在了接口的输入位置。
具体而言,如果我们实现了 IPrettyPrinter<SourceCode> ,就可以将其当作 IPrettyPrinter<CSharpCode> 来使用。
3. 不变性:双向传递的值
如果协变性适用于仅从API输出值的情况,而逆变性用于仅向API输入值的情况,那么如果值双向传递会如何呢?
简而言之,什么也不会发生。这种类型是不变体(invariant)。
下面的接口表示可以对数据类型进行序列化和反序列化的类型:
1 public interface IStorage<T> 2 { 3 byte[] Serialize(T value); 4 T Deserialize(byte[] data); 5 }
这时,如果存在一个具有特定类型 T 的 IStorage<T> 实例,我们不能将其视为该接口更具体或更一般类型的实现。
如果以协变的方式使用(如将 IStorage<Customer> 视为 IStorage-<Person> ),则可能在调用 Serialize 时传入一个无法处理的对象。
类似地,如果以逆变的方式使用,则可能在反序列化数据时得到一个预料之外的类型。
如果有助于理解的话,可以将不变性看成 ref 参数:按引用传递变量,其类型必须与参数本身的类型完全一致,因为值被传入了方法内部,并且同样被高效地传出。