簡介: 前兩篇文章講了關於泛型的一些基礎,下面筆者通過這篇文章來給剛剛接觸泛型的朋友介紹一下 <1>.原理性的東西----” 泛型的協變和逆變 “ <2>.以及常用的接口----” IEnumerable 及其泛型版的IEnumerable<out T> “ ------------------------------------------------------------------------------------------------------------------------------------------------------------ <泛型的協變與逆變|泛型修飾符‘out’與‘in’> |首先這2個拗口的名詞先不用去管它,先知道協變和逆變主要是用在泛型的接口和委托上就可以了,下面我們通過一個例子來看看: |在這之前我們插點別的東西,我們知道接口是可以體現多態的,當然接口體現的多態注重的功能上的多態,這和抽象類不同,抽象類更注重的是建立在血緣關系上的多態。 知道接口是可以體現多態的之後,我們來看看一個相關的例子-- 鳥和飛機都會飛,把飛定義成一個借口,在定義2個類 復制代碼 public interface IFlyable { void fly(); } class Bird:IFlyable { public void fly() { Console.WriteLine("鳥兒飛!"); } } class Plane:IFlyable { public void fly() { Console.WriteLine("飛機飛!"); } } 復制代碼 下面看看接口體現的多態性: 復制代碼 IFlyable ifly; ifly = new Bird(); ifly.fly(); ifly = new Plane(); ifly.fly(); 復制代碼 運行結果: 鳥兒飛! 飛機飛! 了解了接口的多態性後我們再來看一個例子: 這裡定義了2個類 Animal 和 Cat (Cat繼承了Animal) 復制代碼 public class Animal { } public class Cat:Animal { } 復制代碼 繼續往下看: Cat cat = new Cat(); 下面這句代碼,cat向animal轉,子類向父類轉換,這時cat會隱式轉換為animal 我們說“兒子像父親” 這是完全可以理解的 Animal animal = cat; 但是 說”父親像兒子“ 這是說不過去的 ,但是有的時候如果兒子坑爹,強制轉換了一下還是可以的 cat = (Cat)animal; (協變) List<Cat> catArray = new List<Cat>(); List<Animal> animalArray = catArray; 如果是上面說的類,這樣寫是可以的,但是這裡是會報錯的 如圖 繼續往下看 這樣寫卻可以 IEnumerable<Cat> lCat = new List<Cat>(); IEnumerable<Animal> lAnimal = lCat; 對 IEnumerable<Cat> 轉到定義 如圖 我們發現這裡多了一個 “out” 關鍵字 概念引入: 1.對於泛型類型參數,out 關鍵字指定該類型參數是協變的。 可以在泛型接口和委托中使用 out 關鍵字。“協變”是指能夠使用與原始指定的派生類型相比,派生程度更大的類型。 --對於 “協變” 筆者是這樣理解的就是”說的通變化“ 就像 “兒子像父親一樣”(假定父親派生程度0那麼兒子的派生程度就是1了,所以父親可以使用派生程度更大的兒子) 協變與多態性類似,因此它看起來非常自然。 (逆變) 我們知道IComparable<T>接口中,T的修飾符是‘in’,下面我們修改一下上面的代碼演示一下 復制代碼 class Cat : Animal, IComparable<Cat> { //僅演示 public int CompareTo(Cat other) { return 1; } } class Animal : IComparable<Animal> { //僅演示 public int CompareTo(Animal other) { return 1; } } 復制代碼 這裡Cat和Animal都實現了IComparable<T>接口,然後我們這樣寫 IComparable<Cat> ICat = new Cat(); IComparable<Animal> IAnimal = new Animal(); ICat = IAnimal; 代碼中ICat(高派生程度)使用 IAnimal(低派生程度) “父親像兒子” 和上面的例子完全相反。 概念引入: 2.對於泛型類型參數,in 關鍵字指定該類型參數是逆變的。 可以在泛型接口和委托中使用 in 關鍵字。“逆變”則是指能夠使用派生程度更小的類型。 --對於 “逆變” 筆者的理解則是 “坑爹兒子” 反過來硬說 “父親像兒子” 這是 “說不過去的” 只是利用了強硬的手段 在了解了上面的內容後,我們來看看“out” 與 “in” 關鍵字的特性 IEnumerable<T>接口的IEnumerator<T> GetEnumerator()方法返回了一個迭代器 ,不難發現T如果用 out 標記,則T代表了輸出,也就說只能作為結果返回。 IComparable<T>接口的CompareTo(T other)方法傳入了一個T類型的Other參數,不難發現T如果用 in 標記,則T代表了輸入,也就是它只能作為參數傳入。 下面我們演示一個例子 將動物會叫這功能,定義成一個泛型借口用 out 修飾 這裡會出現一個錯誤 把第二個帶參數的setSound方法,去掉後編譯可以正常通過 下面我們把 out 改成 in 這裡會出現一個錯誤 把第一個setSound方法,去掉後編譯可以正常通過,或者把第一個方法的返回值,改成其它非T類型,編譯也可通過 這個演示充分說明了:out 修飾 T 則 T只能作為結果輸出而不能作為參數 ; in 修飾 T 則 T只能作為參數而不能作為結果返回; ------------------------------------------------------------------------------------------------------------------------------------------------------------ <IEnumerable接口及其泛型版> 為什麼要用IEnumerable接口? 下面我們通過一個例子看看: 復制代碼 //定義Person類 public class Person { public Person(string _name) { this.name = _name; } public string name; } //定義People類 public class People { private Person[] _people; public People(Person[] pArray) { //實例化數組 用於存Person實例 _people = new Person[pArray.Length]; for (int i = 0; i < pArray.Length; i++) { _people[i] = pArray[i]; } } } 復制代碼 上面的代碼我們定義了一個 Person 類和一個 People 類,顯然 People是用來存放多個Person實例的集合,下面我們嘗試用 Foreeach 遍歷集合的每個元素 輸出: 復制代碼 static void Main(string[] args) { Person[] personArray = new Person[3]{ new Person("Keiling1"), new Person("Keiling2"), new Person("Keiling3"), }; People people = new People(personArray); foreach (Person item in people) { Console.WriteLine(item.name); } } 復制代碼 這裡編譯不能通過,出現了一個錯誤 GetEnumerator:是IEnumerable接口中的一個方法,它返回一個 IEnumerator(迭代器),如下圖 IEnumerator內部規定了,實現一個迭代器的所有基本方法,包括 如下圖 為了在foreach中使用 People的實例, 我們給People實現IEnumerable接口,代碼如下: 復制代碼 public class People:IEnumerable { private Person[] _people; public People(Person[] pArray) { //實例化數組 用於存Person實例 _people = new Person[pArray.Length]; for (int i = 0; i < pArray.Length; i++) { _people[i] = pArray[i]; } } ////IEnumerable和IEnumerator通過IEnumerable的GetEnumerator()方法建立了連接,可以通過IEnumerable的GetEnumerator()得到IEnumerator對象。 IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator)GetEnumerator(); } public PeopleEnum GetEnumerator() { return new PeopleEnum(_people); } } public class PeopleEnum:IEnumerator { public Person[] _people; public PeopleEnum(Person [] pArray) { _people = pArray; } //游標 int position = -1; //是否可以往下 移 public bool MoveNext() { position++; return (position < _people.Length); } //集合的所有元素取完了之後 重置position public void Reset() { position = -1; } //實現 IEnumerator的 Current方法 返回當前所指的Person對象 object IEnumerator.Current { get { return Current; } } //Current是返回Person類實例的只讀方法 public Person Current { get { try { return _people[position]; } catch (IndexOutOfRangeException) { throw new InvalidOperationException(); } } } } 復制代碼 測試運行: 復制代碼 static void Main(string[] args) { Person[] personArray = new Person[3]{ new Person("Keiling1"), new Person("Keiling2"), new Person("Keiling3"), }; People people = new People(personArray); foreach (Person item in people) { Console.WriteLine(item.name); } } 復制代碼 結果: 總結: 1.一個集合要支持foreach方式的遍歷,必須實現IEnumerable接口,描述這類實現了該接口的對象,我們叫它 ‘序列’。 比如 List<T> 支持 foreach 遍歷 是因為它實現了IEnumerable接口和其泛型版,如圖-- 2. IEnumerator對象具體實現了迭代器(通過MoveNext(),Reset(),Current)。 3. 從這兩個接口的用詞選擇上,也可以看出其不同:IEnumerable是一個聲明式的接口,聲明實現該接口的class是“可枚舉(enumerable)”的,但並沒有說明如何實現迭代器, 而IEnumerator是一個實現式的接口,IEnumerator對象就是一個迭代器。 關於IEnumerable<T>我們來了解一下它的代碼: 4.由於IEnumerable<T>繼承了IEnumerable接口,所以要實現IEnumerator<T> ,還需要實現IEnumerator接口,由於和泛型版本的方法同名,所以該方法的實現需要使用顯式接口實現。這裡就不繼續介紹它的具體實現了,和IEnumerator基本一致,這裡就不詳述了,讀者可以自己動手寫一下。 ps 了解IEnumerable和IEnumerable<T>對今後學西理解LINQ是有很大幫助的。