到如今爲止,全部在類聲明中用到的類型都是特定的類型–或是程序員定義的,或是語言或BCL定義的。然而,不少時候,咱們須要把類的行爲提取或重構出來,使之不只能用到它們編碼的數據類型上,還能應用到其餘類型上。
泛型能夠作到這一點。咱們重構代碼並額外增長一個抽象層,對於這樣的代碼來講,數據類型就不用硬編碼了。這是專門爲多段代碼在不一樣的數據類型上執行相同指令的狀況專門設計的。程序員
聽起來比較抽象,下面看一個示例安全
假設咱們聲明一個MyIntStack類,該類實現一個int類型的棧。它容許int值的壓入彈出。函數
class MyIntStack { int StackPointer=0; int[] StackArray; public void Push(int x) { ... } public int Pop() { ... } }
假設如今但願將相同的功能應用與float類型的值,能夠有幾種方式來實現。不用泛型,按照咱們之前的思路產生的代碼以下。this
class MyFloatStack { int StackPointer=0; float[] StackArray; public void Push(float x) { ... } public float Pop() { ... } }
這個方法固然可行,但容易出錯且有以下缺點:編碼
泛型(generic)特性提供了一種更優雅的方式,可讓多個類型共享一組代碼。泛型容許咱們聲明類型參數化(type-parameterized)的代碼,能夠用不一樣的類型進行實例化。即咱們能夠用「類型佔位符」來寫代碼,而後在建立類的實例時指明真實的類型。
本書讀到這裏,咱們應該很清楚類型不是對象而是對象的模板這個概念了。一樣地,泛型類型也不是類型,而是類型的模板。
C#提供了5種泛型:類、結構、接口、委託和方法。
注意,前4個是類型,而方法是成員。 spa
將MyIntStack和MyFloatStack兩個類改成MyStack泛型類。設計
class MyStack<T> { int StackPointer=0; T[] StackArray; public void Push(T x){...} public T Pop(){...} }
建立和使用常規的、非泛型的類有兩個步驟:聲明和建立類的實例。可是泛型類不是實際的類,而是類的模板,因此咱們必須從它們構建實際的類類型,而後建立實例。
下圖從一個較高的層面上演示了該過程。3d
聲明一個簡單的泛型類和聲明普通類差很少,區別以下。調試
class SomeClass<T1,T2> { public T1 SomeVar=new T1(); public T2 OtherVar=new T2(); }
泛型類型聲明中沒有特殊的關鍵字,取而代之的是尖括號中的類型參數列表。code
一旦建立了泛型類型,咱們就須要告訴編譯器能使用哪些真實類型來替代佔位符(類型參數)。
建立構造類型的語法以下,包括列出類名並在尖括號中提供真實類型來替代類型參數。要替代類型參數的真實類型叫作類型實參(type argument)。
SomeClass<short,int>
編譯器接受類型實參而且替換泛型類主體中的相應類型參數,產生構造類型–從它建立真實類型的實例。
下圖演示了類型參數和類型實參的區別。
在建立引用和實例方面,構造類類型的使用和常規類型類似。
MyNonGenClass myNGC=new MyNonGenClass();
SomeClass<short,int> mySc1=new SomeClass<short,int>(); var mySc2=new SomeClass<short,int>();
和非泛型同樣,引用和實例能夠分開建立。
SomeClass<short,int> myInst; myInst=new SomeClass<short,int>();
能夠從同一泛型類型構建不一樣類類型。每一個獨立的類類型,就好像它們都有獨立的非泛型類聲明同樣。
class SomeClass<T1,T2> { ... } class Program { static void Main() { var first=new SomeClass<short,int>(); var second=new SomeClass<int,long>(); } }
class MyStack<T> { T[] StackArray; int StackPointer=0; public void Push<T x> { if(!IsStackFull) { StackArray[StackPointer++]=x; } } public T Pop() { return (!IsStackEmpty) ?StackArray[--StackPointer] :StackArray[0]; } const int MaxStack=10; bool IsStackFull{get{return StackPointer>=MaxStack;}} bool IsStackEmpty{get{return StackPointer<=0;}} public MyStack() { StackArray=new T[MaxStack]; } public void Print() { for(int i=StackPointer-1;i>=0;i--) { Console.WriteLine(" Value:{0}",StackArray[i]); } } } class Program { static void Main() { var StackInt=new MyStack<int>(); var StackString=new MyStack<string>(); StackInt.Push(3); StackInt.Push(5); StackInt.Push(7); StackInt.Push(9); StackInt.Print(); StackString.Push("This is fun"); StackString.Push("Hi there! "); StackString.Print(); } }
在泛型棧的示例中,棧除了保存和彈出它包含的一些項以外沒作任何事情。它不會嘗試添加、比較或作其餘任何須要用到項自己的運算符的事情。理由是,泛型棧不知道它保存的項的類型是什麼,也不知道這些類型實現的成員。
然而,C#對象都從object類繼承,所以,棧能夠確認,這些保存的項都實現了object類的成員。它們包括ToString、Equals以及GetType。
若是代碼嘗試使用除object類的其餘成員,編譯器會產生錯誤。
例:
class Simple<T> { static public bool LessThan(T i1,T i2) { return i1<i2; //錯誤 } ... }
要讓泛型變得更有用,咱們須要提供額外的信息讓編譯器知道參數能夠接受哪些類型。這些信息叫作約束(constrain)。只有符合約束的類型才能替代類型參數。
約束使用Where子句列出。
where子句語法以下:
類型參數 約束列表 ↓ ↓ where TypeParam:constraint,constraint,... ↑ ↑ 關鍵字 冒號
有關where子句的要點:
例:where子句示例
class MyClass<T1,T2,T3> where T2:Customer where T3:IComparable { ... }
where子句能夠以任何次序列出。然而where子句中的約束必須有特定順序。
例:約束示例
class SortedList<S> where S:IComparable<S>{...} class LinkedList<M,N> where M:IComparable<M> where N:ICloneable{...} class MyDictionary<KeyType,ValueType> where KeyType:IEnumerable, new() {...}
與其餘泛型不同,方法是成員,不是類型。泛型方法能夠在泛型和非泛型類以及結構和接口中聲明。
泛型方法具備類型參數列表和可選的約束
類型參數列表 約束子句 ↓ ↓ public void PrintData<S,T>(S p,T t)where S:Person { ↑ ... 方法參數列表 }
記住,類型參數列表在方法名稱後,在方法參數列表前。
調用方法,需在調用時提供類型實參,以下:
MyMethod<short,int>(); MyMethod<int,long>();
例:調用泛型方法示例
若是咱們爲方法傳入參數,編譯器有時能夠從方法參數中推斷出泛型方法的類型形參用到的那些類型。這樣就可使方法調用更簡單,可讀性更強。
以下代碼,若咱們使用int類型變量調用MyMethod,方法調用中的類型參數信息就多餘了,由於編譯器能夠從方法參數得知它是int。
int myInt=5; MyMethod<int>(myInt);
因爲編譯器能夠從方法參數中推斷類型參數,咱們能夠省略類型參數和調用中的尖括號,以下:
MyMethod(myInt);
class Simple { static public void ReverseAndPrint<T>(T[] arr) { Array.Reverse(arr); foreach(T item in arr) { Console.WriteLine("{0},",item.ToString()); } Console.WriteLine(""); } } class Program { static void Main() { var intArray=new int[]{3,5,7,9,11}; var stringArray=new string[]{"first","second","third"}; var doubleArray=new double[]{3.567,7,891,2,345}; Simple.ReverseAndPrint<int>(intArray); Simple.ReverseAndPrint(intArray); Simple.ReverseAndPrint<string>(stringArray); Simple.ReverseAndPrint(stringArray); Simple.ReverseAndPrint<double>(doubleArray); Simple.ReverseAndPrint(doubleArray); } }
在第7章中,咱們詳細介紹了擴展方法,它也能夠和泛型類結合使用。它容許咱們將類中的靜態方法關聯到不一樣的泛型類上,還容許咱們像調用類結構實例的實例方法同樣來調用方法。
和非泛型類同樣,泛型類的擴展方法:
static class ExtendHolder { public static void Print<T>(this Holder<T>h) { T[] vals=h.GetValue(); Console.WriteLine("{0},\t{1},\t{2}",vals[0],vals[1],vals[2]); } } class Holder<T> { T[] Vals=new T[3]; public Holder(T v0,T v1,T v2) { Vals[0]=v0;Vals[1]=v1;Vals[2]=v2; public T[] GetValues(){return Vals;} } } class Program { static void Main() { var intHolder=new Holder<int>(3,5,7); var stringHolder=new Holder<string>("a1","b2","c3"); intHolder.Print(); stringHolder.Print(); } }
與泛型類類似,泛型結構能夠有類型參數和約束。泛型結構的規則和條件與泛型類同樣。
struct PieceOfData<T> { public PieceOfData(T value){_data=value;} private T _data; public T Data { get{return _data;} set{_data=value;} } } class Program { static void Main() { var intData=new PieceOfData<int>(10); var stringData=new PieceOfData<string>("Hi there."); Console.WriteLine("intData ={0}",intData.Data); Console.WriteLine("stringData ={0}",stringData.Data); } }
泛型委託與非泛型委託很是類似,不過類型參數決定能接受什麼樣的方法。
`delegate R MyDelegate<T,R>(T Value);`
例:泛型委託示例
delegate void MyDelegate<T>(T value); class Simple { static public void PrintString(string s) { Console.WriteLine(s); } static public void PrintUpperString(string s) { Console.WriteLine("{0}",s.ToUpper()); } } class Program { static void Main() { var myDel=new MyDelegate<string>(Simple.PrintString); myDel+=Simple.PrintUpperString; myDel("Hi There."); } }
C#的LINQ(第19章)特性在不少地方使用了泛型委託,但在介紹LINQ前,有必要給出另一個示例。
public delegate TR Func<T1,T2,TR>(T1 p1,T2 p2);//泛型委託 class Simple { static public string PrintString(int p1,int p2) { int total=p1+p2; return total.ToString(); } } class Program { static void Main() { var myDel=new Fun<int,int,string>(Simple.PrintString); Console.WriteLine("Total:{0}",myDel(15,13)); } }
泛型接口容許咱們編寫參數和返回類型是泛型類型參數的接口。
例:IMyIfc泛型接口
interface IMyIfc<T> { T ReturnIt(T inValue); } class Simple<S>:IMyIfc<S> { public S ReturnIt(S inValue) { return inValue; } } class Program { static void Main() { var trivInt=new Simple<int>(); var trivString=new Simple<string>(); Console.WriteLine("{0}",trivInt.ReturnIt(5)); Console.WriteLine("{0}",trivString.ReturnIt("Hi there.")); } }
以下示例演示了泛型接口的兩個額外能力:
例:Simple是實現泛型接口的非泛型類。
interface IMyIfc<T> { T ReturnIt(T inValue); } class Simple:IMyIfc<int>,IMyIfc<string> //非泛型類 { public int ReturnIt(int inValue) //實現int類型接口 {return inValue;} public string ReturnIt(string inValue) //實現string類型接口 {return inValue;} } class Program { static void Main() { var trivial=new Simple(); Console.WriteLine("{0}",trivial.ReturnIt(5)); Console.WriteLine("{0}",trivial.ReturnIt("Hi there.")); } }
實現泛型類接口時,必須保證類型實參組合不會在類型中產生兩個重複的接口。
例:Simple類使用了兩個IMyIfc接口的實例化。
對於泛型接口,使用兩個相同接口自己沒有錯,但這樣會產生一個潛在衝突,由於若是把int做爲類型參數來替代第二個接口中的S的話,Simple可能會有兩個相同類型的接口,這是不容許的。
interface IMyIfc<T> { T ReturnIt(T inValue); } class Simple<S>:IMyIfc<int>,IMyIfc<S> //錯誤 { public int ReturnIt(int inValue) {return inValue;} public S ReturnIt(S inValue) //若是它不是int類型的 {return inValue;} //將和上個示例的接口同樣 }
說明:泛型接口的名字不會和非泛型衝突。例如,在前面的代碼中咱們還能夠聲明一個名爲IMyIfc的非泛型接口。
縱觀本章,你們已經看到,若是你建立泛型類型的實例,編譯器會接受泛型類型聲明以及類型參數來構造類型。可是,你們一般會錯誤的將派生類型分配給基類型的變量。下面咱們來看一下這個主題,這叫作可變性(variance)。它分爲三種–協變(convariance)、逆變(contravariance)和不變(invariance)。
首先回顧已學內容,每一個變量都有一種類型,能夠將派生類對象的實例賦值給基類變量,這叫賦值兼容性。
例:賦值兼容性
class Animal { public int NumberOfLegs=4; } class Dog:Animal { } class Program { static void Main() { var a1=new Animal(); var a2=new Dog(); Console.WriteLine("Number of dog legs:{0}",a2.NumberOfLegs); } }
如今,咱們來看一個更有趣的例子,用下面的方式對代碼進行擴展。
class Animal{public int NumberOfLegs=4;} class Dog:Animal{} delegate T Factory<T>(); class Program { static Dog MakeDog() { return new Dog(); } static void Main() { Factory<Dog> dogMaker=MakeDog; Factory<Animal>animalMaker=dogMaker; Console.WriteLine(animalMaker().Legs.ToString()); } }
上面代碼在Main的第二行會報錯,編譯器提示:不能隱式把右邊的類型轉換爲左邊的類型。
看上去由派生類型構造的委託應該能夠賦值給由基類構造的委託,那編譯器爲什麼報錯?難道賦值兼容性原則不成立了?
不是,原則依然成立,可是對於這種狀況不適用!問題在於儘管Dog是Animal的派生類,可是委託Factory<Dog>
沒有從委託Factory<Animal>
派生。相反,兩個委託對象是同級的,它們都從delegate類型派生。
再仔細分析一下這種狀況,咱們能夠看到,若是類型參數只用做輸出值,則一樣的狀況也適用於任何泛型委託。對於全部這樣的狀況,咱們應該可使用由派生類建立的委託類型,這樣應該可以正常工做,由於調用代碼老是指望獲得一個基類的引用,這也正是它會獲得的。
若是派生類只是用於輸出值,那麼這種結構化的委託有效性之間的常數關係叫作協變。爲了讓編譯器知道這是咱們的指望,必須使用out關鍵字標記委託聲明中的類型參數。
增長out關鍵字後,代碼就能夠經過編譯並正常工做了。
delegate T Factory<out T>(); ↑ 關鍵字指定了類型參數的協變
T Factory<out T>()
的委託類型,其中類型變量T是Animal類如今來看另外一種狀況。
class Animal{public int NumberOfLegs=4;} class Dog:Animal{} delegate T Factory<T>(); class Program { delegate void Action1<in T>(T a); static void ActOnAnimal(Animal a) { Console.WriteLine(a.NumberOfLegs); } static void Main() { Action1<Animal> act1=ActOnAnimal; Action1<Dog> dog1=act1; dog1(new Dog()); } }
和以前狀況類似,默認狀況下不能夠賦值兩種不兼容的類型。但在某些狀況下可讓這種賦值生效。
其實,若是類型參數只用做委託中方法的輸入參數的話就能夠了。由於即便調用代碼傳入了一個程度更高的派生類的引用,委託中的方法也只指望一個程度低一些的派生類的引用,固然,它也仍然接受並知道如何操做。
這種指望傳入基類時容許傳入派生對象的特性叫作逆變。能夠在類型參數中顯式使用in關鍵字來使用。
void Action1<in T>(T p)
類型的委託,其類型變量是Dog類下圖總結了泛型委託中協變和逆變的不一樣
F<out T>()
類型的委託,類型變量是叫作Base的類F<in T>(T p)
類型的委託,類型參數是Derived類如今你應該已經理解了協變和逆變能夠應用到委託上。其實相同的原則也可用到接口上,能夠在聲明接口的時候使用out和in關鍵字。
例:使用協變的接口
class Animal{public string Name;} class Dog:Animal{}; interface IMyIfc<out T> { T GetFirst(); } class SimpleReturn<T>:IMyIfc<T> { public T[] items=new T[2]; public T GetFirst() { return items[0]; } } class Program { static void DoSomething(IMyIfc<Animal>returner) { Console.WriteLine(returner.GetFirst().Name); } static void Main() { SimpleReturn<Dog> dogReturner=new SimpleReturn<Dog>(); dogReturner.items[0]=new Dog(){Name="Avonlea"}; IMyIfc<Animal> animalReturner=dogReturner; DoSomething(dogReturner); } }
以前的兩小節解釋了顯式的協變和逆變。還有一些狀況編譯器能夠自動識別某個已構建的委託是協變或是逆變並自動進行類型強制轉換。這一般發生在沒有爲對象的類型賦值的時候,以下代碼演示了該例子。
class Animal{public int Legs=4;} class Dog:Animal{} class Program { delegate T Factory<out T>(); static Dog MakeDog() { return new Dog(); } static void Main() { Factory<Animal> animalMaker1=MakeDog;//隱式強制轉換 Factory<Dog> dogMaker=MakeDog; Factory<Animal> animalMaker2=dogMaker;//須要out標識符 Factory<Animal> animalMaker3 =new Factory<Dog>(MakeDog);//須要out標識符 } }
有關可變性的其餘一些重要事項以下:
協變 ↓ delegate T Factory<out R,in S,T>(); ↑ ↑ 逆變 不變