1 概述
1.1 引入泛型方法
在某些情况下,一个类型中可能只有少数方法成员用到了类型参数,这时就未必需要将整个类型都定义成为泛型。例如在下面的代码中,泛型类GC<T>定义了一个静态方法Contain,用于判断一个元素是否存在于一个数组之中:
public class GC<T> { //静态字段 static readonly double PI=3.14; //方法 public static bool Contain(T[] ts,T tp) { foreach(T t1 in ts) { if(t1.Equals(tp)) return true; } return false; } }
在每次调用该方法时,需要指定该泛型类的一个封闭的构造类型:
short[] sArray = new short[] {1,3,15,255}; bool b1 = GC<short>.Contain(sArray,short.MaxValue);//false int [] iArray = new int[]{1,3,15,255,65535}; bool b2=GC<int>.Contain(iArray,ushort.MaxValue);//true
泛型类GC<T>中定义了一个静态字段PI。由于静态成员属于泛型类的构造类型所有,所以对于每一个构造类型,都为该字段分配了存储空间。
而下面的程序将类型参数从类的定义中转移到方法的定义中,这就是泛型方法:
class GenericsMethodSample { static void Main() { short[] sArray = new short[] { 1, 3, 15, 255 }; Console.WriteLine(C.Contain<short>(sArray, short.MaxValue));//false int[] iArray = new int[] { 1, 3, 15, 255, 65535 }; Console.WriteLine(C.Contain<int>(iArray, ushort.MaxValue));//true } } /// <summary> /// 泛型方法类 /// </summary> public class C { //静态字段 static readonly double PI = 3.14; //泛型方法 public static bool Contain<T>(T[] ts, T tp) { foreach (T t1 in ts) { if (t1.Equals(tp)) return true; } return false; } }
上面程序中,两次方法的调用都是通过同一个类进行的,静态字段PI在内存中只会占用一个存储。
1.2 泛型方法的定义
定义泛型方法也是在方法名之后将类型参数包含在一对分隔符<>中。如果有多个类型参数,则相互间用“,”号分割。之后,所定义的类型参数既可以作为方法的参数类型和返回类型,也可以用来在方法的执行代码中定义局部变量。除此之外,泛型方法的定义规则与普通方法的定义规则相同。
如果在同一个类型中定义了多个泛型方法,它们的类型参数是互不相关的。
如果泛型方法属于一个泛型类型,而二者又定义了相同的类型参数,那么类型参数的使用也服从“屏蔽规则”,即泛型方法中出现的类型参数属于该方法的定义,而在类型的其它成员中出现的类型参数属于类的定义。例如:
public class GArith<T> { private T[] m_list; public static void Swap<T>(ref T tp1,ref T tp2) { ... } }
这时为泛型类GArith<T>创建任何一个构造类型的实例,其私有有字段m_list类型都被替换为相应的封闭类型,但不会影响到泛型方法Swap<T>的参数类型。同样,指定给方法的封闭类型也和类的构造类型无关。例如:
int x=2; int y=5; GArith<string>.Swap<int>(ref x,ref y);//correct Garith<int>.Swap<string>(ref x,ref y);//error:传递类型与实际类型不同
所以,为了提高程序的可读性,应尽量避免为泛型类型及其泛型的成员方法定义同名的类型参数。
在泛型方法中同样可以对类型参数进行限制,限制方式和泛型类的相同
public static T Max<T>(T tp1,T tp2) where T : IComparable<T> { if(tp1.CompareTo(tp2) > 0) return tp1; else return tp2; }
泛型方法既可以属于普通类型,也可以属于泛型类型(泛型类、泛型结构、泛型接口)。
C#中不允许定义泛型的属性、事件、索引函数和操作符。???
1.3 调用泛型方法
调用泛型方法有两种方式:
一是在调用时显式指定方法的类型参数列表,前面的例子就是这这种方式。
Console.WriteLine(C.Contain<short>(sArray,short.MaxValue));
二是在调用时省略方法的类型参数列表:
Console.WrtiteLine(C.Contain(sArray,short.MaxValue));
由于传递给泛型方法的两个参数类型分别是short[]和short,调用的过程和结果与完整调用是一样的。
在采用简写调用方式时,编译器需要检查每个参数的类型,并试图由此推断出每个类型参数最终被哪个封闭类型所取代。这个过程称为类型推断。如果推断成功,则会将简写调用代码映射到对应的完整调用代码;否则编译就会失败。
编译器进行类型推断的算法较为复杂,简单的理解是:在泛型方法的声明中出现相同的类型参数的地方,调用时必须以相同的封闭类型去替换这些类型参数,否则类型推断就会失败,例如下面的代码就是错误的:
Console.WriteLine(C.Contain(iArray,ushort.MaxValue));
因为方法的第一个参数,T被替换成int;而对于第二个参数则被替换成ushort,所以转换失败。如果仍要采用简写方式,则应首先进行类型转换:
int x = ushort.MaxValue; Console.WriteLine(C.Contain(iArray,x));
或者合并为下面的形式(尽管ushort类型可以隐式转换到int类型,但在简写调用泛型方法时仍然需要对参数采用显式转换的格式。这一点和调用普通方法以及完整调用泛型方法是不同的):
Console.WriteLine(C.Contain(iArray,(int)ushort.MaxValue));
1.4 惟一性规则
如果泛型方法的名称相同,那么通过指定不同的类型参数能否使方法具有不同的标识呢?可以分几种情况来讨论:
(1)有无类型参数以及以及类型参数的不同个数,足以区分不同的方法
public class C
{
public void F(){} public void F<T>(){} public void F<T,S>(){}
}
(2)仅仅是类型参数的名称不同,不足以区分不同的方法:
public class C
{
public void F<T>(T t1){} public void S<S>(S s1){}//error
}
(3)类型参数作为传递给方法的参数类型时,不同的参数类型足以区分不同的方法,但在调用时,因为类型参数的替换出现了多个可能,在调用时就可能是错误。
public class C
{
public static void F<R,S>(R r1,S s1){} public static void F<R,S>(S s1,R r1){}
}
对于以上定义,下面的第一次调用是合法的,而第二次调用会出现错误
int x=2; int y=3; string s="hello"; C.F<string,int>(s,x); C.F<int,int>(x,y);//error
(4)如果泛型方法属于泛型类型所有,而外部类型的类型参数在方法中出现。这种情况较为复杂,但判断规则和第3种情况相同,即不同的参数类型足以区分不同的方法,而在调用时不允许出现歧义。例如下面的定义都是合法的:
public class GA<T> { } public class GC<T> { public static void F<S>(T t1,S s1){} public static void F<S>(S s1,T t1){} public static void F<R,S>(GA<R> a,GA<S> b){} public static void F<R,S>(GA<S> a,GA<R> b){} }
但在下面的调用代码中,最后两行是错误的,它们会引起歧义:
int x=2; int y=3; string s="hello"; GC<string>.F<int>(s,x); GC<string>.F<int,double>(new GA<int>(),new GA<double>()); GC<int>.F<int>(x,y);//error:与方法一和方法二存在歧义 GC<string>.F<int,int>(new GA<int>(),new GA<int>());//error:与方法三和方法四存在歧义
(5)泛型方法的类型限制不是方法标识的一部分,不同的限制不足以区分不同的方法。例如下面的方法定义都是重复的:
public class C { public static void F<T>() where T : IComparable {} public static void F<T>() where T : IComparable<T> {} public static void F<T>() where T : IComparer<T> {} public static void F<U>() {}
}
2 泛型方法的重载
2.1 概述
和普通方法一样,泛型方法也可以被定义成为虚拟方法、重载方法、抽象方法或密封方法。普通方法的继承和多态性的内容同样适用于泛型方法。当泛型方法属于某个泛型类型时,8章中2.3小节中讲的嵌套泛型类的规则和8章中5小节泛型类之间的继承的规则也同样适用。
下面的代码示范了泛型方法的继承及重载
public abstract class GA { public abstract void MethodA<T>(T tp1,T tp2); } public class GB<T> : GA { public override void MethodA<R>(R r1,R r2) {} public virtual void MethoB<S>(S sp,T tp) {} } public class GC<T> : GB<int> { public override void MethodA<R r1,R r2) {} public sealed override void MethodB<S>(S sp,int i2) {} }
和泛型类之间的继承类似,派生类在继承或重载基类中的泛型方法时,同时也继承了基类中对泛型方法的类型限制。不过在泛型方法继承中,不需要在派生类的泛型方法中再明确写出这种限制。但如果在派生类中使用new修饰符对基类中的泛型方法进行了覆盖,那么基类中对泛型方法的类型限制则不再有效。
例如对于下面的类和继承定义,派生类Derived重载了基类的泛型方法Method1<S>,虽然它没有显式地写出对类型参数S的限制,但在方法调用时仍然要满足限制要求:
public class A { } public abstract class Base { public abstract void Method1<S>(S[] ss) where S : IComparable; public virtual void Method2<T>(T tp1,T tp2) where T : IComparable<T>{} } public class Derived : Base { public override void Method1<S>(S[] ss) {} public new void Method2<T>(T tp1,T tp2){} }
下面的代码中,调用Derived对象的Method1<int[]>方法是合法的,而调用其Method1<A[]>则是不合法的,因为类A的定义中并没有说明它继承的接口IComparable:
Derived d1 = new Derived(); int[] iArray = new int[5]; d1.Method1<int>(iArray); A[] array = new A[5]; d1.Method1<A>(array);//error
而对于方法Method2<T>,它在隐藏基类方法的同时也隐藏了对类型限制的要求。下面的代码是完全合法的:
Derived d1 = new Derived(); A a1 = new A(); A a2 = new A(); d1.Method2(a1,a2);
2.2 示例程序:读写器
本节将使用泛型方法来进一步改进管理联系人类型的应用程序。将对联系人内容的输入输出工作抽象出来,放在一个新定义的读写器类中加以管理。而通过读写器类的不同派生类,可以实现不同方式的输入输出,如控制台输入输出、Windows窗体输入输出、文件流输入输出。
2.2.1 读写器定义
/// <summary> /// 抽象类:读写器 /// </summary> public abstract class RW { public abstract bool OpenRead(); public abstract bool OpenWrite(); public abstract void CloseRead(); public abstract void CloseWrite(); public abstract string Read(string sPrompt); public abstract void Write(string sPrompt, string sContent); } /// <summary> /// 派生类:控制台读写器 /// </summary> public class ConsoleRW:RW { public override bool OpenRead() { return true; } public override bool OpenWrite() { return true; } public override void CloseRead() { } public override void CloseWrite() { } public override string Read(string sPrompt) { Console.WriteLine("请输入{0}", sPrompt); return Console.ReadLine(); } public override void Write(string sPrompt, string sContent) { Console.WriteLine("{0}:{1} ", sPrompt, sContent); } } /// <summary> /// 派生类:不带分行的控制台读写器 /// </summary> public class NoBreakConsoleRW:ConsoleRW { public override void Write(string sPrompt, string sContent) { Console.Write("{0}:{1} ", sPrompt, sContent); } }
2.2.2 联系人定义
class GenericsMethodOverrideSample { static void Main() { Contact[] cons = new Contact[100]; ConsoleRW rw = new ConsoleRW(); int iCount = 0; while (rw.Read("继续输入联系人信息?(Y/N)").ToUpper()!="N") { cons[iCount]=new Contact(); cons[iCount].Input(rw); iCount++; if(iCount==100) break; } if(iCount==0) return; if (rw.Read("请选择分块输出(按任意键)或单行输出(S)").ToUpper() == "S") rw = new NoBreakConsoleRW(); for (int i = 0; i < iCount; i++) { cons[i].Output(rw); Console.WriteLine(); } } } /// <summary> /// 基类:联系人Contact /// </summary> public class Contact:IComparable<Contact> { //字段 protected string m_name = "未知"; protected string m_gender = "女"; protected string[] m_phones; //属性 public string Name { get { return m_name; } set { m_name = value; } } public string Gender { get { return m_gender; } set { if (value == "男" || value == "女") m_gender = value; } } //构造函数 public Contact() { m_phones = new string[3]; } public Contact(string sName) { m_name = sName; m_phones = new string[3]; } //方法 public int CompareTo(Contact con) { return this.m_name.CompareTo(con.m_name); } public bool Equals(Contact con) { return this.Name.Equals(con.Name); } public virtual void Input<T>(T tp) where T : RW { m_name = tp.Read("姓名"); Gender = tp.Read("性别"); m_phones[0] = tp.Read("住宅电话"); m_phones[1] = tp.Read("办公电话"); m_phones[2] = tp.Read("手机"); } public virtual void Output<T>(T tp) where T : RW { tp.Write("姓名", m_name); tp.Write("性别", m_gender); tp.Write("住宅电话", m_phones[0]); tp.Write("办公电话", m_phones[1]); tp.Write("手机", m_phones[2]); } } /// <summary> /// 派生类:商务Business /// </summary> public class Business:Contact { //字段 protected string m_company = ""; protected string m_title = ""; //属性 public string Company { get { return m_company; } set { m_company = value; } } public string Title { get { return m_title; } set { m_title = value; } } //构造函数 public Business() { m_phones = new string[4]; } public Business(string sName) { m_name = sName; m_phones = new string[4]; } //重载方法 public override void Input<T>(T tp) { m_name = tp.Read("姓名"); Gender = tp.Read("性别"); m_company = tp.Read("公司"); m_title = tp.Read("职务"); m_phones[0] = tp.Read("办公电话"); m_phones[1] = tp.Read("商务电话"); m_phones[2] = tp.Read("住宅电话"); m_phones[3] = tp.Read("手机"); } public override void Output<T>(T tp) { tp.Write("姓名", m_name); tp.Write("性别", m_gender); tp.Write("公司", m_company); tp.Write("职务", m_title); tp.Write("办公电话", m_phones[0]); tp.Write("商务电话", m_phones[1]); tp.Write("住宅电话", m_phones[2]); tp.Write("手机", m_phones[3]); } } /// <summary> /// 派生类:同学Classmate /// </summary> public class Classmate:Contact { //字段 protected DateTime m_birthday; //属性 public DateTime Birthday { get { return m_birthday; } set { m_birthday = value; } } //构造函数 public Classmate() : base() { } public Classmate(string sName) : base(sName) { } //方法 public override void Input<T>(T tp) { base.Input(tp); m_birthday = DateTime.Parse(tp.Read("生日(yyyy-mm-dd):")); } public override void Output<T>(T tp) { base.Output(tp); tp.Write("生日:", m_birthday.ToShortDateString()); } }
程序通过Contact类中定义的泛型方法进行内容的输入和输出。运行结果如下:
请输入继续输入联系人信息?(Y/N) Y 请输入姓名 张三 请输入性别 男 请输入住宅电话 666666 请输入办公电话 888888 请输入手机 13888888888 请输入继续输入联系人信息?(Y/N) N 请输入请选择分块输出(按任意键)或单行输出(S) 姓名:张三 性别:男 住宅电话:666666 办公电话:888888 手机:13888888888 请按任意键继续. . .
上面的程序将所有的联系人都视为同一种类型。而如果要实现不同类别的联系人的管理功能,对程序进行改进,主要是增加联系人类别的选择功能。
class GenericsReadWriteSample { static void Main() { Contact[] cons = new Contact[100]; ConsoleRW rw = new ConsoleRW(); int iCount = 0; while (rw.Read("继续输入联系人信息?(Y/N)").ToUpper() != "N") { string sType = rw.Read("请选择类别(1.普通 2.商务 3.同学)"); if (sType == "1") cons[iCount] = new Contact(); else if (sType == "2") cons[iCount] = new Business(); else if (sType == "3") cons[iCount] = new Classmate(); else break; cons[iCount].Input(rw); iCount++; if (iCount == 100) break; } if (iCount == 0) return; if (rw.Read("请选择分块输出(按任意键)或单行输出(S)").ToUpper() == "S") rw = new NoBreakConsoleRW(); for (int i = 0; i < iCount; i++) { cons[i].Output(rw); Console.WriteLine(); } } }
程序运行结果:
请输入继续输入联系人信息?(Y/N) y 请输入请选择类别(1.普通 2.商务 3.同学) 1 请输入姓名 张三 请输入性别 男 请输入住宅电话 666666 请输入办公电话 888888 请输入手机 138888888 请输入继续输入联系人信息?(Y/N) Y 请输入请选择类别(1.普通 2.商务 3.同学) 2 请输入姓名 李四 请输入性别 男 请输入公司 Microsoft 请输入职务 经理 请输入办公电话 666666 请输入商务电话 888888 请输入住宅电话 999999 请输入手机 13999999999 请输入继续输入联系人信息?(Y/N) Y 请输入请选择类别(1.普通 2.商务 3.同学) 3 请输入姓名 王五 请输入性别 女 请输入住宅电话 666666 请输入办公电话 888888 请输入手机 13777777777 请输入生日(yyyy-mm-dd): 1981-12-01 请输入继续输入联系人信息?(Y/N) N 请输入请选择分块输出(按任意键)或单行输出(S) S 姓名:张三 性别:男 住宅电话:666666 办公电话:888888 手机:138888888 姓名:李四 性别:男 公司:Microsoft 职务:经理 办公电话:666666 商务电话:888888 住宅电话:999999 手机:13999999999 姓名:王五 性别:女 住宅电话:666666 办公电话:888888 手机:13777777777 生日::1 981-12-1 请按任意键继续. . .
4 小结
泛型的概念不只适用于类型,也适用于类型的方法成员。泛型方法通过类型参数对普通方法进行了抽象,只需要一次定义就可以扩展到多种类型。和普通方法一样,泛型方法需要满足惟一性规则,也可以在类的继承层次中进行重载,还可以通过代表来封装和调用。使用泛型方法的关键是把握类型参数及其封闭类型之间的关系。