關於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 物件,說起來,其本質還是為了遵循里氏替換原則。