C++泛型用法

來源:互聯網
上載者:User

 我們先來看一個最為常見的泛型型別List<T>的定義

(真正的定義比這個要複雜的多,我這裡刪掉了很多東西)

 
  1. [Serializable]  
  2. public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>  
  3. {  
  4.     public T this[int index] { get; set; }  
  5.     public void Add(T item);  
  6.     public void Clear();  
  7.     public bool Contains(T item);  
  8.     public int IndexOf(T item);  
  9.     public bool Remove(T item);  
  10.     public void Sort();  
  11.     public T[] ToArray();  

List後面緊跟著一個<T>表示它操作的是一個未指定的資料類型(T代表著一個未指定的資料類型)

可以把T看作一個變數名,T代表著一個類型,在List<T>的原始碼中任何地方都能使用T。

T被用作方法的參數和傳回值。

Add方法接收T類型的參數,ToArray方法返回一個T類型的數組

注意:

泛型參數必須以T開頭,要麼就叫T,要麼就叫TKey或者TValue;

這跟介面要以I開頭是一樣的,這是約定。

下面來看一段使用泛型型別的代碼

 
  1. var a = new List<int>();  
  2.             a.Add(1);  
  3.             a.Add(2);  
  4.             //這是錯誤的,因為你已經指定了泛型型別為int,就不能在這個容器中放入其他的值  
  5.             //這是編譯器錯誤,更提升了排錯效率,如果是運行期錯誤,不知道要多麼煩人  
  6.             a.Add("3");  
  7.             var item = a[2]; 

請注意上面代碼裡的注釋

二、泛型的作用(1):

作為程式員,寫代碼時刻不忘代碼重用。

代碼重用可以分成很多類,其中演算法重用就是非常重要的一類,假設你要為一組整型資料寫一個排序演算法,又要為一組浮點型資料寫一個排序演算法,如果沒有泛型型別,你會怎麼做呢?

你可能想到了方法的重載。

寫兩個同名方法,一個方法接收整型數組,另一個方法接收浮點型的數組。

但有了泛型,你就完全不必這麼做,只要設計一個方法就夠用了,你甚至可以用這個方法為一組字串資料排序。

三、泛型的作用(2):

假設你是一個方法的設計者,這個方法需要有一個輸入參數,但你並能確定這個輸入參數的類型,那麼你會怎麼做呢?

有一部分人可能會馬上反駁:“不可能有這種時候!”

那麼我會跟你說,編程是一門經驗型的工作,你的經驗還不夠,還沒有碰到過類似的地方。

另一部分人可能考慮把這個參數的類型設定成Object的,這確實是一種可行的方案,但會造成下面兩個問題,如果我給這個方法傳遞整形的資料(實值型別的資料都一樣),就會產生額外的裝箱、拆箱操作,造成效能損耗。

如果你這個方法裡的處理邏輯不適用於字串的參數,而使用者又傳了一個字串進來,編譯器是不會報錯的,只有在運行期才會報錯。

(如果質管部門沒有測出這個運行期BUG,那麼不知道要造成多大的損失呢)

這就是我們常說的:類型不安全。

四、泛型的樣本:

像List<T>和Dictionary<TKey,TValue>之類的泛型型別我們經常用到,下面我介紹幾個不常用到的泛型型別。

ObservableCollection<T>

當這個集合發生改變後會有相應的事件得到通知。

請看如下代碼:

 
  1. static void Main(string[] args)  
  2. {  
  3.     var a = new ObservableCollection<int>();  
  4.     a.CollectionChanged += a_CollectionChanged;  
  5. }  
  6.  
  7. static void a_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)  
  8. {  
  9.     //可以通過Action來判斷是什麼操作觸發了事件  
  10.     //e.Action == NotifyCollectionChangedAction.Add  
  11.  
  12.     //可以根據以下兩個屬性來得到更改前和更改後的內容  
  13.     //e.NewItems;  
  14.     //e.OldItems;  

使用這個集合需要引用如下兩個名稱空間

 
  1. using System.Collections.ObjectModel;  
  2. using System.Collections.Specialized; 

BlockingCollection<int>是安全執行緒的集合

來看看下面這段代碼

 
  1. var bcollec = new BlockingCollection<int>(2);  
  2. //試圖添加1-50  
  3. Task.Run(() =>  
  4. {  
  5.     //並行迴圈  
  6.     Parallel.For(1, 51, i =>  
  7.     {  
  8.         bcollec.Add(i);  
  9.         Console.WriteLine("加入:" + i);  
  10.     });  
  11. });  
  12.  
  13. Thread.Sleep(1000);  
  14. Console.WriteLine("調用一次Take");  
  15. bcollec.Take();  
  16.  
  17. //等待無限長時間  
  18. Thread.Sleep(Timeout.Infinite); 

輸出結果為:

 
  1. 加入:1  
  2. 加入:37  
  3. 調用一次Take  
  4. 加入:13 

BlockingCollection<int>還可以設定CompleteAdding和IsCompleted屬性來拒絕加入新元素。

.NET類庫還提供了很多的泛型型別,在這裡就不一一例舉了。

五、泛型的繼承:

在.net中一切都繼承字Object,泛型也不例外,泛型型別可以繼承自其他類型。

來看一下如下代碼

 
  1. public class MyType  
  2. {  
  3.     public virtual string getOneStr()  
  4.     {  
  5.         return "base object Str";  
  6.     }  
  7. }  
  8. public class MyOtherType<T> : MyType  
  9. {  
  10.     public override string getOneStr()  
  11.     {  
  12.         return typeof(T).ToString();  
  13.     }  
  14. }  
  15. class Program  
  16. {  
  17.     static void Main(string[] args)  
  18.     {  
  19.         MyType target = new MyOtherType<int>();  
  20.         Console.WriteLine(target.getOneStr());  
  21.         Console.ReadKey();  
  22.     }  

泛型型別MyOtherType<T>成功的重寫了非泛型型別MyType的方法。

如果我試圖按如下方式從MyOtherType<T>類型派生子類型就會導致編譯器錯誤。

 
  1. //編譯期錯誤  
  2. public class MyThirdType : MyOtherType<T>  
  3. {  
  4. }  
  5.  

但是如果寫成這種方式,就不會出錯

 
  1. public class MyThirdType : MyOtherType<int>  
  2.     {  
  3.         public override string getOneStr()  
  4.         {  
  5.             return "MyThirdType";  
  6.         }  
  7.     } 

注意:

如果按照如上寫法,會造成類型不統一的問題,

如果一個方法接收MyThirdType類型的參數,

那麼不能將一個MyOtherType<int>的執行個體傳遞給這個方法, 

然而一個方法如果接收MyOtherType<int>類型的參數,

卻可以把MyThirdType類型的執行個體傳遞給這個方法,

這是CLR內部實現機製造成的,

這看起來確實很怪異!

寫成如下方式也不會出錯:

 
  1. public class MyThirdType<T> : MyOtherType<T>  
  2.     {  
  3.         public override string getOneStr()  
  4.         {  
  5.             return typeof(T).ToString() + " from MyThirdType";  
  6.         }  
  7.     } 

此中訣竅,只可意會,不可言傳。

六、泛型介面

.NET類庫裡有很多泛型的介面,比如:IEnumerator<T>、IList<T>等,這裡不對這些介面做詳細描述了,值說說為什麼要有泛型介面。

其實泛型介面出現的原因和泛型出現的原因類似,拿IComparable這個介面來說,此介面只描述了一個方法:

 
  1. int CompareTo(object obj); 

大家看到,如果是實值型別的參數,勢必會導致裝箱和拆箱操作。

同時,也不是強型別的,不能在編譯期確定參數的類型,有了IComparable<T>就解決掉這個問題了:

 
  1. int CompareTo(T other); 

七、泛型委派

委託描述方法,泛型委派的由來和泛型介面類似。

定義一個泛型委派也比較簡單:

 
  1. public delegate void MyAction<T>(T obj); 

這個委託描述一類方法,這類方法接收T類型的參數,沒有傳回值。

來看看使用這個委託的方法:

 
  1. public delegate void MyAction<T>(T obj);  
  2. static void Main(string[] args)  
  3. {  
  4.     var method = new MyAction<int>(printInt);  
  5.     method(3);  
  6.     Console.ReadKey();  
  7. }  
  8. static void printInt(int i)  
  9. {  
  10.     Console.WriteLine(i);  

由於定義委託比較繁瑣,.NET類庫在System名稱空間,下定義了三種比較常用的泛型委派。

Predicate<T>委託:

 
  1. public delegate bool Predicate<T>(T obj); 

這個委託描述的方法為接收一個T類型的參數,返回一個BOOL類型的值,一般用於比較方法。

Action<T>委託

 
  1. public delegate void Action<T>(T obj); 
 
  1. public delegate void Action<T1, T2>(T1 arg1, T2 arg2); 

這個委託描述的方法,接收一個或多個T類型的參數(最多16個,我這裡唯寫了兩種類型的定義方式),沒有傳回值。

Func<T>委託

 
  1. public delegate TResult Func<TResult>(); 
 
  1. public delegate TResult Func<T, TResult>(T arg); 

這個委託描述的方法,接收零個或多個T類型的參數(最多16個,我這裡唯寫了兩種類型的定義方式),與Action委託不同的是,它有一個傳回值,傳回值的類型為TResult類型的。

關於委託的描述,您還可以看我這篇文章。

八、泛型方法

泛型型別中的T可以用在這個類型的任何地方,然而有些時候,我們不希望在使用類型的時候就指定T的類型,我們希望在使用這個類型的方法時,再指定T的類型。

來看看如下代碼:

 
  1. public class MyClass  
  2.     {  
  3.         public TParam CompareTo<TParam>(TParam other)  
  4.         {  
  5.             Console.WriteLine(other.ToString());  
  6.             return other;  
  7.         }  
  8.     } 

上面的代碼中MyClass並不是一個泛型型別,但這個類型中的CompareTo<TParam>()卻是一個泛型方法,TParam可以用在這個方法中的任何地方。

使用泛型方法一般用如下代碼就可以了:

 
  1. obj.CompareTo<int>(4);  
  2. obj.CompareTo<string>("ddd"); 

然而,你可以寫的更簡單一些,寫成如下的方式:

 
  1. obj.CompareTo(2);  
  2. obj.CompareTo("123"); 

有人會問:“這不可能,沒有指定CompareTo方法的TParam類型,肯定會編譯出錯的”

我告訴你:不會的,編譯器可以幫你完成類型推斷的工作。

注意:

如果你為一個方法指定了兩個泛型參數,而且這兩個參數的類型都是T,那麼如果你想使用類型推斷,你必須傳遞兩個相同類型的參數給這個方法,不能一個參數用string類型,另一個用object類型,這會導致編譯錯誤。

九、泛型約束

我們設計了一個泛型型別,很多時候,我們不希望使用者傳入任意類型的參數,也就是說,我們希望“約束”一下T的類型。

來看看如下代碼:

 
  1. public class MyClass<T> where T : IComparable<T>  
  2.     {  
  3.         public int CompareTo(T other)  
  4.         {  
  5.             return 0;  
  6.         }  
  7.     } 

上面的代碼要求T類型必須實現了IComparable<T>介面。

如你所見:泛型的約束通過關鍵字where來實現。

泛型方法當然也可以通過類似的方式對泛型參數進行約束。

請看如下代碼:

 
  1. public class MyClass  
  2. {  
  3.     public TParam CompareTo<TParam>(TParam other) where TParam:class 
  4.     {  
  5.         Console.WriteLine(other.ToString());  
  6.         return other;  
  7.     }  

上面代碼中用了class關鍵字約束泛型參數TParam;具體稍後解釋。

注意1:

如果我有一個類型也定義為MyClass<T>但沒有做約束,那麼這個時候,做過約束的MyClass<T>將與沒做約束的MyClass<T>衝突,編譯無法通過。

注意2:

當你重寫一個泛型方法時,如果這個方法指定了約束,在重寫這個方法時,不能再指定約束了。

注意3:

雖然我上面的例子寫的是介面約束,但你完全可以寫一個類型,比如說BaseClass。而且,只要是繼承自BaseClass的類型都可以當作T類型使用,你不要試圖約束T為Object類型,編譯不會通過的。(傻子才這麼幹)

注意4:

有兩個特殊的約束:class和struct。

where T : class 約束T類型必須為參考型別

where T : struct 約束T類型必須為實值型別

注意5:

如果你沒有對T進行class約束,

那麼你不能寫這樣的代碼:T obj = null; 這無法通過編譯,因為T有可能是實值型別的。

如果你沒有對T進行struct約束,也沒有對T進行new約束。

那麼你不能寫這樣的代碼:T obj = new T(); 這無法通過編譯,因為實值型別肯定有無參數構造器,而參考型別就不一定了。

如果你對T進行了new約束:where T : new(); 那麼new T()就是正確的,因為new約束要求T類型有一個公用無參構造器。

注意6:

就算沒有對T進行任何約束,也有一個辦法來處理實值型別和參考型別的問題。

T temp = default(T);

如果T為參考型別,那麼temp就是null;如果T為實值型別,那麼temp就是0;

注意7:

試圖對T類型的變數進行強制轉化,一般情況下會報編譯期錯誤。

但你可以先把T轉化成object再把object轉化成你要的類型(一般不推薦這麼做,你應該考慮把T轉化成一個約束相容的類型)。

你也可以考慮用as操作符進行類型轉化,這一般不會報錯,但只能轉化成參考型別。

關於泛型約束的內容,我在這篇文章裡也有提到。

十、逆變和協變

一般情況下,我們使用泛型時,由T標記的泛型型別是不能更改的。

也就是說,如下兩種寫法都是錯誤的:

 
  1. var a = new List<object>();  
  2. List<string> b = a;  
  3. var c = new List<string>();  
  4. List<object> d = c; 

注意:這裡沒有寫強制轉換,即使寫了強制轉換也是錯誤的,編譯就無法通過,然而泛型提供了逆變和協變的特性,有了這兩種特性,這種轉換就成為了可能。

逆變:

泛型型別T可以從基底類型更改為該類的衍生類別型,用in關鍵字標記逆變形式的型別參數,而且這個參數一般作輸入參數。

協變:

泛型型別T可以從衍生類別型更改為它的基底類型,用out關鍵字來標記協變形式的型別參數,而且這個參數一般作為傳回值。

如果我們定義了一個這樣的委託:

 
  1. public delegate TResult MyAction<in T,out TResult>(T obj); 

那麼,就可以讓如下代碼通過編譯(不用強制轉換)

 
  1. var a = new MyAction<object, ArgumentException>(o => new ArgumentException(o.ToString()));  
  2. MyAction<string, Exception> b = a; 

這就是逆變和協變的威力。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.