到现在为止,所有在类声明中用到的类型都是特定的类型–或是程序员定义的,或是语言或BCL定义的。然而,很多时候,我们需要把类的行为提取或重构出来,使之不仅能用到它们编码的数据类型上,还能应用到其他类型上。
泛型可以做到这一点。我们重构代码并额外增加一个抽象层,对于这样的代码来说,数据类型就不用硬编码了。这是专门为多段代码在不同的数据类型上执行相同指令的情况专门设计的。
听起来比较抽象,下面看一个示例
假设我们声明一个MyIntStack类,该类实现一个int类型的栈。它允许int值的压入弹出。
class MyIntStack{int StackPointer=0;int[] StackArray;public void Push(int x){...}public int Pop(){...}}
假设现在希望将相同的功能应用与float类型的值,可以有几种方式来实现。不用泛型,按照我们以前的思路产生的代码如下。
class MyFloatStack{int StackPointer=0;float[] StackArray;public void Push(float x){...}public float Pop(){...}}
这个方法当然可行,但容易出错且有如下缺点:
泛型(generic)特性提供了一种更优雅的方式,可以让多个类型共享一组代码。泛型允许我们声明类型参数化(type-parameterized)的代码,可以用不同的类型进行实例化。即我们可以用“类型占位符”来写代码,然后在创建类的实例时指明真实的类型。
本书读到这里,我们应该很清楚类型不是对象而是对象的模板这个概念了。同样地,泛型类型也不是类型,而是类型的模板。
C#提供了5种泛型:类、结构、接口、委托和方法。
注意,前4个是类型,而方法是成员。
将MyIntStack和MyFloatStack两个类改为MyStack泛型类。
class MyStack<T>{int StackPointer=0;T[] StackArray;public void Push(T x){...}public T Pop(){...}}
创建和使用常规的、非泛型的类有两个步骤:声明和创建类的实例。但是泛型类不是实际的类,而是类的模板,所以我们必须从它们构建实际的类类型,然后创建实例。
下图从一个较高的层面上演示了该过程。
声明一个简单的泛型类和声明普通类差不多,区别如下。
class SomeClass<T1,T2>{public T1 SomeVar=new T1();public T2 OtherVar=new T2();}
泛型类型声明中没有特殊的关键字,取而代之的是尖括号中的类型参数列表。
一旦创建了泛型类型,我们就需要告诉编译器能使用哪些真实类型来替代占位符(类型参数)。
创建构造类型的语法如下,包括列出类名并在尖括号中提供真实类型来替代类型参数。要替代类型参数的真实类型叫做类型实参(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:Customerwhere 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>();↑ ↑逆变 不变
原文:http://www.cnblogs.com/moonache/p/6385258.html