1.1 理解泛型
1.1.1 为什么要有泛型?
我想不论大家通过什么方式进入了计算机程序设计这个行业,都免不了要面对数据结构和算法这个话题。因为它是计算机科学的一门基础学科,往往越是底层的部分,对于数据结构或者算法的时间效率和空间效率的要求就越高。比如说,当你在一个集合类型(例如ArrayList)的实例上调用Sort()方法对它进行排序时,.Net框架在底层就应用了快速排序算法。.Net框架中快速排序方法名称叫QuickSort(),它位于Array类型中,这可以通过Reflector.exe工具查看到。
我们现在并不是要讨论这个QuickSort()实现的好不好,效率高还是不高,这偏离了我们的主题。但是我想请大家思考一个问题:如果由你来实现一个排序算法,你会怎么做?好吧,我们把题目限定得再窄一些,我们来实现一个最简单的冒泡排序(Bubble Sort)算法,如果你没有使用泛型的经验,我猜测你可能会毫不犹豫地写出下面的代码来,因为这是大学教程的标准实现:
public class SortHelper{
public void BubbleSort(int[] array) {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 对两个元素进行交换
if (array[j] < array[j - 1] ) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
对冒泡排序不熟悉的读者,可以放心地忽略上面代码的方法体,它不会对你理解泛型造成丝毫的障碍,你只要知道它所实现的功能就可以了:将一个数组的元素按照从小到大的顺序重新排列。我们对这个程序进行一个小小地测试:
class Program {
static void Main(string[] args) {
SortHelper sorter = new SortHelper();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
foreach(int i in array){
Console.Write("{0} ", i);
}
Console.WriteLine();
Console.ReadKey();
}
}
输出为:
1 3 4 7 8
我们发现它工作良好,欣喜地认为这便是最好的解决方案了。直到不久之后,我们需要对一个byte类型的数组进行排序,而我们上面的排序算法只能接受一个int类型的数组,尽管我们知道它们是完全兼容的,因为byte类型是int类型的一个子集,但C#是一个强类型的语言,我们无法在一个接受int数组类型的地方传入一个byte数组。好吧,没有关系,现在看来唯一的办法就是将代码复制一遍,然后将方法的签名改一个改了:
public class SortHelper {
public void BubbleSort(int[] array) {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 对两个元素进行交换
if (array[j] < array[j - 1]) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
public void BubbleSort(byte[] array) {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 对两个元素进行交换
if (array[j] < array[j - 1]) {
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}
OK,我们再一次解决了问题,尽管总觉得哪里有点别扭,但是这段代码已经能够工作,按照敏捷软件开发的思想,不要过早地进行抽象和应对变化,当变化第一次出现时,使用最快的方法解决它,当变化第二次出现时,再进行更好的构架和设计。这样做的目的是为了避免过度设计,因为很有可能第二次变化永远也不会出现,而你却花费了大量的时间精力制造了一个永远也用不到的“完美设计”。这很像一个谚语,“fool me once,shame on you. fool me twice, shame on me.”,翻译过来的意思是“愚弄我一次,是你坏;愚弄我两次,是我蠢”。
美好的事情总是很难长久,我们很快需要对一个char类型的数组进行排序,我们当然可以仿照byte类型数组的作法,继续采用复制粘贴大法,然后修改一下方法的签名。但是很遗憾,我们不想让它愚弄我们两次,因为谁也不想证明自己很蠢,所以现在是时候思考一个更佳的解决方案了。
我们仔细地对比这两个方法,会发现这两个方法的实现完全一样,除了方法的签名不同以外,没有任何的区别。如果你曾经开发过Web站点程序,会知道对于一些浏览量非常大的站点,为了避免服务器负担过重,通常会采用静态页面生成的方式,因为使用Url重写仍要要耗费大量的服务器资源,但是生成为html静态网页后,服务器仅仅是返回客户端请求的文件,能够极大的减轻服务器负担。
在Web上实现过静态页面生成时,有一种常用的方法,就是模板生成法,它的具体作法是:每次生成静态页面时,先加载模板,模板中含有一些用特殊字符标记的占位符,然后我们从数据库读取数据,使用读出的数据将模板中的占位符替换掉,最后将模板按照一定的命名规则在服务器上保存成静态的html文件。
我们发现这里的情况是类似的,我来对它进行一个类比:我们将上面的方法体视为一个模板,将它的方法签名视为一个占位符,因为它是一个占位符,所以它可以代表任何的类型,这和静态页面生成时模板的占位符可以用来代表来自数据库中的任何数据道理是一样的。接下来就是定义占位符了,我们再来审视一下这三个方法的签名:
public void BubbleSort(int[] array)
public void BubbleSort(byte[] array)
public void BubbleSort(char[] array)
会发现定义占位符的最好方式就是将int[]、byte[]、char[]用占位符替代掉,我们管这个占位符用T[]来表示,其中T可以代表任何类型,这样就屏蔽了三个方法签名的差异:
public void BubbleSort(T[] array) {
int length = array.Length;
for (int i = 0; i <= length - 2; i++) {
for (int j = length - 1; j >= 1; j--) {
// 对两个元素进行交换
if (array[j] < array[j - 1]) {
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
现在看起来清爽多了,但是我们又发现了一个问题:当我们定义一个类,而这个类需要引用它本身以外的其他类型时,我们可以定义有参数的构造函数,然后将它需要的参数从构造函数传进来。但是在上面,我们的参数T本身就是一个类型(类似于int、byte、char,而不是类型的实例,比如1和'a')。很显然我们无法在构造函数中传递这个T类型的数组,因为参数都是出现在类型实例的位置,而T是类型本身,它的位置不对。比如下面是通常的构造函数:
public SortHelper(类型 类型实例名称);
而我们期望的构造函数函数是:
public SortHelper(类型);
此时就需要使用一种特殊的语法来传递这个T占位符,不如我们定义这样一种语法来传递吧:
public class SortHelper<T> {
public void BubbleSort(T[] array){
// 方法实现体
}
}
我们在类名称的后面加了一个尖括号,使用这个尖括号来传递我们的占位符,也就是类型参数。接下来,我们来看看如何来使用它,当我们需要为一个int类型的数组排序时:
SortHelper<int> sorter = new SortHelper<int>();
int[] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
当我们需要为一个byte类型的数组排序时:
SortHelper<byte> sorter = new SortHelper<byte>();
byte [] array = { 8, 1, 4, 7, 3 };
sorter.BubbleSort(array);
相信你已经发觉,其实上面所做的一切实现了一个泛型类。这是泛型的一个最典型的应用,可以看到,通过使用泛型,我们极大地减少了重复代码,使我们的程序更加清爽,泛型类就类似于一个模板,可以在需要时为这个模板传入任何我们需要的类型。
我们现在更专业一些,为这一节的占位符起一个正式的名称,在.Net中,它叫做类型参数 (Type Parameter),下面一小节,我们将学习类型参数约束。
|