泛型方法是包含在某个类中的一类方法的一般形式。可以根据泛型方法生成对不同类型操作的具体方法。泛型方法严格的来说并不是一个方法,它只是一个方法的蓝图,或者说是一类具体方法的写法格式,在这个格式中有一些类型的占位符需要去填写。方法的调用就是把具体类型填写到这些占位符的过程。
泛型方法在具体调用的时候,应该在方法名后面的<>内给出具体的类型。但是在c#中,允许省略掉方法名后面的类型参数,在程序编译的时候,编译器会自动推断方法调用时参数的类型,然后根据这些参数的类型依据模板自动的生成相应的具体的方法。这个叫类型推断。类型推断时,需要根据方法参数的类型来进行,所以类型推断不适用没有参数的方法。
泛型方法也同样允许重载。比如允许如下形式的重载
void show<T ,U>(T t,U u)
void show<T>(T t)
泛型方法的重载只能依据方法参数个数的不同来进行,如果想依据泛型方法中参数个数相同而类型不同或者不同类型的排列顺序的不同来重载,可能会出问题,比如方法void show<T ,U>(T t,U u)与void show<U,T>(U u,T t)是不能进行重载的,因为在泛型方法中,类型参数T,U都只是一个类型的占位符,给它们指定任何类型都是允许的,它们这里的T,U对于泛型方法来说没有任何的区别,所以对于泛型来说,它们实际上同一个泛型方法。
如果在一个类中存在一个具体方法的写法,比如void Show(int i,char c),而同时又存在一个跟它格式相同的泛型方法,如void Show<T ,U >( T t,U u),那么当调用void Show(int,char)这样形式的方法时,程序将会优先考虑具体的实现方法,而忽略掉该形式的泛型方法。
静态方法也可以写成泛型的形式。
例子GenericsStaticMethod演示了上面提到的几种情况。
namespace GenericsStaticMethod
{
class Program
{
static void Main(string[] args)
{
Program p = new Program();
int a = 4;
int b = 8;
//调用时指明类型参数具体的类型
Change<int>(ref a, ref b);
Console.WriteLine("{0}<---->{1}", a, b);
char c1='g';
char c2='k';
//调用时没有指明类型参数具体的类型,依靠类型推断
Change(ref c1,ref c2);
Console.WriteLine("{0}<---->{1}", c1, c2);
//方法重载时,如果有更精确的匹配,会覆盖掉泛型方法
p.Show(5, 'X');
//在类中没有具体的实现形式,只能使用泛型方法
p.Show('X', 5);
//泛型方法的重载
p.Show('Y');
p.Show(1, 6.8, 'K');
Console.ReadKey();
}
//泛型方法写成静态的
static void Change<T>(ref T a, ref T b)
{
T temp;
temp = a;
a = b;
b = temp;
Console.WriteLine("我是静态泛型方法.");
}
//重载泛型方法的样式一:一个参数
void Show<T>(T t)
{
Console.WriteLine("我是泛型方法的重载形式{0}", t);
}
//重载泛型方法的样式二:两个参数
void Show<T,U>(T t, U u)
{
Console.WriteLine("我是泛型方法的重载形式{0}---{1}", t, u);
}
//一个具体的方法的定义
void Show(int t, char u)
{
Console.WriteLine("我是一般方法调用{0}---{1}", t, u);
}
//重载泛型方法的样式三:三个参数
void Show<T,U,V>(T t,U u,V v)
{
Console.WriteLine("我是泛型方法的重载形式{0},{1},{2}", t,u,v);
}
}
}
程序的运行结果如图10-19所示:
图10-19
10.4.3泛型类
泛型类封装的是一些不针对具体数据类型的操作。泛型类最常用于集合,如链接列表、哈希表、堆栈、队列、树等,其中,像从集合中添加和移除项这样的操作都以大体上相同的方式执行,与所存储数据的类型无关。
泛型类的写法如下
class 类名<类型参数列表>
{
……//具体实现
……
}
例子GenericsClass具体实现了一个泛型类GenericsClass。
namespace GenericsClass
{
class GenericsClass<T>
{
private static int ObjCount = 0;
public GenericsClass()
{
ObjCount++;
}
public int GetCount()
{
return ObjCount;
}
}
}
为了测试代码,先添加一个Point类
namespace GenericsClass
{
class Point
{
public int X { get; set; }
public int Y { get; set; }
}
}
测试代码
namespace GenericsClass
{
class Program
{
static void Main(string[] args)
{
GenericsClass<int> gci1 = new GenericsClass<int>();
GenericsClass<int> gci2 = new GenericsClass<int>();
GenericsClass<double> gcd = new GenericsClass<double>();
GenericsClass<Point> gcp1 = new GenericsClass<Point>();
GenericsClass<Point> gcp2 = new GenericsClass<Point>();
Console.WriteLine(gci1.GetCount());
Console.WriteLine(gci2.GetCount());
Console.WriteLine(gcd.GetCount());
Console.WriteLine(gcp1.GetCount());
Console.WriteLine(gcp2.GetCount());
Console.ReadKey();
}
}
}
程序的运行结果
形如GenericsClass<T>这样的类,一般被称为开放式构造类,被指定具体类型参数的类GenericsClass<int>一般被称为封闭式构造类,使用传统方式写的类一般又称为具体的类。
C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,可以这样认为.泛型类的不同的封闭类是分别不同的数据类型。例:GenericsClass<int>和GenericsClass<Point>是两个完全没有任何关系的类,你可以把它们看成类A和类B。每个不同的类的静态变量也是完全独立的,所以例子GenericsClass中GenericsClass<int>的静态变量显示的结果是2,而GenericsClass<double>显示的结果是1。
如果你创建一个泛型数据结构或类,像例中的GenericsClass<T>,注意其中并没有约束你该使用什么类型来建立类型参数。然而,这可能会带来一些限制。如,你不能在类型参数的实例中使用像==,!=或<等运算符,如:
if (obj1 == obj2) …
像==和!=这样的运算符的实现对于值类型和引用类型都是不同的。如果随意地允许之,代码的行为可能很出乎你的意料。
另外一种限制是缺省构造器的使用。例如,如果你使用的类型参数可能是一个类,但是如果编码像new T(),会出现一个编译错,因为并非所有的类都有一个无参数的构造器。还有一种情况,你可能希望在泛型类中使用类型参数T提供的一些方法,可是现在泛型类T并不知道将会是什么类,所以目前你只能使用Object类的方法,而不能使用一些特定类提供的方法。
所以在定义泛型类时,可以对用户代码能够在实例化类时用于类型参数的类型种类施加限制。如果用户代码尝试使用某个限制所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束。约束是使用 where 上下文关键字指定的。
限制的写法格式是
class 类名<类型化参数>where 类型化参数 :约束
表10-9列出了6中约束,并给出相应的解释说明。
表10-9
书写格式 | 解释说明 |
where T : struct | 类型参数实例化时必须是一种值类型 |
where T : class | 类型参数实例化时是一种引用类型 |
where T : new() | 类型必须有一个无参数的构造器 |
where T : 类名 | 类型可以是类名指定的类或者是它的一个子类 |
where T : 接口名 | 类型必须实现指定的接口 |
T:U | 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。这称为裸类型约束 |
可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型,如下所示:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
因为c#中的单继承性,所以约束列表中只能出现一个类,但是可以有多个接口。如果有多个约束的情况下,class或struct必须位于列表的开头。如果有new()约束与其他多个约束同时存在的情况,那么new()约束必须写在最后。
通过约束类型参数,可以增加约束类型及其继承层次结构中的所有类型所支持的允许操作和方法调用的数量。因此,在设计泛型类或方法时,如果要对泛型成员执行除简单赋值之外的任何操作或调用System.Object不支持的任何方法,您将需要对该类型参数应用约束。
例子StyleLimit演示了new()的限制
namespace StyleLimit
{
class Point
{
private int x, y;
public Point()
{
Console.WriteLine("我有无参数的构造函数");
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
//对泛型类的类型参数进行class与new()操作的限制
class classnew<T> where T : new()
{
public void GetNewObject()
{
T newpoint = new T();
}
}
class Program
{
static void Main(string[] args)
{
classnew<Point> PointClass = new classnew<Point>();
PointClass.GetNewObject();
Console.ReadKey();
}
}
}
运行程序,将对打印出“我有无参数的构造函数”这句话。如果将Point中的无参数构造函数注释掉。编译将会得到“StyleLimit.Point”必须是具有公共的无参数构造函数的非抽象类型的一个错误提示。
在应用 where T : class 约束时,建议不要对类型参数使用 == 、!=、>、<等运算符,因为这些运算符仅测试引用同一性而不测试值相等性。即使在用作参数的类型中重载这些运算符也是如此。下面的代码说明了这一点;即使String类重载 == 运算符,输出也为 false。
public static void OpTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s = = t);
}
static void Main()
{
string s1 = "foo";
System.Text.StringBuilder sb = new System.Text.StringBuilder("foo");
string s2 = sb.ToString();
OpTest<string>(s1, s2);
}
这种情况的原因在于,编译器在编译时仅知道 T 是引用类型,因此必须使用对所有引用类型都有效的默认运算符。如果需要测试值相等性,建议的方法是同时应用 where T : IComparable<T> 约束,并在将用于构造泛型类的任何类中实现该接口。
泛型类可以从具体的、封闭式构造或开放式构造基类继承:
class BaseClass { }//具体的类
class BaseClassGeneric<T> { }//开放式构造的类
下面的继承都是允许的
//从具体类继承
class DerivedClass<T> : BaseClass { }
//从封闭式构造类中继承
class DerivedClass <T> : BaseClassGeneric <int> { }
//从开放式构造类中继承
class DerivedClass <T> : BaseClassGeneric <T> { }
但是
class DerivedClass <T> : BaseClassGeneric <Y> { }是无效的,因为派生的类在具体指定类型参数T时,没有办法具体指定父类中的类型参数Y,但是class DerivedClass <T,Y> : BaseClassGeneric <Y> { }又是有效的。也就是说,从开放式构造类型继承的泛型类必须为基类中未被继承类共享的类型参数提供类型变量,
class BaseClassGeneric <T, U> { }
//正确
class DerivedClass<T> : BaseClassGeneric <T, int> { }
//正确
class DerivedClass<T, U> : BaseClassGeneric <T, U> { }
//错误
//class DerivedClass <T> : BaseClassGeneric <T, U> {}
从开放式构造类型继承的泛型类必须指定约束,这些约束是基类型约束的超集或暗示基类型约束。
具体类可以从封闭式构造基类继承,但无法从开放式构造类或裸类型参数继承,因为在运行时客户端代码无法提供实例化基类所需的类型变量。
//正确
class DerivedClass: BaseClassGeneric <int> { }
//错误
class DerivedClass: BaseClassGeneric <T> {}
//错误
//class DerivedClass: T {}
在泛型类中,非泛型方法可以访问类级别的类型参数,如下所示:
class SampleClass<T>
{
void Swap(ref T lhs, ref T rhs) { }
}
在泛型类中也可以包含泛型方法,但是如果在一个泛型类中包含的泛型方法的类型参数与泛型类中的类型参数相同,编译器将生成警告,因为在泛型方法的内部,该泛型方法的类型参数的具体类型将会屏蔽泛型类的类型参数的类型。这个与一般变量作用域的原理基本相同,局部变量将会截断比其高一级的变量作用的范围,一旦超出局部变量的作用域范围,比其高一级的变量的作用域继续有效。除了类初始化时提供的类型参数之外,如果需要灵活调用具有类型参数的泛型方法,请考虑为方法的类型参数提供其他标识符。
例子GenClsIncludeGenMe演示了这种情况。
namespace GenClsIncludeGenMe
{
//定义一个泛型类,里面包含一个泛型方法。
class GenClass<T>
{
//泛型方法,具有泛型类相同的类型参数
public void GenMethod<T>(T t)
{
Console.WriteLine(t.GetType());
}
}
class Program
{
static void Main(string[] args)
{
GenClass<int> gc = new GenClass<int>();
int t1 = 6;
double t2 = 8.6;
gc.GenMethod<double>(t1);
gc.GenMethod<double>(t2);
Console.ReadKey();
}
}
}
打印输出的结果是
System.Double
System.Double
在类GenClass<int>中t1为int型的变量,也就是泛型类中T的具体类型,可是因为泛型方法GenMethod的类型参数指定的是double,在泛型方法内部将所有具有类型T的变量全部转换为double,而int类型刚好可以转换为double类型,于是打印出结果。如果使用了一个不能转换为泛型方法指定的类型变量时,程序将会提示出错。
所以合理的写法是
class GenericClass<T>
{
//比较合理
void SampleMethod<U>() { }
}
当使用泛型时,要小心可代替性的情况。现在假设实现了一个列表的泛型类List<T>,在实例化泛型类的时候实例化了两个封闭的List<BaseClass>与List< DerivedClass>,其中DerivedClass是BaseClass的子类,尽管DerivedClass与BaseClass有关系,那么类List<BaseClass>与List< DerivedClass>应该是两个不同的类,好像DerivedClass与BaseClass没有关系一样,是两个完全独立的类。例子FruitAndApple就演示了这种关系。
namespace FruitAndApple
{
//定义了一个水果类
class Fruit
{
}
//定义了一个苹果类,从水果类中继承
class Appale : Fruit
{
}
//定义了一个香蕉类,从水果类中继承
class Banana : Fruit
{
}
//定义了一个列表的泛型类
class List<T>
{
//实现向链表中添加的功能
public void add(T t)
{
Console.WriteLine("增加的类型是{0}",t.GetType());
}
}
//定义了一个猴子类,其中有一个吃水果的方法
class Monkey
{
//吃水果的方法,方法的参数是List<Fruit>类型
public void EatFruit(List<Fruit> fl)
{
Console.WriteLine("水果的列表的类型是{0}", fl.GetType());
}
}
class Program
{
static void Main(string[] args)
{
List<Fruit> fruitL = new List<Fruit>();
List<Appale> appleL = new List<Appale>();
fruitL.add(new Fruit());//合法
fruitL.add(new Appale());//合法
fruitL.add(new Banana());//合法
appleL.add(new Appale());//合法
Monkey m1 = new Monkey();
m1.EatFruit(fruitL);//合法
m1.EatFruit(appleL);//非法
Console.ReadKey();
}
}
}
例子中类之间的关系增加了这种情况的迷惑性。苹果与香蕉都属于水果,现在有两个桶,一个桶A是水果桶,可以装任何水果,包括苹果、香蕉。一个桶B是苹果桶,只能装苹果。现在有一个猴子,它只能吃水果桶A(public void EatFruit(List<Fruit> fl)),虽然这个桶里可以放任何的水果,但是这个桶A与桶B还是两个完全不同的桶。不能因为桶A里面可以放苹果就认为猴子也可以吃放苹果的桶B。可替代性具有迷惑性的关键之处就是水果与苹果的关系。这种关系让很多人认为放置水果的桶与放置苹果的桶也有关系,这个是不对的。
如果注释掉m1.EatFruit(appleL),程序的运行结果是
增加的类型是FruitAndApple.Fruit
增加的类型是FruitAndApple.Appale
增加的类型是FruitAndApple.Banana
增加的类型是FruitAndApple.Appale
水果的列表的类型是FruitAndApple.List`1[FruitAndApple.Fruit]