.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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章