不變性、協變性和逆變性(Invariance, Covariance & Contravariance)


源碼下載

一、里氏替換原則(Liskov Substitution Principle LSP)

  我們要講的不是協變性和逆變性(Covariance & Contravariance)嗎?是的,沒錯。但先不要着急,在這之前,我們有必要再回味一下LSP。廢話不多說,直接上代碼:

 1 namespace LSP
 2 {
 3     public class Bird
 4     {
 5         public virtual void Show()
 6         {
 7             Console.WriteLine("It's me, bird.");
 8         }
 9     }
10 }
Bird
 1 namespace LSP
 2 {
 3     public class Swan : Bird
 4     {
 5         public override void Show()
 6         {
 7             Console.WriteLine("It's me, swan.");
 8         }
 9     }
10 }
Swan
 1 namespace LSP
 2 {
 3     public class Program
 4     {
 5         static void Main(string[] args)
 6         {
 7             Bird bird = new Swan();
 8             bird.Show();
 9             Console.ReadLine();
10         }
11     }
12 }
Program

根據里氏替換原則,任何基類可以出現的地方,子類一定可以出現。

因為Swan類繼承於Bird類,所以“Bird bird=new Bird();”中,我需要創建一個Bird對象,你給了我一個Swan對象是完全可行的。通俗地講,我要你提供鳥類動物給我,你給我一只天鵝,當然沒有問題。

然而,我們在調用bird的Show方法時,發生了什么呢?

Bird類和Swan類中都有Show方法,調用這個方法時,編譯器是知道這個bird實際指向的Swan對象的。它會先查看Swan本身是不是有同簽名的方法,如果有就直接調用。如果沒有再往Swan的父類里查看,如果再沒有,再往上面找,直到找到為止。如果最終也沒有找到,就會報錯。

所以,我們看到程序調用的是Swan的Show方法:"It's me, swan."

 

二、協變和逆變是什么?

關於這個,我們還是先看看官方的解釋:

協變和逆變都是術語,前者指能夠使用比原始指定的派生類型的派生程度更大(更具體的)的類型,后者指能夠使用比原始指定的派生類型的派生程度更小(不太具體的)的類型。

看了是不是有種“懂的依然懂,不懂的依然不懂的感覺”?

簡單地說,

協變:你可以用一個子類對象去替換相應的一個父類對象,這是完全符合里氏替換原則的,和協(諧)的變。如:用Swan替換Bird。

逆變:你可以用一個父類對象去替換相應的一個父類對象,這貌似不符合里氏替原則的,不和協(諧)的逆變。如:用Bird替換Swan。

那么事實真的如此嗎?協變是不是比逆變更合理?其實他們完全就是一回事,都是里氏替換原則的一種表現形式罷了。

 

三、不變性(Invariance)

我們知道:Bird bird=new Swan();是沒有問題的。

那么對於泛型,List<Bird> birds=List<Swan>();是不是也OK呢?

No!

首先,因為.Net Framework只向泛型接口和委托提供了協變和逆變的便利。

再者,想要實現協變或逆變,也得在語法上注明out(協變)或in(逆變)。

對於這類不支持協變和逆變的情況,我們稱為不變性(Invariance)。為了維持泛型的同質性(Homogeneity),編譯器禁止將List<Swan>隱式或顯式地轉換為List<Bird>。

好了,重點來了!

為什么要這樣?這樣,很不方便。而且,看起來也不符合里氏替換原則。

簡單地說,維持同質性,不允許這樣的轉換,還是為了編譯正常。什么是編譯正常,就是別給咱報錯。

 1 public class Program
 2     {
 3         public static void Main(string[] args)
 4         {
 5             List<object> obj = null;
 6             List<string> str = null;
 7 
 8             /* Error:
 9              * Cannot implicitly convert type
10              * 'System.Collections.Generic.List<string>' 
11              * to 'System.Collections.Generic.List<object>'
12             */
13 
14             //obj = str;
15 
16             Console.ReadLine();
17         }
18     }
VarianceList

如代碼注解的那樣,“obj=str;”編譯器會報錯:

Error :Cannot implicitly convert type 'System.Collections.Generic.List<string>' to 'System.Collections.Generic.List<object>'

List<T>是微軟提供給我們的,里面封閉太多東西,不方便分析,我們就自己動手來寫一個泛型類Invariance<T>。

 1 namespace Invariance
 2 {
 3     public class Invariance<T>
 4     {
 5         T Test(T t)
 6         {
 7             return default(T);
 8         }
 9     }
10 }
Variance<T>

