到如今为止,全部在类声明中用到的类型都是特定的类型–或是程序员定义的,或是语言或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>(); ↑ ↑ 逆变 不变