.NET泛型中的协变与逆变

泛型的可变性:协变性和逆变性

实质上,可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用。

我们已经习惯了普通继承中的可变性:例如,若某方法声明返回类型为Stream,在实现时可以返回一个MemoryStream。泛型可变性的概念与此相同,但要略微复杂一些。可变性应用于泛型接口和泛型委托的类型参数中,这一点必须引起注意。

可变性有两种类型:协变性和逆变性。二者概念基本相同,只是在上下文中转换的方向不同。

我们先从协变性开始,它通常要好理解一些。

  • 协变性:从API返回的值

   协变性用于向调用者返回某项操作的值。例如一个简单的表示工厂模式的泛型接口,它只包含一个方法CreateInstanse,返回适当类型的实例。代码如下:

  

    /// <summary>
    /// 使用out关键字表示协变
    /// </summary>
    /// <typeparam name="T"></typeparam>
    interface IFactory<out T>
    {
        T CreateInstance();
    }

   现在,T在接口中只出现了一次(除了在签名中),它仅作为返回值使用,即方法的输出。这意味着可以将特定类型的工厂视为更一般类型的工厂。如在现实世界里,你可以将比萨工厂视为食品工厂。

  • 逆变性:传入API的值

  逆变性则相反。它指的是调用者向API传入值,即API是在消费值,而不是产生值。我们来想象另一个简单的接口——它可以向控制台打印特定的文档类型。同样,它也只有一个方法Print:

  

    /// <summary>
    /// 使用in关键字表示逆变
    /// </summary>
    /// <typeparam name="T"></typeparam>
    interface IPrettyPrinter<in T>
    {
        void Print(T document);
    }

  这次T只作为参数出现在了接口的输入位置。具体而言,如果我们实现了IPrettyPrinter<SourceCode>,就可以将其当作IPrettyPrinter<CSharpCode>来使用。

不变性:双向传递的值

如果协变性适用于仅从API输出值的情况,而逆变性用于仅向API输入值的情况,那么如果值双向传递会如何呢?简而言之,什么也不会发生。这种类型是不变体(invariant)。下面的接口表示可以对数据类型进行序列化和反序列化的类型:

    /// <summary>
    /// 泛型类型的不变性,既不用 in 关键字限制,也不用 out 关键字限制
    /// </summary>
    /// <typeparam name="T"></typeparam>
    interface IStorage<T>
    {
        byte[] Serialize(T value);

        T Deserialize(byte[] data);
    }

这时,如果存在一个具有特定类型T的IStorage<T>实例,我们不能将其视为该接口更具体或更一般类型的实现。如果以协变的方式使用(如将IStorage<Customer>视为IStorage<Person>),则可能在调用Serialize时传入一个无法处理的对象。

类似地,如果以逆变的方式使用,则可能在反序列化数据时得到一个预料之外的类型。如果有助于理解的话,可以将不变性看成ref参数:按引用传递变量,其类型必须与参数本身的类型完全一致,因为值被传入了方法内部,并且同样被高效地传出。

更多

详见MSDN:https://docs.microsoft.com/zh-cn/dotnet/standard/generics/covariance-and-contravariance

猜你喜欢

转载自www.cnblogs.com/WinHEC/p/Covariance_and_Contravariance_and_Invariance.html