您的位置:首页 > 编程语言 > C#

C#中的泛型

2016-01-17 22:02 323 查看

C#中的泛型

 

前言

现在的.NET版本是5.0了,但是在1.1的时候,最受诟病的一个缺陷就是没有对泛型的支持.你想想,对于一种强类型的语言来说,我写了一个针对整数的排序,但是现在又需要对字符串排序,然又需要对double排序等等,强类型语言又不像弱类型语言一样所有的类型都使用一个关键字定义就行,比如JS就都是使用var来定义变量.所以说,强类型语言如果没有反省的话确实是一种灾难!

 

正文

先来实现一个简单的冒泡排序:

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;
}
}
}
}


如果你不是很了解冒牌排序没关系,这并不影响你对泛型的理解,你只要知道他所实现的功能即可:将一个数组的元素按照从小到大的顺序重新排列.现在对这个程序进行一个简单的测试:



Program p = new Program();
int[] array = { 5, 2, 6, 7, 8, 1 };
p.BubbleSort(array);
foreach (int item in array)
{
Console.WriteLine(item);
}


运行良好!不就之后,你发现需要对一个byte类型的数组进行排序,而上面的排序算法只能接受一个int类型的数组,尽管大家知道byte类型所包含的数值范围是int类型的一个子集,但C#是一个强类型的语言,无法再一个接受int数组类型的地方传入一个byte数组没关系,咱们通过最简单的办法,将代码复制一遍,然后改一下方法的签名:

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])
{
byte temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}

 

完美!但是完美的事情不长久,现在又需要对一个char类型的数组进行排序,你是不是向干掉那个随便修改方案的人?但是确实出现了这种需求!怎么办?你会发现这几个方法有着惊人的相似.如果你曾经开发过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是type的缩写),其中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呢?

 

当定义一个类,并且这个类需要引用它本身以外的其他类型时,可以定义带参数的构造函数,然后将它需要的类型实例通过构造函数传递进来.但是在上面,参数T本身就是一个类型(类似于int,byte,char,而不是类型的实例,比如1和’a’).很显然无法在构造函数中传递T类型的数组,因为参数接受的是类型的实例,而T是类型本身.怎么办?

 

此时就需要使用一种特殊的语法来传递这个T占位符,.NET提供了专门的语法来传递T占位符,其中一种就是在定义类型的时候传递,此时,类型也就变成了泛型类.

public class SortHelper<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;
}
}
}
}
}

在类型名称的后面加一个尖括号,使用这个尖括号来传递占位符,也就是类型参数.接下来,看一下如何使用泛型.当需要为一个int类型的数组排序时:
int[] array = { 5, 2, 6, 7, 8, 1 };
SortHelper<int> sorter = new SortHelper<int>();
sorter.BubbleSort(array);


代码有错误?没关系,别急,一步一步来.可以看到,通过使用泛型,大大的减少了代码量,程序简洁多了,泛型就类似于一个模版,可以在需要时为这个模版传入任何需要的类型.现在咱们统一一下口供,在.NET中,占位符叫做类型参数(Type Parameter).

 

说明一下:并不一定要使用字符T作为类型参数的名称,也可以使用其他的字符,单习惯上使用T.

 

类型参数约束

实际上,如果运行一下上面的代码就会发现连编译都不能通过,为啥呢?考虑这样一个问题:假设我们自定义一个类型,名字叫做Book,它包含两个字段:一个是int类型的Price,代表书的价格,代表书的价格;一个是string类型的Title,代表书的标签:

public class Book
{
private int price;

public int Price
{
get { return price; }

}
private string title;

public string Title
{
get { return title; }

}
public Book(){}
public Book(int price,string title)
{
this.price = price;
this.title = title;
}
}


现在,创建一个Book类型的数组,然后试着使用上面定义的泛型类来对它进行排序,代码或许应该像下面这样:

Book[] bookArray = new Book[2];
Book book1 = new Book(30,"HTML5解析");
Book book2 = new Book(21, "JavaScript实战");
bookArray[0] = book1;
bookArray[1] = book2;
SortHelper<Book> sorter = new SortHelper<Book>();
sorter.BubbleSort(bookArray);
foreach (Book b in bookArray)
{
Console.WriteLine("Price: ",b.Price);
Console.WriteLine("Title: ",b.Title);
}


如果你还没有发现上面的代码有什么问题,那么看的仔细一些,再回顾SortHelper类的BubbleSort()方法的实现:

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;
}
}
}
}


尽管很不愿意,但是问题还是出现了,既然是排序,就免不了要比较大小,大家可以看到两个元素交换时进行了大小的比较,那么现在请问:book1和book2谁比较大?张三可能说book1大,因为book1的Price是30;李四可能说book2大,因为book2的Title是以”J”开头的,”J”排在”H”后面.但是程序无法判断了,程序迷茫了,他不知道是按照张三的标准还是按照李四的标准.这个时候就需要定义一个比较规则.

 

在.NET中,实现比较的基本方法是实现IComparable接口,它有泛型和非泛型两个版本,因为现在正在讲解泛型,而可能你还没有彻底的领悟它,所以简单起见,采用它的非泛型版本.

 

假设Book类型已经实现了这个接口,当像下面这样调用时:

book1.CompareTo(book2);
如果book1比book2小,返回一个小于0 的整数;如果book1和book2相等,返回0;如果book1比book2大,返回一个大于0 的正数.

 

接下来让Book类实现IComparable接口,此时又会面对排序标准的问题,说通俗一点,就是按照张三的标准还是按照李四的标准.这里咱们暂时按照张三的标准,希望李四的粉丝不要生气.

修改Book类如下:

