從2.0起我們一直就在談論泛型,那么什么是泛型,泛型有什么好處,與泛型相關的概念又該怎么使用,比如泛型方法,泛型委托。這一篇我會全面的介紹泛型。
那么首先我們必須搞清楚什么是泛型,泛型其實也是一種類型,跟我們使用的int,string一樣都是.net的類型。泛型其實就是一個模板類型,萬能類型。它允許我們在設計類的時候使用一個類型空白。預留一個類型。等到我們使用這個類的時候,我們可以使用特定的類型來替換掉我們預留的這個類型。這就是泛型。
那么這樣使用的好處有什么呢?
1,類型安全性
2,性能提高
3,代碼重用
4,擴展性
為什么會有這幾個好處,我們來解析一下。
在我們討論泛型的優點的時候,先來看看怎么使用泛型,泛型一般與集合一起使用。但是我們也可以創造自己的泛型類。這里我們定義一個類Person。這個類有3個變量,ID,FirstName,LastName.FirstName和LastName的類型很確定就是string。而ID的類型我們卻不確定,這里的不確定是為了更好的擴展性,而不是說不能確定,比如ID可以是純int的格式,比如1,2.同時也可以是string的ET001,ET002.當然我們可以通過拼接字符串來完成這個的操作,但是如果我們使用泛型,就能實現很好的擴展性,性能,安全性。類如下如下。
public class Person<T> { private T _t; public T T1 { get { return _t; } set { _t = value; } } private string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; } } private string _lastName; public string LastName { get { return _lastName; } set { _lastName = value; } } public Person() { } public Person(T t1, string firstName, string lastName) { this._t = t1; this._firstName = firstName; this._lastName = lastName; } }
泛型類的定義是很簡單的<T>,這樣就可以定義泛型類,這里我們使用了泛型T,預留了一個類型。泛型所能理解的操作是:1這里是一個類型,2這個設計時未知,3我們可以在以后指定實際類型來替換這個類型。其實有點像委托。只不過委托預留的是一個具有特定簽名的方法抽象。而泛型預留的是一個類型。這就足以說明面向對象其實從某種角度來說就是面向抽象而不是面向具體的實現。
這里我們定義的泛型類型T,就可以在后續使用時使用不同的類型來替換。這里就可以做到我們前面提到的使用int,或者string,或者其他的任何我們想要的類型,甚至是我們自己定義的類型。我們來看看調用代碼。
Person<int> person = new Person<int>(1, "Edrick", "Liu"); Person<string> personString = new Person<string>("ET001", "Edrick", "Liu"); Console.WriteLine("INT:ID:{0},FirstName:{1},LastName:{2}",person.T1,person.FirstName,person.LastName); Console.WriteLine("STRING:ID:{0},FirstName:{1},LastName:{2}",personString.T1,personString.FirstName,personString.LastName);
這里我們不需要拼接字符串,不需要做任何額外的操作就可以實現。
這里我們說明了代碼重用性。
我們可以擴展類型T,在任何時候,如果需求發生了變化,又要以不同的格式來輸出ID。我們甚至可以擴展一個ID類。然后用ID類來替換T。
public class MyID { private string _city; public string City { get { return _city; } set { _city = value; } } private string _school; public string School { get { return _school; } set { _school = value; } } private string _className; public string ClassName { get { return _className; } set { _className = value; } } private string _number; public string Number { get { return _number; } set { _number = value; } } public MyID() { } public MyID(string city, string school, string className, string number) { this._city = city; this._school = school; this._className = className; this._number = number; } }
我們擴展了一個ID類,用這個復雜類型來用作我們的ID。這里我們不需要更改Person類就可以直接擴展ID了。因為T是可以使用任何類型來替換的。
MyID myId =new MyID("WuHan", "SanZhong", "YiBan", "0001"); Person<MyID> personID = new Person<MyID>(myId, "Edrick", "Liu"); Console.WriteLine("ID:{0},FirstName:{1},LastName:{2}",myId.City+"-"+myId.School+"-"+myId.ClassName+"-"+myId.Number,personID.FirstName,personID.LastName);
這里說明了擴展性
當然有人會說,你這里泛型可以做到的,我們用object也同樣可以做到,是的,這里泛型可以做到的,object也同樣可以做到。但是我們來看下一個實例。這里我們使用ArrayList來做這個示例。
ArrayList list = new ArrayList(); list.Add(1); list.Add(2); list.Add(3); IEnumerator ie = list.GetEnumerator(); while (ie.MoveNext()) { int i = (int)ie.Current; Console.WriteLine(i); }
很簡單的一個示例。示例話一個ArrayList,然后添加3個數字。然后枚舉。這里我為什么要使用枚舉而不直接foreach呢,這樣我們更能直接看清楚使用object的時候類型之間的轉換。如果不清楚foreach為什么可以以這樣的代碼替換的,可以參考我的迭代器一文。
我們來看一幅圖。
這就是我們往集合里面添加元素時候的提示,我們可以看到類型是object。如果我們往里面加入int型元素,那么元素自然會被裝箱。那么在我們迭代的時候呢?上面的代碼顯示了有一個強制轉換,就是拆箱了。所以這里進行了一次裝箱和拆箱。裝箱和拆箱是會有性能損失的,園子里也有朋友做過測試。http://archive.cnblogs.com/a/2213803/就做了一個測試,大家可以看看。這里我們需要知道的就是使用集合實際上是發生了裝箱和拆箱。那么還有一個問題也就出來了,既然這里我們可以使用int,當然也可以加入string類型的元素。因為他們都可以成功的轉換為object,因為object是最終父類。所以以下代碼也是可以通過編譯的。
ArrayList list = new ArrayList(); list.Add(1); list.Add(2); list.Add(3); list.Add("e");
這段代碼毫無疑問的可以通過運行的,但是我們在迭代的時候就會出問題了。很明顯(int)e.這個強制轉換是不能成功的。編譯器期間無錯誤而錯誤發生在運行期。這對我們來說是不希望看到的,那么泛型的處理方式呢?
這里我們可以看到,我們使用的是int類型替換的類型T,所以我們在add的時候就只能add替換T的int類型,而不是想非泛型的任何類型都可以add。
所以這里既說明了性能和安全性
這里有一個問題需要注意以下,我們在聲明泛型T的時候,並不是一定類型名是T,T是在一個類型的時候,如果我們需要使用多個泛型來實例化一個類型,那么我們就需要使用說明性的名稱,比如TId,TFirstName之類的。
public class PerosnMoreTypeOfT<TId,TFirstName,TLastName> { private TId _id; public TId Id { get { return _id; } set { _id = value; } } private TFirstName _firstName; public TFirstName FirstName { get { return _firstName; } set { _firstName = value; } } private TLastName _lastName; public TLastName LastName { get { return _lastName; } set { _lastName = value; } } public PerosnMoreTypeOfT() { } public PerosnMoreTypeOfT(TId tId, TFirstName tFirstName, TLastName tLastName) { this._id = tId; this._firstName = tFirstName; this._lastName = tLastName; } }
調用代碼
PerosnMoreTypeOfT<int, string, string> person = new PerosnMoreTypeOfT<int, string, string>(1, "Edrick", "Liu"); Console.WriteLine("ID:{0},FirstName:{1},LastName:{2}",person.Id,person.FirstName,person.LastName);
這是需要注意一下的。
泛型類型的約束
所謂的泛型類型約束,實際上就是約束的類型T。使T必須遵循一定的規則。比如T必須繼承自某個類,或者T必須實現某個接口等等。那么怎么給泛型指定約束?其實也很簡單,只需要where關鍵字。加上約束的條件。
約束條件有以下
where T : struct -類型T必須是值類型
where T : class -類型T必須是引用類型
where T : Ifoo -類型T必須執行接口Ifoo
where T : foo -類型T必須繼承自 foo
where T : new() -類型T必須有一個默認構造函數
where T : U -指定泛型類型T1派生於T2。
下面我會解釋每個約束該怎么用,使用約束不單單可以限制T,而且還可以使T具有類型可用性,上面我們介紹了,我們只有在實例化的時候才替換泛型類型,所以我們除了能把泛型轉換為object外,基本上在定義的時候不能與其他類型做任何交互,如果這里我約束泛型T實現了接口IFoo,那么我們就可以把泛型轉換為IFoo,從而使用Ifoo里定義的方法。這樣就使類型在定義的時候就可以使用,而不需要等到實例化。
而指定T的類型是非常簡單的。
public class Person<T>where T:struct
這時,如果我們使用引用類型替換T就會編譯出錯。我們也可以約束T為引用類型,這里寫一個例子,怎么使約束為接口和基類型,然后使用這些類型。
public interface IPerson { void DisplayPerosnWithOutId(); void DisplayPerosnWithId(); }
定義接口。
public class Person<T>:IPerson where T:MyID { private T _t; public T T1 { get { return _t; } set { _t = value; } } private string _firstName; public string FirstName { get { return _firstName; } set { _firstName = value; } } private string _lastName; public string LastName { get { return _lastName; } set { _lastName = value; } } public Person() { } public Person(T t1, string firstName, string lastName) { this._t = t1; this._firstName = firstName; this._lastName = lastName; } public void DisplayPerosnWithOutId() { Console.WriteLine("FirstName:{0},LastName:{1}",FirstName,LastName); } public void DisplayPerosnWithId() { MyID myId = T1; Console.WriteLine("ID:{0},FirstName:{1},LastName:{2}", myId.City + "-" + myId.School + "-" + myId.ClassName + "-" + myId.Number, FirstName, LastName); } }
這里讓我們的Perosn實現這個接口,然后我們的Perosn里面的泛型T必須是繼承自MyId的。注意,這里約束的是我們的Person的T
public class DisPerson<T>where T:IPerson { private T t { get; set; } public DisPerson() { } public DisPerson(T t1) { this.t = t1; } public void dis() { IPerson p = (IPerson)t; p.DisplayPerosnWithId(); p.DisplayPerosnWithOutId(); } }
這里就是我們的泛型類,這個類的約束是T必須實現IPerson。所以T就可以跟IPerson實現轉換,從而調用IPerosn里調用的方法。
MyID myId = new MyID("WuHan", "SanZhong", "YiBan", "0001"); Person<MyID> perosn = new Person<MyID>(myId,"Edrick","Liu"); DisPerson<Person<MyID>> dis = new DisPerson<Person<MyID>>(perosn); dis.dis();
這里使用到了2種約束,而值類型約束跟引用類型約束是很簡單的,我們只需要where一下。下面來看看U約束。代碼很簡單
public class ClassA { } public class ClassB:ClassA { } public class ClassC<TClassA,TClassB> where TClassB:TClassA { }
這里ClassB必須是繼承是ClassA。
ClassA a = new ClassA(); ClassB b = new ClassB(); ClassC<ClassA, ClassB> c = new ClassC<ClassA, ClassB>();
如果這里我們的ClassB不繼承自ClassA,那么編譯將不能通過。
Default關鍵字
default關鍵字其實不需要解釋太多,這里只解釋一下原理就行了。我們前面提到,泛型只是一個模板類型,就是我們在定義的時候根本就不可能知道用戶在實例化的時候會以何種類型來替換。有可以是值類型,也有可能是引用類型。值類型是不能賦值為null的。所以泛型類型不能賦值為null,但是這里仍然有50%的幾率是引用類型,我們還是需要50%的機會需要泛型T為null。這時就需要default關鍵字。
private T t =default(T);
這里就可以避免我們上面所說的問題,這里會有兩種情況。一種是如果T為值類型,則賦值0,如果T為引用類型則賦值為null。
泛型類的靜態成員
泛型類的靜態成員跟我們平時處理靜態成員有些許不同。一段代碼就可以解釋清楚。
StaticGeneric<int>.x = 5; StaticGeneric<int>.x = 7; StaticGeneric<string>.x = 6; Console.WriteLine(StaticGeneric<int>.x); Console.WriteLine(StaticGeneric<string>.x);
輸出的是7和6.使用不同的類型替換泛型得到的是不同的類實例。
泛型繼承和泛型接口
現在我們來看看泛型的繼承和泛型接口。我們先來看看泛型繼承。
類可以繼承自泛型基類,泛型類也可以繼承自泛型基類。有一個限制,在繼承的時候,必須顯示指定基類的泛型類型。我們來看看示例
public abstract class Base<T>where T:struct { private int _id; public int Id { get { return _id; } set { _id = value; } } public abstract T Add(T x, T y); }
一個抽象類,定義了一個ID屬性,定義了一個抽象方法。下面是繼承類
public class SonClass<T>:Base<int> { private T _t; public T T1 { get { return _t; } set { _t=value;} } public SonClass() { } public SonClass(T t,int id) { this._t = t; base.Id = id; } public override int Add(int x, int y) { return x + y; } public void Prit() { Console.WriteLine(T1); } }
實現了基類里面的抽象方法,本身實現了一個方法,這里的T跟我們的基類的泛型類型沒有任何關系。調用代碼
SonClass<string> son = new SonClass<string>("EdrickLiu",10); Console.WriteLine(son.Id); Console.WriteLine(son.Add(3,5)); son.Prit();
其實跟我們的非泛型繼承沒有多少太大的區別。那么,泛型接口呢?其實也很簡單。我們定義一個接口ICompare<T>接口,這個接口很簡單,按值比較對象。其實跟繼承大同小異
public interface ICompare<T> where T:class { bool CompareTo(T one,T other); }
這個接口很簡單,定義了一個方法,比較兩個對象,然后我們實現這個接口
public class CompareClass:ICompare<MyID> { public bool CompareTo(MyID one, MyID other) { if (one != null && other != null) { if (one.City == other.City && one.School == other.School & one.ClassName == other.ClassName && one.Number == other.Number) { return true; } else { return false; } } else { return false; } } }
這里跟繼承一樣,實現接口的時候需要制定泛型類型。然后我們就可以調用了
MyID id = new MyID("Wuhan", "Shanzhong", "YiBan", "ET001"); MyID myid = new MyID("Wuhan", "Shanzhong", "YiBan", "ET002"); ICompare<MyID> compare = new CompareClass(); Console.WriteLine(compare.CompareTo(id, myid));
其實有了這個方法,我們就可以不需要重載運算符或者重載Equals了。下面我們來看看泛型泛型方法和泛型委托,當初在寫委托的時候我在考慮泛型委托要放在什么地方寫,最后還是放在這里了。
泛型方法&泛型委托
泛型方法其實跟泛型類差不多,方法在定義的時候使用泛型類型定義參數。調用的時候使用實際類型替換。這樣就可以使用不同的類型來調用方法,我們先來看一個簡單的,交換兩個數。可以是int也可以是double,或者別的類型。
public static void Swap<T>(ref T x,ref T y) { T temp; temp = x; x = y; y = temp; }
這里使用int是因為我們要改變x,y的值,x,y都是值類型,所以要調用ref。調用代碼就很簡單了
int x = 8; int y = 9; GenericMethod.Swap<int>(ref x,ref y); Console.WriteLine("X:{0},Y:{1}",x,y);
我們這里也可以省略<int>,寫成GenericMethod.Swap(ref x,ref y);編譯器會自己判斷。這只是一個很簡單的方法,前面我們說過泛型與集合一起使用會很強大,我們來看一個泛型方法與集合一起使用的例子。我們有一個實體類Person,它有3個字段,ID,Name,Salary.我們要實現的功能就是自動計算總的薪水。首先定義實體。
public class SalaryPerson { private int _id; public int Id { get { return _id; } set { _id = value; } } private string _name; public string Name { get { return _name; } set { _name = value; } } private decimal _salary; public decimal Salary { get { return _salary; } set { _salary = value; } } public SalaryPerson() { } public SalaryPerson(int id, string name, decimal salry) { this._id = id; this._name = name; this._salary = salry; } }
然后再我們剛剛的泛型方法類里加入方法
public static decimal AddSalary(IEnumerable e) { decimal total = 0; foreach (SalaryPerson p in e) { total += p.Salary; } return total; }
這里就有一個問題了,我們只能SalaryPerson類型了,那么這里我們就可以引入泛型了,泛型的作用就是在我們不確定類型的時候做一個替換類型,所以我們這里就可以使用泛型了。更改過后的方法是
public static decimal AddSalary<T>(IEnumerable<T> e) where T:SalaryPerson { decimal total = 0; foreach (T t in e) { total += t.Salary; } return total; }
我們在前面說到了,如果要在定義T的時候使用T,就應該使它繼承於或者是某類,或者實現某個接口。但是我們這里還是只能計算SalaryPerson或者派生於SalaryPerson的類,我們能不能計算別的類,計算的邏輯由我們定義呢?當然可以,就是泛型委托。
泛型委托
我想詳細的講講泛型委托,因為我覺得自從3.0之后泛型委托是用得越來越多,泛型委托與lamda也是越用越多,lamda表達式我在委托一文中講到了。委托的概念我也講了,所以這里我不過多的講述什么是委托。委托是可以引用方法的,只要方法簽名符合,比如一個很簡單的方法簽名public int Add(int x,int y).這里我們需要注意兩點。一點是返回類型,一點是參數。如果我們需要定義的只是一個功能,但是功能的實現要到具體的地方才能確定,我們就可以使用委托,但是使用委托我們的方法返回值和參數類型就確定了,我們可以讓委托具有更高等級的抽象,返回值,參數類型都到具體的地方制定。這里的具體地方就是我們要實現的方法。這樣,我們的委托就具有更高級別的抽象。我們設計的類就具有更高級別的可以用性,我們只需要實現方法的細節就可以了。方法的細節怎么實現,可以使用匿名方法,或者lamda表達式。下面我們來看看在具體的代碼中我們該怎么實現。繼續我們上面的那個例子。首先定義一個類GenericInFramerwork
public delegate TResult Action<TInput,TResult>(TInput input,TResult result); public static TResult GetSalary<TInput, TResult>(IEnumerable<TInput> e, Action<TInput, TResult> action) { TResult result =default(TResult); foreach (TInput t in e) { result = action(t,result); } return result; } }
這個類里面定義了一個泛型委托,委托定了兩個參數,一個是返回類型,一個操作類型。這里解釋一下參數為什么要加上返回類型,因為我們不能用一般的算術運算符來操作泛型類型,+=是不允許的,所以這里只能使用result=action(t,result)那么我們就需要一個返回值來保持傳遞我們的 result.調用代碼
List<SalaryPerson> list = new List<SalaryPerson>(); list.Add(new SalaryPerson(1,"Edrick",5000)); list.Add(new SalaryPerson(1,"Meci",3000)); list.Add(new SalaryPerson(1,"Jun",1000)); GenericAndDelegate.Action<SalaryPerson,Decimal> a = new GenericAndDelegate.Action<SalaryPerson,Decimal>(GetS); decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, a); Console.WriteLine(d); Console.Read(); } public static decimal GetS(SalaryPerson p,decimal d) { d += p.Salary; return d; }
首先實例化委托,然后調用我們的泛型方法。這里就是為什么參數要定義返回類型,這里如果我們去掉參數,而在GetS方法里定義一個局部變量,那么結果是我們只能得到最后意項的結果。相比上面的一個例子,這里的薪水的計算邏輯完全就是可變的,我們可以在調用委托的時候變化我們的邏輯,比如所有的加上200然后存進數據庫,比如加上所有*10.我記得之前有人談過一個問題,就是委托的變化過程,我們這里使用的是最原始的委托的實例化,下面我就來概括一下委托的實例化的發展。上面最原始我的我就不介紹了。還有
GenericAndDelegate.Action<SalaryPerson,Decimal> a = GetS;
這里就是直接把方法名稱賦給委托,這是第二階段。我們可以可以不實例化委托,直接把方法名當做參數傳遞給方法。
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, GetS);
再往后呢,我們可以使用匿名方法
GenericAndDelegate.Action<SalaryPerson, Decimal> a = delegate(SalaryPerson p, decimal d1) { d1 += p.Salary; return d1; }; decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, a);
現在呢?我們就可以使用lamda表達式了。
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, (p,dl)=>dl+=p.Salary);
這里的p,dl我們省略了類型,但是編譯器會幫我們推斷,當然,你加上也是沒有問題的
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, (SalaryPerson p,decimal dl)=>dl+=p.Salary);
我們這里也可以加上我們不同的邏輯。這里我們就不單單是只能對SalaryPerson做操作了,還能對別的對象做操作。
decimal d = GenericAndDelegate.GetSalary<SalaryPerson, Decimal>(list, (SalaryPerson p,decimal dl)=>dl+=p.Salary/10);
這里的泛型委托是我們自己的例子,上面也說了,泛型委托在我們的.netframerwork中的應用也很廣泛,我們舉兩個例子,一個是在數組中,一個是在linq中,這里不介紹 Linq,我只是舉例說明。
int[] ints = { 2,4,6,5,8,9,10}; Array.Sort<int>(ints, (i, j) => i.CompareTo(j)); Array.ForEach(ints, i => Console.WriteLine(i*2)); var query = ints.Where(i => i > 2); foreach (int i in query) { Console.WriteLine(i); } Console.Read();
我們只需要找到對應的委托,然后編寫lamda就可以了。
泛型類型的實例化規律
這一節主要是要我們了解一下泛型在實例化時候的規律。我們可以用值類型或者引用類型實例化范圍,用值類型和引用類型有什么區別呢?我們使用值類型實例化泛型,每次實例化都會創造不同的實例,但是如果實例化的類型不同(都是值類型),那么就會創造不同的版本,不同的實例,引用類型則不同,引用類型會一直使用第一次實例化泛型時候的版本。因為值類型需要復制數據,數據的大小是不同的,所以有不同的版本,而引用類型只需要傳遞引用,所以可以使用同一個版本。
List<int>和List<int>會是同個版本不同實例,但是他們共享list<int>的單個實例。
List<int>和List<long>會創造不同的版本,不同的實例。這就是我們上面說的靜態值不同的原因。
List<string>和List<object>或創造同一版本但是實例不同。
這些我們理解就行了。
還有一點需要注意,泛型類型在添加整個集合的時候不支持隱式轉換,比如
List<object> o = new List<object>(); List<int> iss = new List<int>(); iss.Add(1); o.AddRange(iss);
這里我們需要顯示轉換一下,我們可以寫一個方法。
public void Convert<TInput,TOut>(IList<TInput> input,IList<TOut> outo) where TInput:TOut { foreach (TInput t in input) { outo.Add(t); } }
泛型這里也介紹得差不多了,.netframerwork中有些泛型,比如Nullable<T>我在可空類型介紹了,事件泛型,我會在事件中介紹。泛型跟反射我會在反射中介紹,泛型跟屬性,我會在屬性中介紹。最后就是一個類型了,介紹一下。ArraySegment<T>.表示數組段。直接看代碼吧
int[] ints = { 2,3,4,5,6,7,8,9}; ArraySegment<int> arr = new ArraySegment<int>(ints, 2, 3); for (int i = arr.Offset; i < arr.Count+arr.Offset; i++) { Console.WriteLine(arr.Array[i]); }
Offset是相對原數組的索引,而count是現在的容量