寫好了泛型類,我們再來試一試。

 1 namespace Invariance
 2 {
 3     public class Program
 4     {
 5         public static void Main(string[] args)
 6         {
 7            Invariance<object> invarianceObj = new Invariance<object>();
 8             Invariance<string> invaricaceStr = new Invariance<string>();
 9 
10             //invarianceObj = invaricaceStr;
11             //invaricaceStr = invarianceObj;
12            
13             Console.ReadLine();
14         }
15     }
16 }
Variance<T> Test

"invarianceObj = invaricaceStr;"報錯:

Error : Cannot implicitly convert type 'Invariance.Invariance<string>' to 'Invariance.Invariance<object>' 

“invaricaceStr = invarianceObj;”報錯:

Error : Cannot implicitly convert type 'Invariance.Invariance<object>' to 'Invariance.Invariance<string>' 

講到這么多報錯,還是沒講到核心,為什么要報錯。

我們可以假設,如果不報錯,運行起來會是怎樣:Invariance<T>類型參數T是在使用時,確定具體類型的。

先來說貌似符合里氏替換原則的情況,

Invariance<object> invarianceObj =new Invariance<string>();

用string替換object沒有問題。但這個語句表達的不僅僅是用string來替換object,也表示用object來替換string。

關鍵在於類型參數,是在泛型類中使用的,我們不敢保證他是否於參數還是返回值。

如:Invariance<object> invarianceObj調用Test(object obj),傳入的是自身的類型參數,而實際執行時,是執行實際指向的對象Invariance<string> invarianceStr的Test(string str)方法。很明顯,Invariance<string> invariance的Test(string str)方法需要接收一個string類型的參數,得到卻是一個object。這是不合法的。

那是不是反過來就可以了呢?

Invariance<string> invaricaceStr=new Invariance<object>();

這樣,你實際執行方法時,需要一個object類型的參數,我給你一個string總沒問題了吧。

OK,這樣完全沒有問題。

然而,不要忘了,方法可能不只是有參數,還可能有返回值。

參數:Invariance<string> invaricaceStr調用Test(string str),將string傳給invarianceObj的Test(object obj)方法。目前為止,OK。

返回值:Invariance<string> invaricaceStr要求Test(string str)返回一個string對象。而實際執行方法的invarianceObj卻只能保證返回一個object對象。NG!

看到了吧。這就是為什么.Net Framework要保持類型參數的同質性,而不允許T類型參數,哪怕從子類到父類或父類到子類的任何一種轉換。

因為你只能保證參數或返回值,其中一項轉換成功。

 

四、協變性(Covariance)

理解了為什么要堅持不變性,理解起協變性就容易多了。如果我能在泛型接口或者委托中保證,我的類型參數,只能外部取出,不允許外部傳入。那么就不存在上面講的將類型參數作為參數傳入方法的情況了。

怎么保證?只需要在類型參數前加out關鍵字就可以了。

1 namespace Covariance
2 {
3     public interface ITest<out T>
4     {
5         T Test();
6     }
7 }
ITest<out T>
 1 namespace Covariance
 2 {
 3     public class Program
 4     {
 5         public static void Main(string[] args)
 6         {
 7             ITest<object> obj = null;
 8             ITest<string> str = null;
 9             obj = str;
10 
11             IEnumerable<object> enuObj = null;
12             IEnumerable<string> enuStr = null;
13             enuObj = enuStr;
14         }
15     }
16 }
Covariance

注:interface IEnumerable<out T>是微軟提供的支持協變的泛型接口之一。

 

五、逆變性(Contravariance)

與逆變性類似,如果我能在泛型接口或者委托中保證,我的類型參數,只能作為參數從外部傳入,不允許將其取出。那么就不存在將類型參數作為返回值返回的情況了。

同樣,我們只需要在類型參數前加in關鍵字就可以了。

1 namespace Contravariance
2 {
3     public interface ITest<in T>
4     {
5         void Test(T t);
6     }
7 }
ITest<in T>
 1 namespace Contravariance
 2 {
 3     public class Program
 4     {
 5         public static void Main(string[] args)
 6         {
 7             ITest<object> obj = null;
 8             ITest<string> str = null;
 9             str = obj;
10 
11             IComparable<object> comObj = null;
12             IComparable<string> comStr = null;
13             comStr = comObj;
14         }
15     }
16 }
Contravariance

注:interface IComparable<in T>是微軟提供的支持逆變的泛型接口之一。

 

后記:常常只是在博客園看大神們的文章,自己總是不敢出聲,第一次在這里寫東西,有理解錯誤的地方,懇請批評指正(QQ:582043340)。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM