出处:http://www.ondotnet.com/pub/a/dotnet/2004/05/17/liberty.htmlhtml
generics:泛型
type-safe:类型安全
collection: 集合
compiler:编译器
run time:程序运行时
object: 对象
.NET library:.Net类库
value type: 值类型
box: 装箱
unbox: 拆箱
implicity: 隐式
explicity: 显式
linked list: 线性链表
node: 结点
indexer: 索引器node
Visual C# 2.0 的一个最受期待的(或许也是最让人畏惧)的一个特性就是对于泛型的支持。这篇文章将告诉你泛型用来解决什么样的问题,以及如何使用它们来提升你的代码质量,还有你没必要恐惧泛型的缘由。安全
不少人以为泛型很难理解。我相信这是由于他们一般在了解泛型是用来解决什么问题以前,就被灌输了大量的理论和范例。结果就是你有了一个解决方案,可是却没有须要使用这个解决方案的问题。ide
这篇文章将尝试着改变这种学习流程,咱们将以一个简单的问题做为开始:泛型是用来作什么的?答案是:没有泛型,将会很难建立类型安全的集合。函数
C# 是一个类型安全的语言,类型安全容许编译器(可信赖地)捕获潜在的错误,而不是在程序运行时才发现(不可信赖地,每每发生在你将产品出售了之后!)。所以,在C#中,全部的变量都有一个定义了的类型;当你将一个对象赋值给那个变量的时候,编译器检查这个赋值是否正确,若是有问题,将会给出错误信息。性能
在 .Net 1.1 版本(2003)中,当你在使用集合时,这种类型安全就失效了。由.Net 类库提供的全部关于集合的类全是用来存储基类型(Object)的,而.Net中全部的一切都是由Object基类继承下来的,所以全部类型均可以放到一个集合中。因而,至关于根本就没有了类型检测。学习
更糟的是,每一次你从集合中取出一个Object,你都必须将它强制转换成正确的类型,这一转换将对性能形成影响,而且产生冗长的代码(若是你忘了进行转换,将会抛出异常)。更进一步地讲,若是你给集合中添加一个值类型(好比,一个整型变量),这个整型变量就被隐式地装箱了(再一次下降了性能),而当你从集合中取出它的时候,又会进行一次显式地拆箱(又一次性能的下降和类型转换)。测试
关于装箱、拆箱的更多内容,请访问 陷阱4,警戒隐式的装箱、拆箱。this
为了生动地感觉一下这些问题,咱们将建立一个尽量简单的线性链表。对于阅读本文的那些从未建立过线性链表的人。你能够将线性链表想像成有一条链子栓在一块儿的盒子(称做一个结点),每一个盒子里包含着一些数据 和 连接到这个链子上的下一个盒子的引用(固然,除了最后一个盒子,这个盒子对于下一个盒子的引用被设置成NULL)。htm
为了建立咱们的简单线性链表,咱们须要下面三个类:
一、Node 类,包含数据以及下一个Node的引用。
二、LinkedList 类,包含链表中的第一个Node,以及关于链表的任何附加信息。
三、测试程序,用于测试 LinkedList 类。
为了查看连接表如何运做,咱们添加Objects的两种类型到链表中:整型 和 Employee类型。你能够将Employee类型想象成一个包含关于公司中某一个员工全部信息的类。出于演示的目的,Employee类很是的简单。
public class Employee{
private string name;
public Employee (string name){
this.name = name;
}
public override string ToString(){
return this.name;
}
}
这个类仅包含一个表示员工名字的字符串类型,一个设置员工名字的构造函数,一个返回Employee名字的ToString()方法。
连接表自己是由不少的Node构成,这些Note,如上面所说,必须包含数据(整型 和 Employee)和链表中下一个Node的引用。
public class Node{
Object data;
Node next;
public Node(Object data){
this.data = data;
this.next = null;
}
public Object Data{
get { return this.data; }
set { data = value; }
}
public Node Next{
get { return this.next; }
set { this.next = value; }
}
}
注意构造函数将私有的数据成员设置成传递进来的对象,而且将 next 字段设置成null。
这个类还包括一个方法,Append,这个方法接受一个Node类型的参数,咱们将把传递进来的Node添加到列表中的最后位置。这过程是这样的:首先检测当前Node的next字段,看它是否是null。若是是,那么当前Node就是最后一个Node,咱们将当前Node的next属性指向传递进来的新结点,这样,咱们就把新Node插入到了链表的尾部。
若是当前Node的next字段不是null,说明当前node不是链表中的最后一个node。由于next字段的类型也是node,因此咱们调用next字段的Append方法(注:递归调用),再一次传递Node参数,这样继续下去,直到找到最后一个Node为止。
public void Append(Node newNode){
if ( this.next == null ){
this.next = newNode;
}else{
next.Append(newNode);
}
}
Node 类中的 ToString() 方法也被覆盖了,用于输出 data 中的值,而且调用下一个 Node 的 ToString()方法(译注:再一次递归调用)。
public override string ToString(){
string output = data.ToString();
if ( next != null ){
output += ", " + next.ToString();
}
return output;
}
这样,当你调用第一个Node的ToString()方法时,将打印出全部链表上Node的值。
LinkedList 类自己只包含对一个Node的引用,这个Node称做 HeadNode,是链表中的第一个Node,初始化为null。
public class LinkedList{
Node headNode = null;
}
LinkedList 类不须要构造函数(使用编译器建立的默认构造函数),可是咱们须要建立一个公共方法,Add(),这个方法把 data存储到线性链表中。这个方法首先检查headNode是否是null,若是是,它将使用data建立结点,并将这个结点做为headNode,若是不是null,它将建立一个新的包含data的结点,并调用headNode的Append方法,以下面的代码所示:
public void Add(Object data){
if ( headNode == null ){
headNode = new Node(data);
}else{
headNode.Append(new Node(data));
}
}
为了提供一点集合的感受,咱们为线性链表建立一个索引器。
public object this[ int index ]{
get{
int ctr = 0;
Node node = headNode;
while ( node != null && ctr <= index ){
if ( ctr == index ){
return node.Data;
}else{
node = node.Next;
}
ctr++;
}
return null;
}
}
最后,ToString()方法再一次被覆盖,用以调用headNode的ToString()方法。
public override string ToString(){
if ( this.headNode != null ){
return this.headNode.ToString();
}else{
return string.Empty;
}
}
咱们能够添加一些整型值到链表中进行测试:
public void Run(){
LinkedList ll = new LinkedList();
for ( int i = 0; i < 10; i ++ ){
ll.Add(i);
}
Console.WriteLine(ll);
Console.WriteLine(" Done. Adding employees...");
}
若是你对这段代码进行测试,它会如预计的那样工做:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...
然而,由于这是一个Object类型的集合,因此你一样能够将Employee类型添加到集合中。
ll.Add(new Employee("John"));
ll.Add(new Employee("Paul"));
ll.Add(new Employee("George"));
ll.Add(new Employee("Ringo"));
Console.WriteLine(ll);
Console.WriteLine(" Done.");
输出的结果证明了,整型值和Employee类型都被存储在了同一个集合中。
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Done. Adding employees...
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, John, Paul, George, Ringo
Done.
虽然看上去这样很方便,可是负面影响是,你失去了全部类型安全的特性。由于线性链表须要的是一个Object类型,每个添加到集合中的整型值都被隐式装箱了,如同 IL 代码所示:
IL_000c: box [mscorlib]System.Int32
IL_0011: callvirt instance void ObjectLinkedList.LinkedList::Add(object)
一样,若是上面所说,当你从你的列表中取出项目的时候,这些整型必须被显式地拆箱(强制转换成整型),Employee类型必须被强制转换成 Employee类型。
Console.WriteLine("The fourth integer is " + Convert.ToInt32(ll[3]));
Employee d = (Employee) ll[11];
Console.WriteLine("The second Employee is " + d);
这些问题的解决方案是建立一个类型安全的集合。一个 Employee 线性链表将不能接受 Object 类型;它只接受 Employee类的实例(或者继承自Employee类的实例)。这样将会是类型安全的,而且再也不须要类型转换。一个 整型的 线性链表,这个链表将再也不须要装箱和拆箱的操做(由于它只能接受整型值)。
做为示例,你将建立一个 EmployeeNode,该结点知道它的data的类型是Employee。
public class EmployeeNode {
Employee employeedata;
EmployeeNode employeeNext;
}
Append 方法如今接受一个 EmployeeNode 类型的参数。你一样须要建立一个新的 EmployeeLinkedList ,这个链表接受一个新的 EmployeeNode:
public class EmployeeLinkedList{
EmployeeNode headNode = null;
}
EmployeeLinkedList.Add()方法再也不接受一个 Object,而是接受一个Employee:
public void Add(Employee data){
if ( headNode == null ){
headNode = new EmployeeNode(data);}
else{
headNode.Append(new EmployeeNode(data));
}
}
相似的,索引器必须被修改为接受 EmployeeNode 类型,等等。这样确实解决了装箱、拆箱的问题,而且加入了类型安全的特性。你如今能够添加Employee(但不是整型)到你新的线性链表中了,而且当你从中取出Employee的时候,再也不须要类型转换了。
EmployeeLinkedList employees = new EmployeeLinkedList();
employees.Add(new Employee("Stephen King"));
employees.Add(new Employee("James Joyce"));
employees.Add(new Employee("William Faulkner"));
/* employees.Add(5); // try to add an integer - won't compile */
Console.WriteLine(employees);
Employee e = employees[1];
Console.WriteLine("The second Employee is " + e);
这样多好啊,当有一个整型试图隐式地转换到Employee类型时,代码甚至连编译器都不能经过!
但它很差的地方是:每次你须要建立一个类型安全的列表时,你都须要作不少的复制/粘贴 。一点也不够好,一点也没有代码重用。同时,若是你是这个类的做者,你甚至不能提早欲知这个连接列表所应该接受的类型是什么,因此,你不得不将添加类型安全这一机制的工做交给类的使用者---你的用户。
解决方案,如同你所猜测的那样,就是使用泛型。经过泛型,你从新得到了连接列表的 代码通用(对于全部类型只用实现一次),而当你初始化链表的时候你告诉链表所能接受的类型。这个实现是很是简单的,让咱们从新回到Node类:
public class Node{
Object data;
...
注意到 data 的类型是Object,(在EmployeeNode中,它是Employee)。咱们将把它变成一个泛型(一般,由一个大写的T表明)。咱们一样定义Node类,表示它能够被泛型化,以接受一个T类型。
public class Node <T>{
T data;
...
读做:T类型的Node。T表明了当Node被初始化时,Node所接受的类型。T能够是Object,也多是整型或者是Employee。这个在Node被初始化的时候才能肯定。
注意:使用T做为标识只是一种约定俗成,你可使用其余的字母组合来代替,好比这样:
public class Node <UnknownType>{
UnknownType data;
...
经过使用T做为未知类型,next字段(下一个结点的引用)必须被声明为T类型的Node(意思是说接受一个T类型的泛型化Node)。
Node<T> next;
构造函数接受一个T类型的简单参数:
public Node(T data)
{
this.data = data;
this.next = null;
}
Node 类的其他部分是很简单的,全部你须要使用Object的地方,你如今都须要使用T。LinkedList 类如今接受一个 T类型的Node,而不是一个简单的Node做为头结点。
public class LinkedList<T>{
Node<T> headNode = null;
再来一遍,转换是很直白的。任何地方你须要使用Object的,如今改作T,任何须要使用Node的地方,如今改作 Node<T>。下面的代码初始化了两个连接表。一个是整型的。
LinkedList<int> ll = new LinkedList<int>();
另外一个是Employee类型的:
LinkedList<Employee> employees = new LinkedList<Employee>();
剩下的代码与第一个版本没有区别,除了没有装箱、拆箱,并且也不可能将错误的类型保存到集合中。
LinkedList<int> ll = new LinkedList<int>();
for ( int i = 0; i < 10; i ++ )
{
ll.Add(i);
}
Console.WriteLine(ll);
Console.WriteLine(" Done.");
LinkedList<Employee> employees = new LinkedList<Employee>();
employees.Add(new Employee("John"));
employees.Add(new Employee("Paul"));
employees.Add(new Employee("George"));
employees.Add(new Employee("Ringo"));
Console.WriteLine(employees);
Console.WriteLine(" Done.");
Console.WriteLine("The fourth integer is " + ll[3]);
Employee d = employees[1];
Console.WriteLine("The second Employee is " + d);
泛型容许你不用复制/粘贴冗长的代码就实现类型安全的集合。并且,由于泛型是在运行时才被扩展成特殊类型。Just In Time编译器能够在不一样的实例之间共享代码,最后,它显著地减小了你须要编写的代码。