源碼下載
一、裡氏替換原則(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)。