public int CompareTo(object obj)
{
Book book2=(Book)obj;
return this.Price.CompareTo(book2.Price);
}

需要注意的是并没有在CompareTo()方法中直接去比较当前的Book的Price与传递进来的Book的Price,而是通过在Price上调用CompareTo()方法,再次将对它们的比较委托给了int类型.这是因为int类型也实现了IComparable接口.

 

顺便一提,大家有没有发现上面的代码存在一个问题?因为这个CompareTo()方法是一个很通用的方法,为了办证所有的类型都能使用这个节口,所以他的参数接受了一个object类型的参数.因为,为了获得Book类型,需要在方法内部进行一个乡下的强制转换.如果熟悉面向对象编程,那么应该能想到这违反了里氏替换原则.这个原则要求方法内部不应该对方法所接受的参数进行乡下的强制转换.为啥?定义集成体系的目的就是为了代用通用,让基类实现通用的职责,而让子类实现其本身的职责,当定义一个接受基类的方法时,设计本身是优良的,但是当在方法内部进行强制转换时,就破坏了这个继承体系,因为尽管方法的签名是面向接口编程,方法的内部还是面向实现编程的.

 

既然Book类已经实现了IComparable接口,那么泛型类应该可以工作了吧?不行的...还要记得:泛型类是一个模板类,它对在执行时传递的类型参数是一无所知的,也不会做任何猜测.尽管作为开发者,我们知道Book类现在实现了IComparable,对它进行比较是很容易的,但是SortHelper<T>泛型类并不知道,咋办?这就需要告诉SortHelper<T>类(准确说是告诉编译器),它所接受的T类型参数必须能够进行比较,换言之,也就是实现IComparable接口,这就引出了咱们下一个知识点:反省参数约束.

 

为了要求类型参数T必须实现IComparable接口,需要像下面这样重新定义SortHelper<T>:

 

public class SortHelper<T>where T:IComparable

上面的定义说明了类型参数T必须实现IComparable接口,否则将无法通过编译,从而保证了方法体可以正确的运行.因为现在T已经实现了IComparable,而数组array中的成员是T的实例,所以当在array[i]后面点击小数点”.”时,VS的只能提示将会显示出IComparable的成员,也就是CompareTo()方法.修改BubbleSort()方法,让它使用CompareTo()方法进行比较:

  public class SortHelper<T>where T:IComparable
{
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].CompareTo(array[j - 1])<0)
{
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}

此时再次运行上面定义的代码,终于能看出输出了!

 

除了可以约束类型参数T实现某个接口以外,还可以约束T是一个结构,T是一个类,T拥有构造函数,T继承自某个基类等,但将这些每一种用法都在这里说一遍实在是无意义,他们的概念是一样的,只是声明的语法有些差异,而这点差异,大家完全可以通过MSDN或者其他的渠道来解决.

 

泛型方法

 

再来考虑这样一个问题:加入有一个很复杂的类----SuperCalculator,包含了诸多的方法:

public class SuperCalculator
{
//很多方法
}


假设这个类对对算法的要求比较高,.NET框架内置的快速排序算法不满足要求了,所以打算重新实现一个更优秀的排序算法,并将实现算法的方法命名为SpeedSort().为了其他类型可以使用,按照前面说的内容,应当使用泛型来重构SuperCalculator类,代码可以这么写:

  public class SuperCalculator<T>where T:IComparable
{
//很多方法
}

说明一下

这里穿插讲述一个关于类型设计的问题:将SpeedSort()方法放在SuperCalculator中是不合适的.为啥呢?因为将它们的职责混淆了,SuperCalculator的意思是”超级计算器”,那么它包含的公开方法都应该是与计算相关的,而SpeedSort()出现在这里显得不伦不类,当发现一个方法的名称与类的关系不大时,就应该考虑将这个方法抽象出去,把它放置到一个新的类中,哪怕这个类只有它一个方法.

 

尽管现在SuperCalculator类确实可以完成所需的工作,但是它的使用却变得复杂了,为啥呢?因为SpeedSort()方法污染了它,仅仅为了能够使用SpeedSort()这一个方法,却不得不将类型参数T传递给SuperCalculator类,使得即使不需要调用SpeedSort()方法时,创建SuperCalculator实例也必须传递一个类型参数.

 

为了解决这个问题,我们很容易能想到:有没有办法把类型参数T加到方法上,而非整个类上,也就是降低T作用的范围.肯定行!这就是咱们这个知识点的讲解:泛型方法.类似的,只要修改一个SpeedSort()方法的签名就可以了,让它接受一个类型参数,此时SuperCalculator的定义如下:

  public class SuperCalculator
{
//很多方法
public void SpeedSort<T>(T[] array) where T : IComparable
{
int length = array.Length;
for (int i = 0; i <= length - 2; i++)
{
for (int j = length - 1; j >= 1; j--)
{
if (array[j].CompareTo(array[j - 1]) < 0)
{
T temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
}
}

接下来我们编写一点代码来对SpeedSort进行一个测试:

Book[] bookArray = new Book[2];
bookArray[0] = new Book(30,"HTML5");
bookArray[1] = new Book(21,"JavaScript");
SuperCalculator calculator = new SuperCalculator();
calculator.SpeedSort(bookArray);
foreach (Book item in bookArray)
{
Console.WriteLine("Price : {0}",item.Price);
Console.WriteLine("Title : {0}",item.Title);
}


没啥好说的,只是有一个地方不知道大家看出来了没有,编译器貌似能够推断出传递的数组类型,以及是否满足了泛型约束,所以,上面的SpeedSort()虽是一个泛型方法,但是在使用上和普通方法已经没啥区别了.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C# .net