“泛型”(generic)是公共语言运行库(CLR)和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。

  大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。此外,CLR还允许创建泛型接口和泛型委托。少数情况下,一个方法可能封装了一个有用的算法,所以CLR允许创建一个引用类型、值类型或接口中定义的泛型方法。

  泛型为开发人员提供了以下优势:

  1. 源代码保护:使用一个泛型算法的开发人员不需要访问算法的源代码。

  2. 类型安全性:将一个泛型算法应用于一个特定的类型时,编译器和CLR能理解开发人员的意图,并保证只有与指定数据类型兼容的对象才能随同算法使用。

  3. 更加清晰的代码:由于编译器强制类型安全性,所以减少了源代码中必须进行的转型次数。这样,代码更容易编写和维护。

  4. 更佳的性能

  泛型最明显的应用就是集合类。FCL已经定义了几个泛型集合类。微软建议开发人员使用新的泛型集合类,不鼓励使用非泛型集合类。下表总结了这些泛型集合类及其取代的非泛型集合类。

  其中许多集合类都使用了辅助类(helper class)。Dictionary和SortedDictionary类使用了KeyValuePair<TKey,TValue>类,后者是非泛型Dictionary-Entry类的泛型等价类。

  集合类实现了许多接口,利用放在集合中的对象所实现的接口,集合类可执行像排序、搜索这样的操作。FCL内建了许多泛型接口定义,所以在我们操作接口的时候,也能体会到泛型带来的好处。

  新的泛型接口并不是设计用来完全取代旧的非泛型接口的。许多时候,为了保证向后兼容性,我们不得不同时使用两者。如,假定List<T>类只实现了IList<T>接口,那么没有代码会将一个List<DateTime>对象视为一个IList。

  同时,应该指出的是,System.Array类(所有数组类的基类)提供了大量静态泛型方法,比如AsReadOnly、BinarySearch、ConvertAll、Exists、Find、FindAll、FindIndex、FindLast、FindLastIndex、ForEach、IndexOf、LastIndexOf、Resize、Sort和TrueForAll等。使用方法可见下例:

public static void Main()
{
Byte[] byteArray
= new Byte[] { 5, 1, 4, 2, 3 };
//调用Byte[]排序算法
Array.Sort<Byte>(byteArray);
//调用Byte[]二叉搜索算法
Int32 i = Array.BinarySearch<Byte>(byteArray, 1);
Console.WriteLine(i);
}

  Microsoft的设计规范称,泛型参数变量要么为T,要么至少以大写字母T开头。

开放和封闭式泛型

  CLR为应用程序使用的每个类型创建一个内部数据结构,这种数据结构称为“类型对象”(type object)。事实上,具有泛型类型参数的一个类型仍是一个类型,所以CLR同样会为其创建一个内部类型对象。无论引用类型、值类型、接口类型、还是委托类型,这一条都是适用的。然而,具有泛型类型参数的一个类型被称为“开放式类型”(open type),CLR禁止构造开放式类型的任何类型。这一点类似于CLR禁止构造一个接口类型的实例。

  在代码中引用一个泛型类型时,可能会指定一系列泛型类型实参。假如为所有类型参数传递的实参代表的都是实际的数据类型,类型就称为“封闭式类型”(closed type)。CLR允许构造封闭式类型的实例。然而,当代码引用一个泛型类型的时候,可能保持一些泛型类型参数的未指定状态。这样会在CLR中创建一个新的开放式类型对象,该类型的实例是无法创建的。见下例代码:

using System;
using System.Collections.Generic;

//一个部分指定的开放式类型
internal sealed class DictionaryStringKey<TValue> : Dictionary<String, TValue>
{}

