簡體   English   中英

了解 C# 中的協變和逆變接口

[英]Understanding Covariant and Contravariant interfaces in C#

我在一本關於 C# 的教科書中遇到過這些,但我很難理解它們,可能是由於缺乏上下文。

是否有一個關於它們是什么以及它們在那里有用的簡潔解釋?

編輯以澄清:

協變接口:

interface IBibble<out T>
.
.

逆變接口:

interface IBibble<in T>
.
.

使用<out T> ,您可以將接口引用視為層次結構中的向上引用。

使用<in T> ,您可以將接口引用視為層次結構中的一個向下引用。

讓我試着用更多的英語來解釋它。

假設您要從動物園中檢索動物列表,並打算處理它們。 所有動物(在你的動物園里)都有一個名字和一個唯一的 ID。 有些動物是哺乳動物,有些是爬行動物,有些是兩棲動物,有些是魚,等等。但它們都是動物。

因此,通過您的動物列表(其中包含不同類型的動物),您可以說所有動物都有一個名稱,因此顯然獲得所有動物的名稱是安全的。

但是,如果您只有魚的清單,但需要像對待動物一樣對待它們,那行得通嗎? 直覺上,它應該可以工作,但在 C# 3.0 及之前,這段代碼將無法編譯:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

這樣做的原因是編譯器不“知道”您打算或可以在您檢索動物集合后對它做什么。 就它所知,可能有一種方法通過IEnumerable<T>將對象放回列表中,這可能允許您將不是魚的動物放入應該只包含的集合中魚。

換句話說,編譯器不能保證這是不允許的:

animals.Add(new Mammal("Zebra"));

所以編譯器直接拒絕編譯你的代碼。 這就是協方差。

讓我們看看逆變。

既然我們的動物園可以處理所有的動物,它當然可以處理魚,所以讓我們嘗試在我們的動物園中添加一些魚。

在 C# 3.0 及之前,這不會編譯:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));

在這里,編譯器可以允許這段代碼,即使該方法返回List<Animal>只是因為所有的魚都是動物,所以如果我們只是將類型更改為:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));

然后它會起作用,但編譯器無法確定您沒有嘗試這樣做:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];

由於該列表實際上是一個動物列表,這是不允許的。

因此,逆變和協變是您處理對象引用的方式以及您可以用它們做什么。

C# 4.0 中的inout關鍵字專門將接口標記為一個或另一個。 使用in ,您可以將泛型類型(通常是 T)放在輸入位置,這意味着方法參數和只寫屬性。

使用out ,您可以將泛型類型放在output -positions 中,即方法返回值、只讀屬性和 out 方法參數。

這將允許您對代碼執行打算執行的操作:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe

List<T>List<T>有入方向和出方向,因此它既不是協變也不是逆變,而是一個允許您添加對象的接口,如下所示:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}

將允許你這樣做:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe

以下是一些展示這些概念的視頻:

下面是一個例子:

namespace SO2719954
{
    class Base { }
    class Descendant : Base { }

    interface IBibbleOut<out T> { }
    interface IBibbleIn<in T> { }

    class Program
    {
        static void Main(string[] args)
        {
            // We can do this since every Descendant is also a Base
            // and there is no chance we can put Base objects into
            // the returned object, since T is "out"
            // We can not, however, put Base objects into b, since all
            // Base objects might not be Descendant.
            IBibbleOut<Base> b = GetOutDescendant();

            // We can do this since every Descendant is also a Base
            // and we can now put Descendant objects into Base
            // We can not, however, retrieve Descendant objects out
            // of d, since all Base objects might not be Descendant
            IBibbleIn<Descendant> d = GetInBase();
        }

        static IBibbleOut<Descendant> GetOutDescendant()
        {
            return null;
        }

        static IBibbleIn<Base> GetInBase()
        {
            return null;
        }
    }
}

沒有這些標記,以下內容可以編譯:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant

或這個:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants

這篇文章是我讀過的關於這個主題的最好的文章

簡而言之,協方差/逆變/不變性處理自動類型轉換(從基礎到派生,反之亦然)。 只有在對轉換對象執行的讀/寫操作方面遵守某些保證時,才能進行這些類型轉換。 閱讀帖子了解更多詳情。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM