從2.0起咱們一直就在談論泛型,那麼什麼是泛型,泛型有什麼好處,與泛型相關的概念又該怎麼使用,好比泛型方法,泛型委託。這一篇我會全面的介紹泛型。數據庫
那麼首先咱們必須搞清楚什麼是泛型,泛型其實也是一種類型,跟咱們使用的int,string同樣都是.net的類型。泛型其實就是一個模板類型,萬能類型。它容許咱們在設計類的時候使用一個類型空白。預留一個類型。等到咱們使用這個類的時候,咱們可使用特定的類型來替換掉咱們預留的這個類型。這就是泛型。數組
那麼這樣使用的好處有什麼呢?安全
1,類型安全性ide
2,性能提升函數
3,代碼重用性能
4,擴展性測試
爲何會有這幾個好處,咱們來解析一下。this
在咱們討論泛型的優勢的時候,先來看看怎麼使用泛型,泛型通常與集合一塊兒使用。可是咱們也能夠創造本身的泛型類。這裏咱們定義一個類Person。這個類有3個變量,ID,FirstName,LastName.FirstName和LastName的類型很肯定就是string。而ID的類型咱們卻不肯定,這裏的不肯定是爲了更好的擴展性,而不是說不能肯定,好比ID能夠是純int的格式,好比1,2.同時也能夠是string的ET001,ET002.固然咱們能夠經過拼接字符串來完成這個的操做,可是若是咱們使用泛型,就能實現很好的擴展性,性能,安全性。類以下以下。spa
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咱們能夠在之後指定實際類型來替換這個類型。其實有點像委託。只不過委託預留的是一個具備特定簽名的方法抽象。而泛型預留的是一個類型。這就足以說明面向對象其實從某種角度來講就是面向抽象而不是面向具體的實現。.net
這裏咱們定義的泛型類型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是如今的容量