前言
工作一年了,平時也喜歡看看書,逛逛園子;但說到寫博,還真的沒有,說到底,只有一個字:懶!現在想改掉這個“毛病”了,希望多把平時工作學習到的知識和遇到的問題記錄下來,一是可以梳理自己的思路,加深理解;二是可以向更多的朋友學習和分享;三是可以鍛煉自己的寫作水平;可謂百利而無一害!
平時偶爾會遇到一些小問題,很多時候都是查了記住,或者簡單寫寫筆記,當時理解就過了,沒有形成文檔,等過段時間又遇到同樣的問題,又要重新去查去理解,甚是麻煩。希望以後把這些東西寫成文章,盡管可能是很小的問題,也當做筆記記錄。關於c#的一些概念、語法或者規范,就記錄在【c#筆記】。由於不是初學邊學邊記,所以沒有一定的時間和學習順序,只是平時遇到覺得有必要,就記錄下來。
一、遇到問題
工作是基於.net3.5開發,實際過程遇到一個問題。假設我們有一個 Base 類,一個 Derived 類,Derived 繼承了 Base。如下:
class Base { } class Derived : Base { }
當我用IEnumerable<Base> 作為形參,List<Derived> 作為實參時,發現編譯出錯了!原本父類作為形參,傳遞子類是再正常不過的,但在泛型中確編譯不通過。
二、探究問題
通常我們在設計參數和返回值都有一個原則,參數要盡可能“泛”,返回值要盡可能的“細”。泛,指得是用接口或者父類作為參數,這樣可以接收更多的參數類型;細,指的是返回具體類型,這樣可以更好說明方法的作用。
舉個例子:
string[] strs = new string[] { "hello", "word" }; //這樣的缺點是數組就傳遞不了了,還要調用一次 ToList() static void Test_1(List<string> list) { } //正確的做法,應該用IEnumerable<T> static void Test_2(IEnumerable<string> list) { }
可見,參數的“泛”可以提供更大的靈活性。
接著就進入本次的主題:抗變與協變。需要說明的是,抗變與協變是在4.0開始支持的。假設有一個方法需要Derived集合作為參數,那麼基於上面的原則,我們會這樣設計:
static void TestIn(IEnumerable<Base> bases) { }
接著我們向下面這樣調用,在3.5下就會發現編譯不通過,提示無法將 List<Derived>轉換為IEnumerable<Base>。
List<Derived> listIn = new List<Derived>(); TestIn(listIn);
同樣的代碼,我們拿到4.0下,發現編譯通過了。比較 IEnumerable泛型接口,我們發現4.0下的定義為:
public interface IEnumerable<out T> : IEnumerable
發現多了 out 關鍵字,這就是協變。msdn對於類型參數的解釋是:out T 要枚舉的對象的類型。該類型參數是協變的。即可以使用指定的類型或派生程度更高的類型。
我們可以這樣理解協變,參數的類型就是協變的,父類用子類代替,也就是子類當父類使用。
理解協變後,抗變就好理解了。函數的返回值就是抗變的,子類用父類代替,也就是父類當子類使用。在非泛型的情況下,我們可以這樣接收方法的返回值:
object obj = Test_3(); static string Test_3() { return "hello world"; }
當然,我們覺得這樣調用也應該是可以的:
IEnumerable<Base> listOut = TestOut(); static IEnumerable<Derived> TestOut() { return new List<Derived>(); }
在3.5下,這樣同樣會編譯錯誤。4.0下就沒有問題。
三、總結
協變與抗變的概念其實我們經常遇到(參數協變、返回值抗變),而且我們也會習慣的這樣設計。但對於泛型,.net 到了4.0才提供這樣的支持,這為泛型的使用提供了更大的靈活性。
ok,實際我們不怎麼需要去理解概念性的東西,知道原理和理解怎麼使用即可。以上是我的個人理解,如果有朋友想要更深入的理解,可以參見msdn。