[译]C# 理解泛型

术语表

generics:泛型
type-safe:类型安全
collection: 集合
compiler:编译器
run time:程序运行时
object: 对象
.NET library:.Net类库
value type: 值类型
box: 装箱
unbox: 拆箱
implicity: 隐式
explicity: 显式
linked list: 线性链表
node: 结点
indexer: 索引器

简介

Visual C# 2.0 的一个最受期待的(或许也是最让人畏惧)的一个特性就是对于泛型的支持。这篇文章将告诉你泛型用来解决什么样的问题,以及如何使用它们来提高你的代码质量,还有你不必恐惧泛型的原因。

泛型是什么?

很多人觉得泛型很难理解。我相信这是因为他们通常在了解泛型是用来解决什么问题之前,就被灌输了大量的理论和范例。结果就是你有了一个解决方案,但是却没有需要使用这个解决方案的问题。

这篇文章将尝试着改变这种学习流程,我们将以一个简单的问题作为开始:泛型是用来做什么的?答案是:没有泛型,将会很难创建类型安全的集合。

C# 是一个类型安全的语言,类型安全允许编译器(可信赖地)捕获潜在的错误,而不是在程序运行时才发现(不可信赖地,往往发生在你将产品出售了以后!)。因此,在C#中,所有的变量都有一个定义了的类型;当你将一个对象赋值给那个变量的时候,编译器检查这个赋值是否正确,如果有问题,将会给出错误信息。

在 .Net 1.1 版本(2003)中,当你在使用集合时,这种类型安全就失效了。由.Net 类库提供的所有关于集合的类全是用来存储基类型(Object)的,而.Net中所有的一切都是由Object基类继承下来的,因此所有类型都可以放到一个集合中。于是,相当于根本就没有了类型检测。

更糟的是,每一次你从集合中取出一个Object,你都必须将它强制转换成正确的类型,这一转换将对性能造成影响,并且产生冗长的代码(如果你忘了进行转换,将会抛出异常)。更进一步地讲,如果你给集合中添加一个值类型(比如,一个整型变量),这个整型变量就被隐式地装箱了(再一次降低了性能),而当你从集合中取出它的时候,又会进行一次显式地拆箱(又一次性能的降低和类型转换)。

关于装箱、拆箱的更多内容,请访问 陷阱4,警惕隐式的装箱、拆箱。

创建一个简单的线性链表

为了生动地感受一下这些问题,我们将创建一个尽可能简单的线性链表。对于阅读本文的那些从未创建过线性链表的人。你可以将线性链表想像成有一条链子栓在一起的盒子(称作一个结点),每个盒子里包含着一些数据 和 链接到这个链子上的下一个盒子的引用(当然,除了最后一个盒子,这个盒子对于下一个盒子的引用被设置成NULL)。

为了创建我们的简单线性链表,我们需要下面三个类:

1、Node 类,包含数据以及下一个Node的引用。

2、LinkedList 类,包含链表中的第一个Node,以及关于链表的任何附加信息。

3、测试程序,用于测试 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编译器可以在不同的实例之间共享代码,最后,它显著地减少了你需要编写的代码。