public static class Program
{
public static void Main()
{
object o = null;

//Dictionary<,>是一个开放式类型,它有两个类型参数
Type t = typeof(Dictionary<,>);

//尝试创建该类型的一个实例(失败)
o = CreateInstance(t);
Console.WriteLine();

//DictionaryStringKey<>是一个开放式类型,它有1个类型参数
t = typeof(DictionaryStringKey<>);

//尝试创建该类型的一个实例(失败)
o = CreateInstance(t);
Consolw.WriteLine();

//DictionaryStringKey<Guid>是一个封闭式类型
t = typeof(DictionaryStringKey<Guid>);

//尝试创建该类型的一个实例(成功)
o = CreateInstance(t);

//证明它确实能够工作
Console.WriteLine("Object type=" + o.GetType());
}

private static Object CreateInstance(Type t)
{
object o = null;
try
{
o
= Activator.CreateInstance(t);
Console.Write(
"Created instance of {0}", t.ToString());
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
return o;
}
}

  编译并运行上述代码,得到以下输出:

  无法创建System.Collections.Generic.Dictionary'2[TKey,TValue]的实例,因为Type.ContainsGenericParameters为True
  无法创建DictionaryStringKey'1[TValue]的实例,因为Type.ContainsGenericParameters为True
  Created instance of DictionaryStringKey'1[System.Guid]
  Object type=DictionaryStringKey'1[System.Guid]

  可以看出,Activator的CreateInstance方法会在我们试图构造一个开放式类型的实例时抛出一个ArgumentException异常。

  在输出中,可以看出类型名是以一个“'”符号和一个数字结尾的。这个数字代表类型的arity,也就是类型要求的类型参数的数量。例如,Dictionary类的arity为2,因为它要求为TKey和TValue这两个类型参数指定具体的类型。DictionaryStringKey类的arity为1,因为它只要求TValue指定一个类型。

  还要指出的是,CLR会在类型对象内部分配一个类型的静态字段。因此,每个封闭式类型都有它自己的静态字段。也就是说,假如List<T>字义了任何静态字段,这些字段不会在一个List<DateTime>和一个List<String>之间共享。另外,假如一个泛型类型定义了一个静态构造器,那么针对每个封闭式类型,这个构造器都会执行一次。在泛型类型上定义一个静态构造器的目的是保证传递的类型实参满足特定的条件。例如,如果希望一个泛型类型只用于处理枚举类型,就可以像下面这样定义:

internal sealed class GenericTypeThatRequiresAnEnum<T>
{
static GenericTypeThatRequiresAnEnum()
{
if(!typeof(T).IsEnum)
{
throw new ArgumentException("T must be an enumerated type");
}
}
}

  CLR提供了一个名为“约束”的特性,利用它可以更好地定义一个泛型类型来指出哪些类型的实参是有效的。但遗憾的是,使用约束,无法将类型实参限制为“仅枚举类型”。正因为此原因,所以上例需要用一个静态构造器来保证类型是一个枚举类型。

泛型类型和继承

  泛型类型本质上还是一个类型,所以它能从其他任何类型派生。使用一个泛型类型并指定类型实参时,实际是在CLR中定义一个新类型,新类型的对象是从泛型类型派生自的那个类型派生的。也就是说,由于List<T>是从Object派生的,所以List<String>和List<Guid>也是从Object派生的。指定类型实参时,不会与继承层次结构发生任何关系--理解这一点,有助于判断哪些转型是能够进行的,哪些转型是不能进行的。

  让我们看看下面的例子:

internal sealed class Node<T>
{
public T m_data;
public Node<T> m_next;

public Node(T data) : this(data, null) {}

public Node(T data, Node<T> next)
{
m_data
= data; m_next = next;
}

public override string ToString()
{
return m_data.ToString() + ((m_next != null) ? m_next.ToString() : null);
}
}

  然后,我们来构造一个链表:

private static void SameDataLinkedList()
{
Node
<Char> head = new Node<Char>('C');
head
= new Node<Char>('B', head);
head
= new Node<Char>('A', head);
}

