(一)泛型概述 泛型不僅是C#編程語言的一部分,而且與程式集中的IL代碼緊密地集成。泛型不僅是C#語言的一種結構,而且是CLR定義的。有了泛型就可以創建獨立於被包含類型的類和方法了。 1、性能 泛型的一個主要優點就是性能。對值類型使用非泛型集合類,在把值類型轉化為引用類型,和把引用類型轉換為值類型時 ...
(一)泛型概述
泛型不僅是C#編程語言的一部分,而且與程式集中的IL代碼緊密地集成。泛型不僅是C#語言的一種結構,而且是CLR定義的。有了泛型就可以創建獨立於被包含類型的類和方法了。
1、性能
泛型的一個主要優點就是性能。對值類型使用非泛型集合類,在把值類型轉化為引用類型,和把引用類型轉換為值類型時,需要進行裝箱和拆箱操作。
下麵的例子顯示了System.Collections名稱空間中的ArrayList類。ArrayList存儲對象,Add()方法定義為需要把對象作為參數,所以要裝箱一個整數類型。在讀取ArrayList中的值時,要進行拆箱操作,把對象轉化為整數類型。可以使用類型裝置轉換運算符把ArrayList集合的第一個元素賦予變數i1,在訪問int類型的變數i2的foreach語句中,也要使用類型強制轉換運算符:
var list = new ArrayList(); list.Add(44);//此處會裝箱 int i1 = (int)list[0];//此處會拆箱 foreach (int i2 in list) { Console.WriteLine(i2);//此處會拆箱 }
System.Collections.Generic名稱空間中的List<T>類不使用對象,而是在使用時定義類型。裝箱和拆箱操作很容易,但性能損失比較大,遍歷許多項尤其如此。
下麵的例子。List<T>類的泛型類型定義為int,所以int類型在JIT編譯器動態生成的類中使用,不再進行裝箱和拆箱操作:
var list = new List<int>(); list.Add(44); int i1 = (int)list[0]; foreach (int i2 in list) { Console.WriteLine(i2); }
2、類型安全
泛型的另一個特性是類型安全。
在泛型類List<T>中,泛型類型T定義了允許使用的類型。有了List<int>的定義,就只能把整數類型添加到集合中。編譯器不會編譯這段代碼,因為Add()方法無效,這樣類型就安全了:
var list = new List<int>(); list.Add(44); list.Add("str");
這個時候編譯器會報錯:
3、二進位代碼的重用
泛型允許更好地重用二進位代碼。泛型類可以定義一次,並且可以用許多不同的類型實例化。
例如,System.Collections.Generic名稱空間中的List<T>類用一個int、一個字元串和一個MyClass類實例化:
var intList = new List<int>(); intList.Add(1); var stringList = new List<string>(); stringList.Add("str"); var myClassList = new List<MyClass>(); myClassList.Add(new MyClass());
4、代碼的擴展
在不同的特定類型實例化泛型時,會創建多少代碼?因為泛型類的定義會放在程式集中,所以用特定類型實例化泛型類不會再IL代碼中賦值這些類。但是,在JIT編譯器把泛型類編譯為本地代碼時,會給每個值類型創建一個新類。引用類型共用同一個本地類的所有相同的實現代碼。這是因為引用類型在實例化的泛型類中只需要4個位元組的記憶體地址(32位系統),就可以引用一個引用類型。值類型包含在實例化的泛型類的記憶體中,同時因為每個值類型對記憶體的要求都不同,所以要為每個值類型實例化一個新類。
5、命名的約定
在程式中使用泛型,在區分泛型類型和非泛型類型時就會有一定的幫助。下麵是泛型類型的命名規則:
l 泛型類型的名稱用字母T作為首碼。
l 如果沒有特殊的要求,泛型類型允許使用任意類替代,且只使用了一個泛型類型,就可以用字元T作為泛型類型的名稱。
public class List<T> { } public class LinkedList<T> { }
如果泛型類型有特定的要求(例如,它必須實現一個介面或派生自基類),或者使用了兩個或多個泛型類型,就應給泛型類型使用描述性的名稱:
public class SortedList<TKey, TValue> { }
(二)創建泛型類
泛型提供了一種新的創建類型的機制,使用泛型創建的類型將帶有類型形參。每個處理對象類型的類都可以有泛型實現方式。另外如果類使用了層次結構就非常有助於消除類型強制轉換操作。
public class LinkedListNode<T> { public LinkedListNode(T value) { this.Value = value; } public T Value { get; private set; } public LinkedListNode<T> Next { get; internal set; } public LinkedListNode<T> Prev { get; internal set; } }
(三)泛型類的功能
1、預設值
通過default關鍵字,將null賦予引用類型,將0賦予值類型。
public T GetDocument() { T doc = default(T); return DealDocument(doc); }
default關鍵字根據上下文可以有多種含義。switch語句使用default定義預設情況。在泛型中,根據泛型類型是引用類型還是值類型,泛型default用於將泛型類型初始化為null或0。
2、約束
在定義泛型類時,可以對客戶端代碼能夠在實例化類時用於類型參數的類型種類施加限制。如果客戶端代碼嘗試使用某個約束所不允許的類型來實例化類,則會產生編譯時錯誤。這些限制稱為約束。約束是使用 where 上下文關鍵字指定的。下表列出了六種類型的約束:
約束 |
說明 |
where T:struct |
類型參數必須是值類型。可以指定除 Nullable 以外的任何值類型。 |
where T:Class |
類型參數必須是引用類型,包括任何類、介面、委托或數組類型。 |
where T:new() |
類型參數必須具有無參數的公共構造函數。當與其他約束一起使用時,new() 約束必須最後指定。 |
where T:<基類名> |
類型參數必須是指定的基類或派生自指定的基類。 |
where T:<介面名稱> |
類型參數必須是指定的介面或實現指定的介面。可以指定多個介面約束。約束介面也可以是泛型的。 |
where T1:T2 |
為 T1 提供的類型參數必須是為 T2 提供的參數或派生自為 T2 提供的參數。這稱為裸類型約束。 |
使用約束的原因
如果要檢查泛型列表中的某個項以確定它是否有效,或者將它與其他某個項進行比較,則編譯器必須在一定程度上保證它需要調用的運算符或方法將受到客戶端代碼可能指定的任何類型參數的支持。這種保證是通過對泛型類定義應用一個或多個約束獲得的。例如,基類約束告訴編譯器:僅此類型的對象或從此類型派生的對象才可用作類型參數。一旦編譯器有了這個保證,它就能夠允許在泛型類中調用該類型的方法。約束是使用上下文關鍵字 where 應用的。
使用泛型類型還可以合併多個約束:
public class MyClass<T> where T : IClass, new() { }
3、繼承
泛型類型可以實現泛型介面,也可以派生自一個類(要求是必須重覆介面或基類的泛型類型):
public class LinkedList<T> : IEnumerable<T> { } public class MyClass<T> : MyBaseClass<T> { }
派生類也可以是泛型或非泛型的,其要求是必須制定基類的類型
public class IntClass : MyBaseClass<int> { }
4、靜態成員
泛型類的靜態成員只能在類的一個實例中共用:
public class StaticDemo<T> { public static int x; } static void Main(string[] args) { StaticDemo<string>.x = 1; StaticDemo<int>.x = 2; Console.WriteLine(StaticDemo<string>.x); Console.WriteLine(StaticDemo<int>.x); Console.ReadKey(); }
運行以上代碼,結果如下:
當T類型不同時靜態成員不共用。
(四)泛型介面
使用泛型可以定義介面,在泛型介面中定義的方法可以帶泛型參數。
public interface IDeal<T> { T Deal(T value); }
協變和抗變指對參數和返回值類型進行轉換。在.NET中參數類型時協變的,返回類型是抗變的。
1、協變和抗變
例子:
參數類型的協變
static void Main(string[] args) { string str = "測試參數的協變"; Show(str); Console.ReadKey(); } public static void Show(object value) { Console.WriteLine(value); }
返回類型的抗變
例子:
static void Main(string[] args) { int value = 1; string str = ConvertToString(value); Console.ReadKey(); } public static string ConvertToString(object value) { return value.ToString(); }
2、泛型介面的協變
如果泛型類型用out關鍵字進行標註,泛型介面就是協變的。(子到父是協變)
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IFactory<Chinese> ChineseFactory = new Factory<Chinese>(); 6 IFactory<People> PeopleFactory = ChineseFactory; //協變 7 People People = PeopleFactory.Create(); 8 Console.ReadKey(); 9 } 10 } 11 public class Chinese : People { } 12 public class People { } 13 public class Factory<T> : IFactory<T> 14 { 15 public T Create() 16 { 17 return (T)Activator.CreateInstance<T>(); 18 } 19 } 20 public interface IFactory<out T> 21 { 22 T Create(); 23 }
3、泛型介面的抗變
如果泛型類型用in關鍵字標註,泛型介面就是抗變的。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IShow<People> ps = new PeopleShow(); 6 IShow<Chinese> cs = ps;//抗變 7 Console.ReadKey(); 8 } 9 } 10 public interface IShow<in T> 11 { 12 void Write(T t); 13 } 14 public class Chinese : People { } 15 public class People 16 { 17 public string Name { get; set; } 18 } 19 public class PeopleShow : IShow<People> 20 { 21 public void Write(People t) 22 { 23 Console.WriteLine("我的名字:" + t.Name + ",現在我表演寫字!"); 24 } 25 }
(五)泛型結構
與類相似,結構也可以是泛型的。它們非常類似於泛型類,只是沒有繼承特性。.NET Framework中的一個泛型結構是Nullable<T>,結構Nullable<T>定義了一個約束:其中泛型類型T必須是一個結構。
因為可空類型使用得非常頻繁,所以C#有一種特殊的語法,它用於定義可空類型的變數。定義這類變數時,不使用泛型結構的語法,而是用“?”運算符。
int? x;
可以使用合併運算符從可空類型轉換為非可空類型。合併運算符“??”
int? x = null; int y = x ?? 0;
y的值顯示為0,因為x是null。
(六)泛型方法
除了定義泛型類之外,還可以定義泛型方法。在泛型方法中,泛型類型用方法聲明來定義。泛型方法可以在非泛型類中定義。
public T ReviseName<T>(T person) where T : People { person.Name = "改名後的:" + person.Name; return person; }
C#編譯器會通過泛型方法來獲取參數類型,所以不需要把泛型類型賦予方法的調用。
Chinese chinese = new Chinese(); chinese.Name = "張三"; Chinese.ReviseName(chinese);
泛型方法可以像泛型方法那樣調用。
1、泛型方法示例
1 class Program 2 3 { 4 5 static void Main(string[] args) 6 7 { 8 9 var accounts = new List<Account>() 10 11 { 12 13 new Account("張三",2000), 14 15 new Account("李四",1300), 16 17 new Account("王麻子",800), 18 19 new Account("趙六",1000) 20 21 }; 22 23 decimal total = AccumulateSimple(accounts); 24 25 Console.ReadKey(); 26 27 } 28 29 public static decimal AccumulateSimple(IEnumerable<Account> source) 30 31 { 32 33 decimal sum = 0; 34 35 foreach (var item in source) 36 37 { 38 39 sum += item.Balance; 40 41 } 42 43 return sum; 44 45 } 46 47 } 48 49 public class Account 50 51 { 52 53 public string Name { get; set; } 54 55 public decimal Balance { get; set; } 56 57 public Account(string name, Decimal balance) 58 59 { 60 61 this.Name = name; 62 63 this.Balance = balance; 64 65 } 66 67 }
2、帶約束的泛型方法
public T ReviseName<T>(T person) where T : People { person.Name = "改名後的:" + person.Name; return person; }
因為方法需要使用T參數的Name屬性,所以為了確保程式不拋出異常,對T參數進行約束,使其必須繼承自People類。
3、帶委托的泛型方法
1 static void Main(string[] args) 2 { 3 int t1 = 1; 4 int t2 = 9; 5 Console.WriteLine(Cal(t1, t2, (i1, i2) => 6 { 7 return i1 + i2; 8 })); 9 Console.ReadKey(); 10 } 11 12 public static T Cal<T>(T t1, T t2, Func<T, T, T> calMethod) 13 { 14 return calMethod(t1, t2); 15 }
定義Cal方法,參數t1,t2是calMethod方法的參數(Func是內置的委托)。
4、泛型方法規範
泛型方法可以重載,為特定的類型定義規範。在編譯期間會使用最佳匹配。
1 static void Main(string[] args) 2 { 3 string str = "str"; 4 int i = 0; 5 First(str); 6 First(i); 7 Console.ReadKey(); 8 } 9 10 public static void First<T>(T obj) 11 { 12 Console.WriteLine("obj:"+obj.GetType().Name); 13 } 14 15 public static void First(int obj) 16 { 17 Console.WriteLine("int"); 18 }
運行以上代碼,結果如下:
需要註意的是,所調用的方法是在編譯期間定義的,而不是運行期間。
static void Main(string[] args) { int i = 0; Second(i); Console.ReadKey(); } public static void Second<T>(T obj) { First(obj); }
運行以上代碼,結果如下:
(七)小結
本章介紹了CLR中一個非常重要的特性:泛型。通過泛型類可以創建獨立於類型的類,泛型方法是獨立於類型的方法。介面、結構和委托也可以用泛型的方式創建。泛型引入了一種新的編程方式。我們介紹瞭如何實現相應的演算法(尤其是操作和謂詞)以用於不同的類,而且它們都是類型安全的。泛型委托可以去除集合中的演算法。