出处:http://www.tracefact.net/CSharp-Programming/Generics-In-CSharp.aspx

术语表

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. [C# 基础知识系列]专题九: 深入理解泛型可变性

    引言: 在C# 2.0中泛型并不支持可变性的(可变性指的就是协变性和逆变性),我们知道在面向对象的继承中就具有可变性,当方法声明返回类型为Stream,我们可以在实现中返回一个FileStream的类 ...

  2. [C# 基础知识系列]专题八: 深入理解泛型(二)

    引言: 本专题主要是承接上一个专题要继续介绍泛型的其他内容,这里就不多说了,就直接进入本专题的内容的. 一.类型推断 在我们写泛型代码的时候经常有大量的"<"和"& ...

  3. java里程碑之泛型--深入理解泛型

    所谓泛型,就是允许在定义类,接口,方法时使用类型形参,这个类型形参将在声明变量,创建对象,调用方法的时候动态的指定.JAVA5之后修改了集合中所有的接口和类,为这些接口和类都提供了泛型的支持. 关于泛 ...

  4. Java基础 -- 深入理解泛型

    一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大. 而泛型很好的解决了这个问题,这也是Java SE5的重大 ...

  5. Kotlin中的“忍者”函数 —— 理解泛型的能力(KAD 12)

    作者:Antonio Leiva 时间:Feb 8, 2017 原文链接:https://antonioleiva.com/generic-functions-kotlin/ Kotlin的一些特性组 ...

  6. Android开发之深入理解泛型extends和super的区别

    摘要: 什么是泛型?什么是擦除边界?什么是上界限定或下界限定(子类型限定或超类型限定)?什么是类型安全?泛型extends关和super关键字结合通配符?使用的区别,两种泛型在实际Android开发中 ...

  7. 【转载】C# 理解泛型

    术语表 generics:泛型type-safe:类型安全collection: 集合compiler:编译器run time:程序运行时object: 对象.NET library:.Net类库va ...

  8. 如何理解泛型中的new()约束

    一:为什么需要New约束 假设有这样一个需求,它需要在定义一个泛型类时同时实例化T对象.有网友说了:"这还不简单,我立刻给你写一个",刷刷刷,得到以下的例子. public cla ...

  9. 通过Java反射来理解泛型的本质

    集合框架中经常会使用泛型指定集合中所存放元素的类型,保证集合的统一性,从集合中取出元素的时候也避免了类型强制转换的操作,所以我们使用常规的方式来往集合中存放元素的时候,如果指定泛型,那么我们只能向集合 ...

随机推荐

  1. Java的从浅至深绕坑而行的学习

    package day02; /** * 1:java初学习,避免面试时一些HR挖的坑. * @author biexiansheng * */ public class Test02 { publi ...

  2. SSL在https和MySQL中的原理思考

    之前对HTTPS通信过程有过了解,HTTPS是应用HTTP协议使用SSL加密的版本,在TCP和HTTP之间增加SSL协议.通过握手阶段认证双方身份,协商对称秘钥对通信信息进行加密.此处只描述常用的服务 ...

  3. JSP取得绝对路径

    在JavaWeb开发中,常使用绝对路径的方式来引入JavaScript和CSS文件,这样可以避免因为目录变动导致引入文件找不到的情况,常用的做法如下: 一.使用${pageContext.reques ...

  4. Android调用浏览器打开网址遇到的问题

    我自己的手机(一加一代,升级了氢OS),然后在点击游戏内一个"隐私政策"-- 需要打开一个网页,然后就crash了.出错的信息如下: 完全是看不出来,然后我单独写了一个demo来测 ...

  5. 【原】MyEclipse8.5集成Tomcat7时启动错误:Exception in thread “main” java.lang.NoClassDefFoundError

    解决方法: MyEclipse->Window->Preferences->MyEclipse->Servers->Tomcat->Tomcat 6.x->L ...

  6. Oracle Redo Log 机制 小结(转载)

    Oracle 的Redo 机制DB的一个重要机制,理解这个机制对DBA来说也是非常重要,之前的Blog里也林林散散的写了一些,前些日子看老白日记里也有说明,所以结合老白日记里的内容,对oracle 的 ...

  7. 【C/C++】随机数问题

    最初问题:从n个数中随机选择m个数(0<=m<=n). 为了便于描述,可以将该问题抽象为:从0-n-1这n个数中随机选择m个数.计算机能够提供的随机数都是伪随机的,我们假设计算机提供的伪随 ...

  8. WPF快速精通版

    命名空间: xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:local="clr-namespace:U ...

  9. RAID卡

        简单的说,RAID是一种把多块独立的物理硬盘按不同方式组合起来形成一个逻辑硬盘,从而提供比单个硬盘有着更高的性能和提供数据冗余的技术.     RAID卡一般分为硬RAID卡和软RAID卡两种 ...

  10. eclipse 运行报java.lang.OutOfMemoryError: PermGen space解决方法

    一.在window下eclipse里面Server挂的是tomcat6,一开始还是以为,tomcat配置的问题,后面发现,配置了tomcat里面的catalina.bat文件,加入 set JAVA_ ...