  在刚才展示的Node类中,对于m_next字段引用的另一个节点来说,它的m_data字段必须包含相同的数据类型。这意味着在链表包含的节点中,所有数据项都必须具有相同的类型(或派生类型)。我们不能使用Node类来创建一个这样的链表:其中一个元素包含Char值,另一个元素包含一个DateTime值。

  然而,通过定义一个非泛型Node基类,然后定义一个泛型TypedNode类(使用Node类为基类),就可让链表的每个节点都包含任意的数据类型:

internal class Node
{
protected Node m_next;

public Node(Node next)
{
m_next
= next;
}
}

internal sealed class TypedNode<T> : Node
{
public T m_data;

public TypeNode(T data) : this(data, null) {}

public TypeNode(T data, Node next) : base(next)
{
m_data
= data;
}

public override String ToString()
{
return m_data.ToString() + ((m_next != null) ? m_next.ToString() : null;
}
}

  现在,我们可以写代码来创建一个链表,其中每个节点都是一个不同的数据类型:

private static void DifferentDataLinkedList()
{
Node head
= new TypedNode<Char>('.');
head
= new TypedNode<DateTime>(DateTime.Now, head);
head
= new TypedNode<string>("Today is ", head);
Console.WriteLine(head.ToString());
}

泛型类型同一性

  有时候,泛型语法会将开发人员弄糊涂,因为源码中可能散布着大量小于和大于符号,这会损害可读性。为对语法进行改进,有的开发人员定义了一个新的非泛型类型,该类型从一个泛型类型派生,后者指定了所有类型实参。如下所示:

//为简化此类代码
List<DateTime> dt = new List<DateTime>();

//使用以下定义
internal sealed class DateTimeList : List<DateTime>{}

//使用时可像下面这样使用
DateTimeList dt = new DateTimeList();

  这样做表面上带来了便利,但绝对不要单纯出于增强源代码易读性的目的来定义一个新类,因为这样会丢失类型同一性和相等性,如下所示:

Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));

  上述代码中,sameType值为false,因为比较的是两个不同类型的对象。这样,如果一个方法原形接受一个DateTimeList,就不能将一个List<DateTime>传给它。而如果方法的原型接受一个List<DateTime>,那么可以将一个DateTimeList传给它,因为DateTimeList是从List<DateTime>派生的。

  我们可以使用using来简化一个泛型封闭式类型的引用,如:

using DateTimeList = System.Collections.Generic.List<System.DateTime>;

代码爆炸

  使用泛型参数的一个方法在进行JIT编译时,CLR获取方法的IL,替换指定的类型参数,然后针对那个方法在指定数据类型上的操作创建特有的本地代码。这正是您所希望的,也是泛型的主要特性之一。然而,这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本地代码。我们将这个现象称为“代码爆炸”,这最终会造成应用程序的工作集显著增大,从而损害性能。

  CLR内建了一些优化措施以缓解这类问题。首先,假如为一特定的类型实参调用了一个方法,以后再次使用相同的类型实参来调用这个方法,那么CLR只会为这种方法/类型组合编译一次代码。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集(加载到同一AppDomain中)也使用List<DateTime->,那么CLR只会为List<DateTime>编译一次方法。

  CLR还提供了另一个优化措施,它认为所有引用类型实参都是完全相同的,所以代码能够共享。例如,CLR为List<String>的方法编译的代码可以用于List<-Stream>的方法,因为String和Stream均为引用类型。事实上,对于任何引用类型,都会使用相同的代码。CLR之所以能够执行这个优化,是因为所有引用类型实参或变量实际只是指向堆上的对象的指针,而对象指针全部是以相同的方式来操纵的。

  但是,如果某个类型实参是值类型,CLR就必须专门为那个值类型生成本地代码。这是因为值类型的大小可能发生变化。即使两个值类型具有相同的大小(如Int32和UInt32),CLR仍然无法共享代码,因为可能要用不同的本地CPU指令来处理这些值。

posted on 2011-06-21 23:55  辛勤的代码工  阅读(2223)  评论(0编辑  收藏  举报