1. 程式人生 > 其它 >關於C#中,泛型協變與逆變的理解

關於C#中,泛型協變與逆變的理解

以下內容僅為個人理解,如有錯誤的地方,還請各位大佬指正!

  一:什麼是協變與逆變,

  二:協變與逆變是如何實現的

  三:為什麼會有泛型協變與逆變的限制

1.首先來說說什麼是協變與逆變

  •   如果有兩個物件A、B,如果每個A型別的值都能轉化為B型別,則認為擁有協變關係,從A到B的轉換成為協變轉換,簡稱協變
  •   與第一條相反,從B到A的轉換成為逆變轉換,簡稱逆變

 * 協變與逆變的在實際開發中比較常見的情況是:假如有class Person{ },class Man{ },Man派生之Person,則一個Man物件轉化為Person物件的行為稱為協變轉換,而Person物件轉化為Man物件成為逆變轉化

  •   泛型的協變與逆變,我們先來看下面程式碼    
  public class Animal
    {    
        public string Name { get; set; }
    }

    public class Bird : Animal
    {
        public void Fly(Bird bird) 
        {
            Console.WriteLine($"這隻鳥的哥哥叫:{bird.Brother.Name}");
        }

        public Bird Brother { get
; set; } } public class Fish : Animal { public void Spit(Fish fish) { Console.WriteLine($"這隻魚的哥哥叫:{fish.Brother.Name}"); } public Fish Brother { get; set; } } public interface IAnimalOperate<T> { T GetAnimal();
void SetBrother(T t); } public class FishOperate : IAnimalOperate<Fish> { public Fish GetAnimal() { Fish f = new Fish() { Name = "Fish Brother" }; return new Fish() { Brother = f }; } public void SetBrother(Fish t) { t.Brother.Name = " Fish new Brother"; } } public class TestClass { public void Test() { IAnimalOperate<Fish> fish = new FishOperate(); IAnimalOperate<Animal> animal = fish; //error 無法將型別IGetAnimal<Fish> 轉化為 IGetAnimal<Animal> } }

這段程式碼很好理解,Fish派生自Animal,但是IAnimalOperate<Fish> 無法轉化為 IAnimalOperate<Animal>,其實無論是從IAnimalOperate<Fish> 轉化為 IAnimalOperate<Animal>(泛型的協變轉化)還是從 IAnimalOperate<Animal> 轉化為IAnimalOperate<Fish>(泛型的逆變轉換) ,目前的寫法都是編譯不通過的,至於原因以及解決方法,我們在後面繼續討論。

2.協變和逆變是如何實現的

  •   協變的實現

    實現泛型的協變轉換,只需要在泛型介面的 IAnimalOperate 申明中加入out關鍵字即可,加入out關鍵字之後,可以實現從泛型 IAnimalOperate<Fish> 到 IAnimalOperate<Animal>的轉換 即

    public interface IAnimalOperate<out T>
    {
        T GetFish();

        // void SetBrother(T t);
    }
  •   逆變的實現

    逆變的實現與協變相反,在泛型介面的IAnimalOperate申明中加入 in 關鍵字即可,加入in關鍵字之後,可以實現從泛型IAnimalOperate<Animal> 到 IAnimalOperate<Fish> 的轉換即  

    public interface IAnimalOperate<in T>
    {
        // T GetFish();

        void SetBrother(T t);
    }

* 泛型的協變逆變實現方式很簡單,下面我們重點討論原理,程式碼中註釋的部分也會在下面做出解釋

3.為什麼會有泛型協變與逆變的限制

  •   通常來說Fish型別派生自Animal型別,所以Fish類的物件可以轉為為Animal物件,但是本例中IAnimalOperate<Fish>並沒有派生自 IAnimalOperate<Animal> ,不具備繼承關係的物件,當然不能做這種隱式轉換,但是這種看似正常的行為又有點奇怪,明明泛型介面的型別具有繼承關係,下面我們重點從另一個方面來討論這個事情
  •   按照上面的例子我們來看一下,如果泛型介面沒有限制,會發生什麼情況   
public class Animal
    {    
        public string Name { get; set; }
    }

    public class Bird : Animal
    {
        public void Fly(Bird bird) 
        {
            Console.WriteLine($"這隻鳥的哥哥叫:{bird.Brother.Name}");
        }

        public Bird Brother { get; set; }
    }

    public class Fish : Animal
    {
        public void Spit(Fish fish) 
        {
            Console.WriteLine($"這隻魚的哥哥叫:{fish.Brother.Name}");
        }
        public Fish Brother { get; set; }
    }

    public interface IAnimalOperate<T>
    {
        T GetFish();

        void SetBrother(T t);
    }

    public class FishOperate : IAnimalOperate<Fish>
    {
        public Fish GetFish()
        {
            Fish f = new Fish() { Name = "Fish Brother" };
            return new Fish() { Brother = f };
        }

        public void SetBrother(Fish t)
        {
            t.Brother.Name = " Fish new Brother";
        }
    }


    public class TestClass
    {
        public void Test()
        {
            IAnimalOperate<Fish> fish = new FishOperate();
            IAnimalOperate<Animal> animal = fish; //假如這一段程式碼成立

            Bird bird = new Bird() { Name = "b1" };
            animal.SetBrother(bird);
        }
    }

我們重點看標紅的兩行程式碼,正常境況下這種程式碼在進行轉化的時候就會被報錯,因為協變會被限制,加入沒有限制的話,我們可以看到,由IAnimalOperate<Fish> 轉化為 IAnimalOperate<Animal> 之後,IAnimalOperate<Animal> 的 SetBrother 方法可以接受一個 Animal 物件了,然後我們把一個 Brid 物件設定給了 fish 的 Brother 屬性,這顯然是不合理的,所以泛型的協變逆變限制也避免了這種情況的發生。


但是,換個角度思考一下,如果我在泛型接口裡面,不對泛型介面的型別進行操作,即不呼叫上述例子中的 SetBrother 方法,是不是就可以避免出現這個問題,所以,C#在這種泛型引數型別只讀的情況下,允許對引數加上out 關鍵字,表示該泛型介面可以發生協變,這也解釋了我上面在實現協變時,註釋了這段程式碼的原因

    public interface IAnimalOperate<T>
    {
        T GetFish();

        void SetBrother(T t);
    }

下面我們來看看逆變,如果沒有限制,逆變會發生什麼情況,逆變與協變相反,類似與把基類的物件轉化為派生類物件,所以我們這裡修改一部分程式碼

public class Animal
    {    
        public string Name { get; set; }
    }

    public class Bird : Animal
    {
        public void Fly(Bird bird) 
        {
            Console.WriteLine($"這隻鳥的哥哥叫:{bird.Brother.Name}");
        }

        public Bird Brother { get; set; }
    }

    public class Fish : Animal
    {
        public void Spit(Fish fish) 
        {
            Console.WriteLine($"這隻魚的哥哥叫:{fish.Brother.Name}");
        }
        public Fish Brother { get; set; }
    }

    public interface IAnimalOperate<T>
    {
        T GetAnimal();

        void SetBrother(T t);
    }

    public class AnimalOperate : IAnimalOperate<Animal>
    {
        public Animal GetAnimal()
        {
            return new Animal() { Name=" Hello " };
        }

        public void SetBrother(Animal t)
        {
            t.Name = " World";
        }
    }


    public class TestClass
    {
        public void Test()
        {
            IAnimalOperate<Animal> animal = new AnimalOperate();
            IAnimalOperate<Fish> fish = animal; //假如這一段程式碼成立

            Fish f1 = fish.GetAnimal();
        }
    }

上述程式碼修改了 IAnimalOperate 的實現,同理,上述程式碼本是編譯不通過的,我們現在假設沒有泛型逆變的顯示,編譯可以通過,那麼此時發生了一種情況 animal 在賦值給 fish 物件之後,fish.GetAnimal() 方法實質上返回的還是一個 Animal 物件,將一個 Animal物件賦值給Fish物件,顯然是不符合我們的變成規範的,泛型的逆變限制就是為了避免這種情況。

但是假如我的泛型介面並不會返回新的資料,就不會出現上述的情況,所以C#的 in 關鍵字,支援對沒有返回泛型型別的介面實現逆變,這也是我上面在實現逆變的時候註釋一段程式碼的原因

    public interface IAnimalOperate<in T>
    {
        // T GetAnimal();

        void SetBrother(T t);
    }

其實仔細研究會發現,out 修飾的泛型介面型別,在發生協變之後,返回泛型型別T的這一過程,其實就是 類似與 Animal a = new Fish() 這一過程,而在 in 修飾的泛型介面中,在發生逆變之後,呼叫 SetBrother( T t ) 方法的時候,實質上還是將一個 子類物件 (Fish或Brid物件)隱式轉化為 Animal 物件,說起來,其本質還是為了遵循里氏替換原則。