C#泛型理解(转)的更多相关文章

  1. 转载.net泛型理解说明

    net泛型理解 泛型简介: 泛型(Generic Type)是.NET Framework2.0最强大的功能之一.泛型的主要思想是将算法与数据结构完全分离开,使得一次定义的算法能作用于多种数据结构,从 ...

  2. .net泛型理解

    泛型简介: 泛型(Generic Type)是.NET Framework2.0最强大的功能之一.泛型的主要思想是将算法与数据结构完全分离开,使得一次定义的算法能作用于多种数据结构,从而实现高度可重用 ...

  3. Map接口下的集合和泛型理解

    一.Map接口 1. Map接口就是最顶层了,上面没有继承了.Map是一个容器接口,它与前面学的List.Set容器不同的是前面学的这些容器,一次只能传入一个元素,但是Map容器一次可以传入一对元素( ...

  4. 泛型理解及应用(二):使用泛型编写通用型Dao层

    相信目前所有的IT公司网站在设计WEB项目的时候都含有持久层,同样地使用过Hibernate的程序员都应该看过或者了解过Hibernate根据数据库反向生成持久层代码的模板.对于Hibernate生成 ...

  5. java泛型理解。代码更明了。

    泛型数据java基础,但真正理解需要悉心品尝.毕竟在工作中用到的是在是太多了. 不要以为new ArrayList<>这就是泛型,这只能属于会使用. 在工作中,相对于现有的项目源码的数据库 ...

  6. Java泛型理解

    Java泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型.当需要使用某一种算法时,又无法具体算法的数据类型,或者想指定类型值的上限或下限,那么这时就需要Java泛型来大显身手 ...

  7. C#泛型理解(一)

    一.什么是泛型 泛型是C#语言和公共语言运行库(CLR)中的一个新功能,它将类型参数的概念引入.NET Framework.类型参数使得设计某些类和方法成为可能,例如,通过使用泛型类型参数T,可以大大 ...

  8. 窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型

    有的小伙伴会问:博主,没有Mac怎么学Swift语言呢,我想学Swift,但前提得买个Mac.非也,非也.如果你想了解或者初步学习Swift语言的话,你可以登录这个网站:http://swiftstu ...

  9. Java学习笔记(二一)——Java 泛型

    [前面的话] 最近脸好干,掉皮,需要买点化妆品了. Java泛型好好学习一下. [定义] 一.泛型的定义主要有以下两种: 在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个 ...

随机推荐

  1. HBase HFile

    HFile index HFile index, which is proportional to the total number of Data Blocks. The total amount ...

  2. vs 2013调试的时候重启的解决方案

    今天在用vs 2013 调试程序的时候,vs 总是莫名其妙的关闭,停止运行,泪蹦了..... 是什么原因呢? 以前的时候可是好好的啊,经过认真的思索,最近装过和vs 2013 相关的程序也只有 ref ...

  3. HDU5039--Hilarity DFS序+线段树区间更新 14年北京网络赛

    题意:n个点的树,每个条边权值为0或者1, q次操作 Q 路径边权抑或和为1的点对数, (u, v)(v, u)算2个. M i修改第i条边的权值 如果是0则变成1, 否则变成0 作法: 我们可以求出 ...

  4. Event — Windows API

    Event即事件是一种用于进行线程/进程间同步的对象,事件有置位和复位两种状态,当线程通过waiting functions等待Event对象置位时该线程将进入阻塞状态,当该Event对象被置位或等待 ...

  5. C# 我是个传奇的 using

    呵呵呵: ----------------------------------------------------------------------------------------------- ...

  6. Lecture Halls

    Lecture Halls (会议安排)   时间限制(普通/Java):1000MS/10000MS     运行内存限制:65536KByte 总提交: 38            测试通过: 2 ...

  7. Robotium API -- 等待执行的方法sleep、waitFor

    测试中经常出现明明有控件或者文本,但是由于界面没有载入完成或者其他原因导致assert判断的结果失败.或者两次执行,一次成功,一次失败的情况.所以需要加入这些等待某些控件或者文本载入的方法,以加强程序 ...

  8. Android TagFlowLayout完全解析 一款针对Tag的布局(转)

    一.概述 本文之前,先提一下关于上篇博文的100多万访问量请无视,博文被刷,我也很郁闷,本来想把那个文章放到草稿箱,结果放不进去,还把日期弄更新了,实属无奈. ok,开始今天的博文,今天要说的是Tag ...

  9. 根据goodsId获得相关商品的列表

    List<Goods> goodsList = goodsDetailService.getGoodsListByproductId(productId); for (Goods good ...

  10. 一览css布局标准

    回顾历史,CSS1于1996.12.17发正式版,它是为辅助HTML的展现效果而生的.1998.5.12,CSS2发正式版.随后发修订版CSS2.1,纠正了CSS2中的一些错误.注意从CSS2起,CS ...