5.2 泛型的解决方案
5.2.1 泛型类的编写
为了说明泛型类的优势,让我们来看一个经典的代码:
案例操作020503:编写泛型类
l 代码片段一:交换数据
public class MyClass
{
public void Swap(ref object obj1, ref object obj2)
{
object tmp = obj1;
obj1 = obj2;
obj2 = tmp;
}
}
上面的代码是想作一个通用的,用于交换两个变量的类,由于不可能为每一种数据类型都去重载一次Swap方法,因此,方法中的参数使用了Object类型。但是问题在于如何使用该Swap方法,如果,在一个大规模的整数排序程序中使用该Swap方法,此时又会出现大量装箱及拆箱操作,速度又会受到影响。另外,由于出现了赋值操作,类型转换的安全性也得不到很好的保障。下面我们利用泛型类来对该类进行改进。
l 代码片段二:编写泛型类
public class GenericTest
{
public void Swap(ref T t1, ref T t2)
{
T tmp = t1;
t1 = t2;
t2 = tmp;
}
}
从上面的代码中可以看出一个基本的泛型编写的方式,在类名后面加上“ ” ,这里的T被称为“类型参数”,它是一个已知类,但是直到使用该类之前,还不知道它应该是哪一种具体的类型。在编写该的实例时,我们去指定T是什么类型。
定义完“T”之后,就可以在类中去应用T类型。在Swap方法中,由于不知道使用会交换哪种数据类型,因此,Swap方法中的参数也是T类型的。
下面,我们来看一下如何使用这个泛型类:
示例的窗口如下图所示:
l 代码片段三:使用泛型类
private void button1_Click(object sender, EventArgs e)
{
int a = int.Parse(this.textBox1 .Text );
int b = int.Parse(this.textBox2 .Text );
string s1 = this.textBox3.Text;
string s2 = this.textBox4.Text;
//指定类型参数为int
GenericTest gt1 = new GenericTest< int>();
gt1.Swap(ref a, ref b);
this.textBox1.Text = a.ToString();
this.textBox2.Text = b.ToString();
//指定类型参数为string
GenericTest gt2 = new GenericTest ();
gt2.Swap(ref s1,ref s2);
this.textBox3.Text = s1;
this.textBox4.Text = s2;
}
在上面的代码片段中,可以看出,使用泛型类是比较简单的,只需要在定义的时候指定类型参数“T”是什么类型即可,如下面两行代码,分别将类型参数T指定为int以及string类型。
GenericTest gt1 = new GenericTest ();
GenericTest gt2 = new GenericTest ();
如果指定了类型参数对应的真正类型,那么该泛型类中的所有“T”,都将被替换成对应的真正类型。比如,gt1是将类型参数指定为int,那么GenericTest中的所有“T”都会被替换成为int;而gt2是将类型参数指定为string,那么GenericTest中的所有“T”都会被替换成为string。此时,Swap方法的对应的参数类型也会随着类型参数“T”的不同而发生改变,如下图所示:
通过IDE给我的智能提示,用户们可以发现,虽然利用泛型写的是通用类,但是,由于指定了类型参数,使得在具体调用Swap方法的时候,参数的类型是确定的,这一点和多态是相反,泛型中的通用,追求的在编码的时候确定类型,而多态中的通用,追求的是在运行的时候确定类型。
关于完整的泛型类编写的案例代码,请参考:
案例操作020503:编写泛型类
5.2.2 泛型类的约束
l 类型默认值
在编写泛型类的时候,我们会采用以下形式
public class MyClass
{//do something}
这里的“T”,我们强调过,它是一种已知类型,但是却不能当做任何一种具体的类型来使用。见下面代码:
public class MyClass
{
public void MM(T t1)
{
if(t1==null)
{
t1=0;
}
}
}
在上面的代码中,判断t1是否为null,以及给t1赋值为0,都是不正确的作法,因为类型参数“T”,目前还不知道是具体的哪一种类,而只有引用类型(Reference Type)可以为null,另外,也只有一部分的值类型(Value Type)如int,long,才可以赋值为0。
因此,这里再次强调,泛型类通常用于编写一些泛化的操作,如果想执行更高级且功能更强的操作,请使用泛型+反射技术。另外,所有的类者是Object类的派生类,因此,我们在T类型的实例中,唯一能使用的操作就是已经在Object类中定义好的内容,比如,我们可调用t1.ToString()。
此外,如果想取得T类型的默认值,请使用default操作符。如下面代码所示:
T t1=default(T);
l 泛型类中方法签名的二义性(Ambiguity)
所谓方法签名,就是指类中方法的原型。由于泛型类中方法的参数可以是类型参数,而该类型参数在真正使用的时候可能是任何类型,这样在使用的时候就有可能产生方法调用的二义性(ambiguity)。
查看下面的代码片段:
class MyClass
{
public string GetStringValue(T t)
{
return t.ToString();
}
public string GetStringValue(int i)
{
return i.ToString();
}
}
private void button1_Click(object sender, EventArgs e)
{
MyClass mc = new MyClass ();
string value = mc.GetStringValue(1);
}
对于一个泛型类来讲,方法也是可以重载的,比如上面的代码中,就存在着GetStringValue方法的两种形式,第一种形式采用了类型参数“T”作为参数类型,第二种形式,是采用了int类参数类型。在我们使用该类的时候,如果指定了类型参数的真正类型为int,这样就会产生歧义(两个GetStringValue方法中的参数类型都是整型!)。那么当使用者调用了GetStringValue方法,并且传递了一个整型变量给该方法时,到底调用的是哪一个版本的GetValueString呢?是GetStringValue (int),还是GetStringValue(int)?
针对于上面的问题,只要把握住一个原则即可,即:只要在原型中存在与之对应的参数类型,那么就按存在对应类型参数的方法调用,如果参数的类型不存在,则调用存在泛型参数的方法。多类型参数的泛型方法操作方式也相同。
在上面代码中,实际上调用的是:
public string GetStringValue(int i)
{
return i.ToString();
}
l 类型参数的条件约束
在很多情况下,类型参数的具体类型是有条件的。比如,要利用泛型作一个可以进行排序的链表,如MyList , 在使用该类时T可以被替换成所需类。但既然是一个可排序的链表,那么就要求用于替换T的类型一定是可以比较的类型。这时我们又应该怎么样处理呢?
我们知道在Sql语句中,可以使用Where子句来设置条件,实际上,在我们也可以利用Where语句对类型参数“T”进行约束。
如上面提到的可排序的泛型链表,就可以写成以下形式:
public class MyList where T:IComparable
{
public int CompareTo(object obj)
{
//….
}
}
这样,所有想利用该链表进行排序的数据,就一定要继承自IComparable接口,否则会出现编译错误!
下表介绍了类型参数“T”可用的约束类型
约束 | 说明 |
where T:MyClass | 该约束指定,T一定要继承自MyClass类型,MyClass可以是泛型类。 |
where T:class | 该约束指定,T一定是一个类(引用类型)。 |
where T:MyStruct | 该约束指定,T一定要继承自MyStruct结构体。 |
where T:struct | 该约束指定,T一定是一个结构体(值类型)。 |
where T:IMyInterface | 该约束指定,T一定要继承自IMyInterface接口,该接口可以是泛型接口。 |
where T:new() | 该约束指定,T一定要有一个默认构造函数。 |
where T:U | 该约束指定,T一定要继承自另外一个类型参数U。 |
5.2.3 泛型类中的静态成员
我们知道,C#中是没有全局变量的,通常情况下,我们可以使用类中的静态字段来实现全局变量的功能,但是这样的一种想法,在泛型中是有所变化的。
泛型类中,静态成员的版本数量并不唯一,其数量与该类所指定的类型参数的真正类型的数量是一样的。
下面的代码中就展示了一个泛型类MyClass ,其中就包括了一个静态成员i:
public class MyClass
{
public static int i;
}
泛型类的静态成员使用方式如下:
MyClass .i=100;
MyClass .i=10;
在泛型类中,要想使用静态成员,就一定要先指定类型参数,上面代码中指定了两个类型参数:string,int。
此时在内存中,静态成员i就存在了两个版本,所以指定了类型参数T为string类型的MyClass中共享一个i,同样,所以指定了类型参数T为int类型的MyClass中共享另外一个i。
5.2.4 泛型方法
在上一节,泛型类中的静态成员的表现可能会让很多开发人员感觉到失望,因为在泛型类中,静态成员已经失去了全局变量的意义。那么,有没有这样的一种结构,既能够使用泛的功能,还能使用静态成员的全局变量的特性呢?
我们可以降低泛型的使用范围,即:在方法中使用泛型。
l 泛型方法的编写
泛型方法,相对于泛型类来讲,要简单一些,下面代码展示了泛型方法的编写过程:
public class MyClass
{
public static int i;
public void Swap (ref T t1,ref T t2)
{
T tmp = t1;
t1 = t2;
t2 = tmp;
}
}
该类中的Swap方法就是一个泛型方法,只要在方法名的后面指明类型参数“T”即可。这样方法可以是泛型的,而静态成员又是全局的。
l 泛型方法的使用
泛型方法的编写很简单,那么泛型方法又应该如何使用呢?
泛型方法的使用可以分为两种形式:1.显示声明类型参数的真正类型。2.隐匿声明类型参数的真正类型(由编译器自动推断类型)。
详细使用过程见下面代码:
class MyClass
{
public static int i;
public void Swap (ref T t1, ref T t2)
{
T tmp = t1;
t1 = t2;
t2 = tmp;
}
}
private void button1_Click(object sender, EventArgs e)
{
MyClass mc = new MyClass();
string s1 = "hello";
string s2 = "world";
int a = 10;
int b = 20;
//显示声明类型参数的真正类型
mc.Swap (ref a,ref b);
mc.Swap (ref s1, ref s2);
//由编译器自动推断类型
mc.Swap(ref a,ref b);
mc.Swap(ref s1,ref s2);
}
5.2.5 泛型结构体
结构体,由于其轻便快速的特点,受到编码人员的喜爱,特别是在作一些简单的存储结构时,结构体是一个比较好的选择。在.NET Framework2.0中,开发人员同样可以编写泛型结构体。代码如下所示:
public struct MyStruct
{
public T Name;
public T Age;
}