cdo

导航

C# 泛型简介

发布日期: 5/30/2005 | 更新日期: 5/30/2005

Juval Lowy
IDesign

摘要:本文讨论泛型处理的问题空间、它们的实现方式、该编程模型的好处,以及独特的创新(例如,约束、一般方法和委托以及一般继承)。此外,本文还讨论 .NET Framework 如何利用泛型。

本文假定读者熟悉 C# 1.1。有关 C# 语言的详细信息,请访问 http://msdn.microsoft.com/vcsharp/language

*
本页内容
简介 简介
泛型问题陈述 泛型问题陈述
什么是泛型 什么是泛型
应用泛型 应用泛型
一般约束 一般约束
泛型和强制类型转换 泛型和强制类型转换
继承和泛型 继承和泛型
一般方法 一般方法
一般委托 一般委托
泛型和反射 泛型和反射
泛型和 .NET Framework 泛型和 .NET Framework
小结 小结

简介

泛型是 C# 2.0 的最强大的功能。通过泛型可以定义类型安全的数据结构,而无须使用实际的数据类型。这能够显著提高性能并得到更高质量的代码,因为您可以重用数据处理算法,而无须复制类型特定的代码。在概念上,泛型类似于 C++ 模板,但是在实现和功能方面存在明显差异。本文讨论泛型处理的问题空间、它们的实现方式、该编程模型的好处,以及独特的创新(例如,约束、一般方法和委托以及一般继承)。您还将了解在 .NET Framework 的其他领域(例如,反射、数组、集合、序列化和远程处理)中如何利用泛型,以及如何在所提供的基本功能的基础上进行改进。

泛型问题陈述

考虑一种普通的、提供传统 Push()Pop() 方法的数据结构(例如,堆栈)。在开发通用堆栈时,您可能愿意使用它来存储各种类型的实例。在 C# 1.1 下,您必须使用基于 Object 的堆栈,这意味着,在该堆栈中使用的内部数据类型是难以归类的 Object,并且堆栈方法与 Object 交互:

public class Stack
{
   object[] m_Items; 
   public void Push(object item)
   {...}
   public object Pop()
   {...}
}

代码块 1 显示基于 Object 的堆栈的完整实现。因为 Object 是规范的 .NET 基类型,所以您可以使用基于 Object 的堆栈来保持任何类型的项(例如,整数):

Stack stack = new Stack();
stack.Push(1);
stack.Push(2);
int number = (int)stack.Pop();

代码块 1. 基于 Object 的堆栈

public class Stack
{
   readonly int m_Size; 
   int m_StackPointer = 0;
   object[] m_Items; 
   public Stack():this(100)
   {}   
   public Stack(int size)
   {
      m_Size = size;
      m_Items = new object[m_Size];
   }
   public void Push(object item)
   {
      if(m_StackPointer >= m_Size) 
         throw new StackOverflowException();       
      m_Items[m_StackPointer] = item;
      m_StackPointer++;
   }
   public object Pop()
   {
      m_StackPointer--;
      if(m_StackPointer >= 0)
      {
         return m_Items[m_StackPointer];
      }
      else
      {
         m_StackPointer = 0;
         throw new InvalidOperationException("Cannot pop an empty stack");
      }
   }
}

但是,基于 Object 的解决方案存在两个问题。第一个问题是性能。在使用值类型时,必须将它们装箱以便推送和存储它们,并且在将值类型弹出堆栈时将其取消装箱。装箱和取消装箱都会根据它们自己的权限造成重大的性能损失,但是它还会增加托管堆上的压力,导致更多的垃圾收集工作,而这对于性能而言也不太好。即使是在使用引用类型而不是值类型时,仍然存在性能损失,这是因为必须从 Object 向您要与之交互的实际类型进行强制类型转换,从而造成强制类型转换开销:

Stack stack = new Stack();
stack.Push("1");
string number = (string)stack.Pop();

基于 Object 的解决方案的第二个问题(通常更为严重)是类型安全。因为编译器允许在任何类型和 Object 之间进行强制类型转换,所以您将丢失编译时类型安全。例如,以下代码可以正确编译,但是在运行时将引发无效强制类型转换异常:

Stack stack = new Stack();
stack.Push(1);
//This compiles, but is not type safe, and will throw an exception: 
string number = (string)stack.Pop();

您可以通过提供类型特定的(因而是类型安全的)高性能堆栈来克服上述两个问题。对于整型,可以实现并使用 IntStack

public class IntStack
{
   int[] m_Items; 
   public void Push(int item){...}
   public int Pop(){...}
} 
IntStack stack = new IntStack();
stack.Push(1);
int number = stack.Pop();

对于字符串,可以实现 StringStack

public class StringStack
{
   string[] m_Items; 
   public void Push(string item){...}
   public string Pop(){...}
}
StringStack stack = new StringStack();
stack.Push("1");
string number = stack.Pop();

等等。遗憾的是,以这种方式解决性能和类型安全问题,会引起第三个同样严重的问题 — 影响工作效率。编写类型特定的数据结构是一项乏味的、重复性的且易于出错的任务。在修复该数据结构中的缺陷时,您不能只在一个位置修复该缺陷,而必须在实质上是同一数据结构的类型特定的副本所出现的每个位置进行修复。此外,没有办法预知未知的或尚未定义的将来类型的使用情况,因此还必须保持基于 Object 的数据结构。结果,大多数 C# 1.1 开发人员发现类型特定的数据结构不实用,并且选择使用基于 Object 的数据结构,尽管它们存在缺点。

什么是泛型

通过泛型可以定义类型安全类,而不会损害类型安全、性能或工作效率。您只须一次性地将服务器实现为一般服务器,同时可以用任何类型来声明和使用它。为此,需要使用 <> 括号,以便将一般类型参数括起来。例如,可以按如下方式定义和使用一般堆栈:

public class Stack
{
   T[] m_Items; 
   public void Push(T item)
   {...}
   public T Pop()
   {...}
}
Stack stack = new Stack();
stack.Push(1);
stack.Push(2);
int number = stack.Pop();

代码块 2 显示一般堆栈的完整实现。将代码块 1代码块 2 进行比较,您会看到,好像 代码块 1 中每个使用 Object 的地方在代码块 2 中都被替换成了 T,除了使用一般类型参数 T 定义 Stack 以外:

public class Stack
{...}

在使用一般堆栈时,必须通知编译器使用哪个类型来代替一般类型参数 T(无论是在声明变量时,还是在实例化变量时):

Stack stack = new Stack();

编译器和运行库负责完成其余工作。所有接受或返回 T 的方法(或属性)都将改为使用指定的类型(在上述示例中为整型)。

代码块 2. 一般堆栈

public class Stack
{
   readonly int m_Size; 
   int m_StackPointer = 0;
   T[] m_Items;
   public Stack():this(100)
   {}
   public Stack(int size)
   {
      m_Size = size;
      m_Items = new T[m_Size];
   }
   public void Push(T item)
   {
      if(m_StackPointer >= m_Size) 
         throw new StackOverflowException();
      m_Items[m_StackPointer] = item;
      m_StackPointer++;
   }
   public T Pop()
   {
      m_StackPointer--;
      if(m_StackPointer >= 0)
      {
         return m_Items[m_StackPointer];
      }
      else
      {
         m_StackPointer = 0;
         throw new InvalidOperationException("Cannot pop an empty stack");
      }
   }
}

T 是一般类型参数(或类型参数),而一般类型为 Stack。Stack 中的 int 为类型实参。

该编程模型的优点在于,内部算法和数据操作保持不变,而实际数据类型可以基于客户端使用服务器代码的方式进行更改。

泛型实现

表面上,C# 泛型的语法看起来与 C++ 模板类似,但是编译器实现和支持它们的方式存在重要差异。正如您将在后文中看到的那样,这对于泛型的使用方式具有重大意义。

在本文中,当提到 C++ 时,指的是传统 C++,而不是带有托管扩展的 Microsoft C++。

与 C++ 模板相比,C# 泛型可以提供增强的安全性,但是在功能方面也受到某种程度的限制。

在一些 C++ 编译器中,在您通过特定类型使用模板类之前,编译器甚至不会编译模板代码。当您确实指定了类型时,编译器会以内联方式插入代码,并且将每个出现一般类型参数的地方替换为指定的类型。此外,每当您使用特定类型时,编译器都会插入特定于该类型的代码,而不管您是否已经在应用程序中的其他某个位置为模板类指定了该类型。C++ 链接器负责解决该问题,并且并不总是有效。这可能会导致代码膨胀,从而增加加载时间和内存足迹。

在 .NET 2.0 中,泛型在 IL(中间语言)和 CLR 本身中具有本机支持。在编译一般 C# 服务器端代码时,编译器会将其编译为 IL,就像其他任何类型一样。但是,IL 只包含实际特定类型的参数或占位符。此外,一般服务器的元数据包含一般信息。

客户端编译器使用该一般元数据来支持类型安全。当客户端提供特定类型而不是一般类型参数时,客户端的编译器将用指定的类型实参来替换服务器元数据中的一般类型参数。这会向客户端的编译器提供类型特定的服务器定义,就好像从未涉及到泛型一样。这样,客户端编译器就可以确保方法参数的正确性,实施类型安全检查,甚至执行类型特定的 IntelliSense。

有趣的问题是,.NET 如何将服务器的一般 IL 编译为机器码。原来,所产生的实际机器码取决于指定的类型是值类型还是引用类型。如果客户端指定值类型,则 JIT 编译器将 IL 中的一般类型参数替换为特定的值类型,并且将其编译为本机代码。但是,JIT 编译器会跟踪它已经生成的类型特定的服务器代码。如果请求 JIT 编译器用它已经编译为机器码的值类型编译一般服务器,则它只是返回对该服务器代码的引用。因为 JIT 编译器在以后的所有场合中都将使用相同的值类型特定的服务器代码,所以不存在代码膨胀问题。

如果客户端指定引用类型,则 JIT 编译器将服务器 IL 中的一般参数替换为 Object,并将其编译为本机代码。在以后的任何针对引用类型而不是一般类型参数的请求中,都将使用该代码。请注意,采用这种方式,JIT 编译器只会重新使用实际代码。实例仍然按照它们离开托管堆的大小分配空间,并且没有强制类型转换。

泛型的好处

.NET 中的泛型使您可以重用代码以及在实现它时付出的努力。类型和内部数据可以在不导致代码膨胀的情况下更改,而不管您使用的是值类型还是引用类型。您可以一次性地开发、测试和部署代码,通过任何类型(包括将来的类型)来重用它,并且全部具有编译器支持和类型安全。因为一般代码不会强行对值类型进行装箱和取消装箱,或者对引用类型进行向下强制类型转换,所以性能得到显著提高。对于值类型,性能通常会提高 200%;对于引用类型,在访问该类型时,可以预期性能最多提高 100%(当然,整个应用程序的性能可能会提高,也可能不会提高)。本文随附的源代码包含一个微型基准应用程序,它在紧密循环中执行堆栈。该应用程序使您可以在基于 Object 的堆栈和一般堆栈上试验值类型和引用类型,以及更改循环迭代的次数以查看泛型对性能产生的影响。

应用泛型

因为 IL 和 CLR 为泛型提供本机支持,所以大多数符合 CLR 的语言都可以利用一般类型。例如,下面这段 Visual Basic .NET 代码使用代码块 2 的一般堆栈:

Dim stack As Stack(Of Integer)
stack = new Stack(Of Integer)
stack.Push(3)
Dim number As Integer
number = stack.Pop()

您可以在类和结构中使用泛型。以下是一个有用的一般点结构:

public struct Point
{
   
   public T X;
   
   public T Y;
}

可以使用该一般点来表示整数坐标,例如:

Point point;
point.X = 1;
point.Y = 2;

或者,可以使用它来表示要求浮点精度的图表坐标:

Point point;
point.X = 1.2;
point.Y = 3.4;

除了到目前为止介绍的基本泛型语法以外,C# 2.0 还具有一些泛型特定的语法。例如,请考虑代码块 2Pop() 方法。假设您不希望在堆栈为空时引发异常,而是希望返回堆栈中存储的类型的默认值。如果您使用基于 Object 的堆栈,则可以简单地返回 null,但是您还可以通过值类型来使用一般堆栈。为了解决该问题,您可以使用 default() 运算符,它返回类型的默认值。

下面说明如何在 Pop() 方法的实现中使用默认值:

public T Pop()
{
   m_StackPointer--;
   if(m_StackPointer >= 0)
   {
      return m_Items[m_StackPointer];
   }
   else
   {
      m_StackPointer = 0;
      return default(T);
   }
}

引用类型的默认值为 null,而值类型(例如,整型、枚举和结构)的默认值为全零(用零填充相应的结构)。因此,如果堆栈是用字符串构建的,则 Pop() 方法在堆栈为空时返回 null;如果堆栈是用整数构建的,则 Pop() 方法在堆栈为空时返回零。

多个一般类型

单个类型可以定义多个一般类型参数。例如,请考虑代码块 3 中显示的一般链表。

代码块 3. 一般链表

class Node
{
   public K Key;
   public T Item;
   public Node NextNode;
   public Node()
   {
      Key      = default(K);
      Item     = defualt(T);
      NextNode = null;
   }
   public Node(K key,T item,Node nextNode)
   {
      Key      = key;
      Item     = item;
      NextNode = nextNode;
   }
}

public class LinkedList
{
   Node m_Head;
   public LinkedList()
   {
      m_Head = new Node();
   }
   public void AddHead(K key,T item)
   {
      Node newNode = new Node(key,item,m_Head.NextNode);
      m_Head.NextNode = newNode;
   }
}

该链表存储节点:

class Node
{...}

每个节点都包含一个键(属于一般类型参数 K)和一个值(属于一般类型参数 T)。每个节点还具有对该列表中下一个节点的引用。链表本身根据一般类型参数 K 和 T 进行定义:

public class LinkedList
{...}

这使该列表可以公开像 AddHead() 一样的一般方法:

public void AddHead(K key,T item);

每当您声明使用泛型的类型的变量时,都必须指定要使用的类型。但是,指定的类型实参本身可以为一般类型参数。例如,该链表具有一个名为 m_Head 的 Node 类型的成员变量,用于引用该列表中的第一个项。m_Head 是使用该列表自己的一般类型参数 K 和 T 声明的。

Node m_Head;

您需要在实例化节点时提供类型实参;同样,您可以使用该链表自己的一般类型参数:

public void AddHead(K key,T item)
{
   Node newNode = new Node<K,T>(key,item,m_Head.NextNode);
   m_Head.NextNode = newNode;
}

请注意,该列表使用与节点相同的名称来表示一般类型参数完全是为了提高可读性;它也可以使用其他名称,例如:

public class LinkedList
{...}

或:

public class LinkedList
{...}

在这种情况下,将 m_Head 声明为:

Node m_Head;

当客户端使用该链表时,该客户端必须提供类型实参。该客户端可以选择整数作为键,并且选择字符串作为数据项:

LinkedList list = new LinkedList();
list.AddHead(123,"AAA");

但是,该客户端可以选择其他任何组合(例如,时间戳)来表示键:

LinkedList list = new LinkedList();
list.AddHead(DateTime.Now,"AAA");   

有时,为特定类型的特殊组合起别名是有用的。可以通过 using 语句完成该操作,如代码块 4 中所示。请注意,别名的作用范围是文件的作用范围,因此您必须按照与使用 using 命名空间相同的方式,在项目文件中反复起别名。

代码块 4. 一般类型别名

using List = LinkedList;

class ListClient
{
   static void Main(string[] args)
   {
      List list = new List();
      list.AddHead(123,"AAA");
   }
}

一般约束

使用 C# 泛型,编译器会将一般代码编译为 IL,而不管客户端将使用什么样的类型实参。因此,一般代码可以尝试使用与客户端使用的特定类型实参不兼容的一般类型参数的方法、属性或成员。这是不可接受的,因为它相当于缺少类型安全。在 C# 中,您需要通知编译器客户端指定的类型必须遵守哪些约束,以便使它们能够取代一般类型参数而得到使用。存在三个类型的约束。派生约束指示编译器一般类型参数派生自诸如接口或特定基类之类的基类型。默认构造函数约束指示编译器一般类型参数公开了默认的公共构造函数(不带任何参数的公共构造函数)。引用/值类型约束将一般类型参数约束为引用类型或值类型。一般类型可以利用多个约束,您甚至可以在使用一般类型参数时使 IntelliSense 反射这些约束,例如,建议基类型中的方法或成员。

需要注意的是,尽管约束是可选的,但它们在开发一般类型时通常是必不可少的。没有它们,编译器将采取更为保守的类型安全方法,并且只允许在一般类型参数中访问 Object 级别功能。约束是一般类型元数据的一部分,以便客户端编译器也可以利用它们。客户端编译器只允许客户端开发人员使用遵守这些约束的类型,从而实施类型安全。

以下示例将详细说明约束的需要和用法。假设您要向代码块 3 的链表中添加索引功能或按键搜索功能:

public class LinkedList
{
   T Find(K key)
   {...}
   public T this[K key]
   {
      get{return Find(key);}
   }
}

这使客户端可以编写以下代码:

LinkedList list = new LinkedList();

list.AddHead(123,"AAA");
list.AddHead(456,"BBB");
string item = list[456];
Debug.Assert(item == "BBB");

要实现搜索,您需要扫描列表,将每个节点的键与您要查找的键进行比较,并且返回键匹配的节点的项。问题在于,Find() 的以下实现无法编译:

T Find(K key)
{
   Node current = m_Head;
   while(current.NextNode != null)
   {
      if(current.Key == key) //Will not compile
         break;
      else
         
         current = current.NextNode;
   }
   return current.Item; 
}

原因在于,编译器将拒绝编译以下行:

if(current.Key == key)

上述行将无法编译,因为编译器不知道 K(或客户端提供的实际类型)是否支持 == 运算符。例如,默认情况下,结构不提供这样的实现。您可以尝试通过使用 IComparable 接口来克服 == 运算符局限性:

public interface IComparable 
{
   int CompareTo(object obj);
}

如果您与之进行比较的对象等于实现该接口的对象,则 CompareTo() 返回 0;因此,Find() 方法可以按如下方式使用它:

if(current.Key.CompareTo(key) == 0)

遗憾的是,这也无法编译,因为编译器无法知道 K(或客户端提供的实际类型)是否派生自 IComparable

您可以显式强制转换到 IComparable,以强迫编译器编译比较行,除非这样做需要牺牲类型安全:

if(((IComparable)(current.Key)).CompareTo(key) == 0)

如果客户端使用的类型不是派生自 IComparable,则会导致运行时异常。此外,当所使用的键类型是值类型而非键类型参数时,您可以对该键执行装箱,而这可能具有一些性能方面的影响。

派生约束

在 C# 2.0 中,可以使用 where 保留关键字来定义约束。在一般类型参数中使用 where 关键字,后面跟一个派生冒号,以指示编译器该一般类型参数实现了特定接口。例如,以下为实现 LinkedList 的 Find() 方法所必需的派生约束:

public class LinkedList where K : IComparable
{
   T Find(K key)
   {
      Node current = m_Head;
      while(current.NextNode != null)
      {
         if(current.Key.CompareTo(key) == 0)
            
            break;
         else      
            
            current = current.NextNode;
      }
      return current.Item; 
   }
   //Rest of the implementation 
}

您还将在您约束的接口的方法上获得 IntelliSense 支持。

当客户端声明一个 LinkedList 类型的变量,以便为列表的键提供类型实参时,客户端编译器将坚持要求键类型派生自 IComparable,否则,将拒绝生成客户端代码。

请注意,即使该约束允许您使用 IComparable,它也不会在所使用的键是值类型(例如,整型)时,消除装箱所带来的性能损失。为了克服该问题,System.Collections.Generic 命名空间定义了一般接口 IComparable

public interface IComparable 
{
   int CompareTo(T other);
   bool Equals(T other);
}

您可以约束键类型参数以支持 IComparable,并且使用键的类型作为类型参数;这样,您不仅获得了类型安全,而且消除了在值类型用作键时的装箱操作:

public class LinkedList where K : IComparable
{...}

实际上,所有支持 .NET 1.1 中的 IComparable 的类型都支持 .NET 2.0 中的 IComparable。这使得可以使用常见类型(例如,int、string、GUID、DateTime 等等)的键。

在 C# 2.0 中,所有约束都必须出现在一般类的实际派生列表之后。例如,如果 LinkedList 派生自 IEnumerable 接口(以获得迭代器支持),则需要将 where 关键字放在紧跟它后面的位置:

public class LinkedList : IEnumerable where K : IComparable
{...}

通常,只须在需要的级别定义约束。在链表示例中,在节点级别定义 IComparable 派生约束是没有意义的,因为节点本身不会比较键。如果您这样做,则您还必须将该约束放在 LinkedList 级别,即使该列表不比较键。这是因为该列表包含一个节点作为成员变量,从而导致编译器坚持要求:在列表级别定义的键类型必须遵守该节点在一般键类型上放置的约束。

换句话说,如果您按如下方式定义该节点:

class Node where K : IComparable
{...}

则您必须在列表级别重复该约束,即使您不提供 Find() 方法或其他任何与此有关的方法:

public class LinkedList where KeyType : IComparable
{
   Node<KeyType,DataType> m_Head;
}

您可以在同一个一般类型参数上约束多个接口(彼此用逗号分隔)。例如:

public class LinkedList where K : IComparable,IConvertible
{...}

您可以为您的类使用的每个一般类型参数提供约束,例如:

public class LinkedList where K : IComparable
                             where T : ICloneable 
{...}

您可以具有一个基类约束,这意味着规定一般类型参数派生自特定的基类:

public class MyBaseClass
{...}
public class LinkedList where K : MyBaseClass
{...}

但是,在一个约束中最多只能使用一个基类,这是因为 C# 不支持实现的多重继承。显然,您约束的基类不能是密封类或静态类,并且由编译器实施这一限制。此外,您不能将 System.DelegateSystem.Array 约束为基类。

您可以同时约束一个基类以及一个或多个接口,但是该基类必须首先出现在派生约束列表中:

public class LinkedList where K : MyBaseClass, IComparable
{...}

C# 确实允许您将另一个一般类型参数指定为约束:

public class MyClass where T : U 
{...}

在处理派生约束时,您可以通过使用基类型本身来满足该约束,而不必非要使用它的严格子类。例如:

public interface IMyInterface
{...}
public class MyClass where T : IMyInterface
{...}
MyClass obj = new MyClass();

或者,您甚至可以:

public class MyOtherClass
{...}

public class MyClass where T : MyOtherClass 
{...}

MyClass obj = new MyClass();

最后,请注意,在提供派生约束时,您约束的基类型(接口或基类)必须与您定义的一般类型参数具有一致的可见性。例如,以下约束是有效的,因为内部类型可以使用公共类型:

public class MyBaseClass
{}
internal class MySubClass where T : MyBaseClass
{}
但是,如果这两个类的可见性被颠倒,例如:
internal class MyBaseClass
{}
public class MySubClass where T : MyBaseClass
{}

则编译器会发出错误,因为程序集外部的任何客户端都无法使用一般类型 MySubClass,从而使得 MySubClass 实际上成为内部类型而不是公共类型。外部客户端无法使用 MySubClass 的原因是,要声明 MySubClass 类型的变量,它们需要使用派生自内部类型 MyBaseClass 的类型。

构造函数约束

假设您要在一般类的内部实例化一个新的一般对象。问题在于,C# 编译器不知道客户端将使用的类型实参是否具有匹配的构造函数,因而它将拒绝编译实例化行。

为了解决该问题,C# 允许约束一般类型参数,以使其必须支持公共默认构造函数。这是使用 new() 约束完成的。例如,以下是一种实现代码块 3 中的一般 Node 的默认构造函数的不同方式。

class Node where T : new() 
{
   public K Key;
   public T Item;
   public Node NextNode;
   public Node()
   {
      Key      = default(K);
      Item     = new T();
      NextNode = null;
   }
}

可以将构造函数约束与派生约束组合起来,前提是构造函数约束出现在约束列表中的最后:

public class LinkedList where K : IComparable,new() 
{...}

引用/值类型约束

可以使用 struct 约束将一般类型参数约束为值类型(例如,int、bool 和 enum),或任何自定义结构:

public class MyClass where T : struct 

{...}

同样,可以使用 class 约束将一般类型参数约束为引用类型(类):

public class MyClass where T : class 

{...}

不能将引用/值类型约束与基类约束一起使用,因为基类约束涉及到类。同样,不能使用结构和默认构造函数约束,因为默认构造函数约束也涉及到类。虽然您可以使用类和默认构造函数约束,但这样做没有任何价值。可以将引用/值类型约束与接口约束组合起来,前提是引用/值类型约束出现在约束列表的开头。

泛型和强制类型转换

C# 编译器只允许将一般类型参数隐式强制转换到 Object 或约束指定的类型,如代码块 5 所示。这样的隐式强制类型转换是类型安全的,因为可以在编译时发现任何不兼容性。

代码块 5. 一般类型参数的隐式强制类型转换

interface ISomeInterface
{...}
class BaseClass
{...}
class MyClass where T : BaseClass,ISomeInterface
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = t;
      BaseClass      obj2 = t;
      object         obj3 = t;
   }
}

编译器允许您将一般类型参数显式强制转换到其他任何接口,但不能将其转换到类:

interface ISomeInterface
{...}
class SomeClass
{...}
class MyClass 
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = (ISomeInterface)t;//Compiles
      SomeClass      obj2 = (SomeClass)t;     //Does not compile
   }
}

但是,您可以使用临时的 Object 变量,将一般类型参数强制转换到其他任何类型:

class SomeClass
{...}

class MyClass 
{
   
   void SomeMethod(T t)
   
   {
      object temp = t;
      SomeClass obj = (SomeClass)temp;
   
   }
}

不用说,这样的显式强制类型转换是危险的,因为如果为取代一般类型参数而使用的类型实参不是派生自您要显式强制转换到的类型,则可能在运行时引发异常。要想不冒引发强制类型转换异常的危险,一种更好的办法是使用 is 和 as 运算符,如代码块 6 所示。如果一般类型参数的类型是所查询的类型,则 is 运算符返回 true;如果这些类型兼容,则 as 将执行强制类型转换,否则将返回 null。您可以对一般类型参数以及带有特定类型实参的一般类使用 is 和 as。

代码块 6. 对一般类型参数使用“is”和“as”运算符

public class MyClass 
{
   public void SomeMethod(T t)
   {
      if(t is int)
      {...} 

      if(t is LinkedList)
      {...}

      string str = t as string;
      if(str != null)
      {...}

      LinkedList list = t as LinkedList;
      if(list != null)
      {...}
   }
}

继承和泛型

在从一般基类派生时,必须提供类型实参,而不是该基类的一般类型参数:

public class BaseClass
{...}
public class SubClass : BaseClass
{...}

如果子类是一般的而非具体的类型实参,则可以使用子类一般类型参数作为一般基类的指定类型:

public class SubClass : BaseClass 
{...}

在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束:

public class BaseClass  where T : ISomeInterface 
{...}
public class SubClass : BaseClass where T : ISomeInterface
{...}

或构造函数约束:

public class BaseClass  where T : new()
{   
   public T SomeMethod()
   {
      return new T();
   }
}
public class SubClass : BaseClass where T : new() 
{...}

基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型:

public class BaseClass
{ 
   public virtual T SomeMethod()
   {...}
}
public class SubClass: BaseClass<int>
{ 
   public override int SomeMethod()
   {...}
}

如果该子类是一般类型,则它还可以在重写时使用它自己的一般类型参数:

public class SubClass: BaseClass
{ 
   public override T SomeMethod()
   {...}
}

您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样:

public interface ISomeInterface
{
   T SomeMethod(T t);
}
public abstract class BaseClass
{
   public abstract T SomeMethod(T t);
}

public class SubClass : BaseClass
{
   public override T SomeMethod(T t) 
   {...)
}

一般抽象方法和一般接口有一种有趣的用法。在 C# 2.0 中,不能对一般类型参数使用诸如 + 或 += 之类的运算符。例如,以下代码无法编译,因为 C# 2.0 不具有运算符约束:

public class Calculator
{
   public T Add(T arg1,T arg2)
   {
      return arg1 + arg2;//Does not compile 
   }
   //Rest of the methods 
}

但是,您可以通过定义一般操作,使用抽象方法(最好使用接口)进行补偿。由于抽象方法的内部不能具有任何代码,因此可以在基类级别指定一般操作,并且在子类级别提供具体的类型和实现:

public abstract class BaseCalculator
{
   public abstract T Add(T arg1,T arg2);
   public abstract T Subtract(T arg1,T arg2);
   public abstract T Divide(T arg1,T arg2);
   public abstract T Multiply(T arg1,T arg2);
}
public class MyCalculator : BaseCalculator
{
   public override int Add(int arg1, int arg2)
   {
      return arg1 + arg2;
   }
   //Rest of the methods 
} 

一般接口还可以产生更加干净一些的解决方案:

public interface ICalculator
{
   T Add(T arg1,T arg2);
   //Rest of the methods 
}
public class MyCalculator : ICalculator
{
   public int Add(int arg1, int arg2)
   {
      return arg1 + arg2;
   }
   //Rest of the methods 
}

一般方法

在 C# 2.0 中,方法可以定义特定于其执行范围的一般类型参数:

public class MyClass
{
   public void MyMethod(X x)
   {...}
}

这是一种重要的功能,因为它使您可以每次用不同的类型调用该方法,而这对于实用工具类非常方便。

即使包含类根本不使用泛型,您也可以定义方法特定的一般类型参数:

public class MyClass
{
   public void MyMethod(T t)
   {...}
}

该功能仅适用于方法。属性或索引器只能使用在类的作用范围中定义的一般类型参数。

在调用定义了一般类型参数的方法时,您可以提供要在调用场所使用的类型:

MyClass obj = new MyClass();
obj.MyMethod(3);

因此,当调用该方法时,C# 编译器将足够聪明,从而基于传入的参数的类型推断出正确的类型,并且它允许完全省略类型规范:

MyClass obj = new MyClass();
obj.MyMethod(3);

该功能称为一般类型推理。请注意,编译器无法只根据返回值的类型推断出类型:

public class MyClass
{
   public T MyMethod()
   {}
}
MyClass obj = new MyClass();
int number = obj.MyMethod();//Does not compile 

当方法定义它自己的一般类型参数时,它还可以定义这些类型的约束:

public class MyClass
{
   public void SomeMethod(T t) where T : IComparable 
   {...}
}

但是,您无法为类级别一般类型参数提供方法级别约束。类级别一般类型参数的所有约束都必须在类作用范围中定义。

在重写定义了一般类型参数的虚拟方法时,子类方法必须重新定义该方法特定的一般类型参数:

public class BaseClass
{
   public virtual void SomeMethod(T t)
   {...}
}
public class SubClass : BaseClass
{
   public override void SomeMethod(T t)
   {...}
}

子类实现必须重复在基础方法级别出现的所有约束:

public class BaseClass
{
   public virtual void SomeMethod(T t) where T : new()
   {...}
}
public class SubClass : BaseClass
{
   public override void SomeMethod(T t) where T : new()
   {...}
}

请注意,方法重写不能定义没有在基础方法中出现的新约束。

此外,如果子类方法调用虚拟方法的基类实现,则它必须指定要代替一般基础方法类型参数使用的类型实参。您可以自己显式指定它,或者依靠类型推理(如果可用):

public class BaseClass
{ 
   public virtual void SomeMethod(T t)
   {...}
}
public class SubClass : BaseClass
{
   public override void SomeMethod(T t)
   {
      base.SomeMethod(t);
      base.SomeMethod(t);
   }
}

一般静态方法

C# 允许定义使用一般类型参数的静态方法。但是,在调用这样的静态方法时,您需要在调用场所为包含类提供具体的类型,如下面的示例所示:

public class MyClass
{
   
   public static T SomeMethod(T t)
   
   {...}
}
int number = MyClass.SomeMethod(3); 

静态方法可以定义方法特定的一般类型参数和约束,就像实例方法一样。在调用这样的方法时,您需要在调用场所提供方法特定的类型 — 可以按如下方式显式提供:

public class MyClass
{
   public static T SomeMethod(T t,X x)
   {..}
}
int number = MyClass.SomeMethod(3,"AAA");

或者依靠类型推理(如果可能):

int number = MyClass.SomeMethod(3,"AAA");

一般静态方法遵守施加于它们在类级别使用的一般类型参数的所有约束。就像实例方法一样,您可以为由静态方法定义的一般类型参数提供约束:

public class MyClass
{
   public static T SomeMethod(T t) where T : IComparable 
   {...}
}

C# 中的运算符只是静态方法而已,并且 C# 允许您为自己的一般类型重载运算符。假设代码块 3 的一般 LinkedList 提供了用于串联链表的 + 运算符。+ 运算符使您能够编写下面这段优美的代码:

LinkedList list1 = new LinkedList();
LinkedList list2 = new LinkedList();
 ...
LinkedList list3 = list1+list2;

代码块 7 显示 LinkedList 类上的一般 + 运算符的实现。请注意,运算符不能定义新的一般类型参数。

代码块 7. 实现一般运算符

public class LinkedList
{
   public static LinkedList operator+(LinkedList lhs,
                                           LinkedList rhs)
   {
      return concatenate(lhs,rhs);
   }
   static LinkedList concatenate(LinkedList list1,
                                      LinkedList list2)
   {
      LinkedList newList = new LinkedList(); 
      Node current;
      current = list1.m_Head;
      while(current != null)
      {
         newList.AddHead(current.Key,current.Item);
         current = current.NextNode;
      }
      current = list2.m_Head;
      while(current != null)
      {
         newList.AddHead(current.Key,current.Item);
         current = current.NextNode;
      }
      return newList;
   }
   //Rest of LinkedList
}

一般委托

在某个类中定义的委托可以利用该类的一般类型参数。例如:

public class MyClass
{
   public delegate void GenericDelegate(T t);
   public void SomeMethod(T t)
   {...} 
}

在为包含类指定类型时,也会影响到委托:

MyClass obj = new MyClass();
MyClass.GenericDelegate del;

del = new MyClass.GenericDelegate(obj.SomeMethod);
del(3);

C# 2.0 使您可以将方法引用的直接分配转变为委托变量:

MyClass obj = new MyClass();
MyClass.GenericDelegate del;

del = obj.SomeMethod;

我将把该功能称为委托推理。编译器能够推断出您分配到其中的委托的类型,查明目标对象是否具有采用您指定的名称的方法,并且验证该方法的签名匹配。然后,编译器创建所推断出的参数类型(包括正确的类型而不是一般类型参数)的新委托,并且将新委托分配到推断出的委托中。

像类、结构和方法一样,委托也可以定义一般类型参数:

public class MyClass
{
   public delegate void GenericDelegate(T t,X x);
}

在类的作用范围外部定义的委托可以使用一般类型参数。在该情况下,在声明和实例化委托时,必须为其提供类型实参:

public delegate void GenericDelegate(T t);

public class MyClass
{
   public void SomeMethod(int number)
   {...}
}

MyClass obj = new MyClass();
GenericDelegate del; 

del = new GenericDelegate(obj.SomeMethod);
del(3);

另外,还可以在分配委托时使用委托推理:

MyClass obj = new MyClass();
GenericDelegate del; 

del = obj.SomeMethod;

当然,委托可以定义约束以伴随它的一般类型参数:

public delegate void MyDelegate(T t) where T : IComparable;

委托级别约束只在使用端实施(在声明委托变量和实例化委托对象时),类似于在类型或方法的作用范围中实施的其他任何约束。

一般委托对于事件尤其有用。您可以精确地定义一组有限的一般委托(只按照它们需要的一般类型参数的数量进行区分),并且使用这些委托来满足所有事件处理需要。代码块 8 演示了一般委托和一般事件处理方法的用法。

代码块 8. 一般事件处理

public delegate void GenericEventHandler (S sender,A args);
public class MyPublisher
{
   public event GenericEventHandler  MyEvent;
   public void FireEvent()
   {
      MyEvent(this,EventArgs.Empty);
   }
}
public class MySubscriber //Optional: can be a specific type
{
   public void SomeMethod(MyPublisher sender,A args)
   {...}
}
MyPublisher publisher = new MyPublisher();
MySubscriber subscriber = new MySubscriber();
publisher.MyEvent += subscriber.SomeMethod;

代码块 8 使用名为 GenericEventHandler 的一般委托,它接受一般发送者类型和一般类型参数。显然,如果您需要更多的参数,则可以简单地添加更多的一般类型参数,但是我希望模仿按如下方式定义的 .NET EventHandler 来设计 GenericEventHandler

public void delegate EventHandler(object sender,EventArgs args);

EventHandler 不同,GenericEventHandler 是类型安全的(如代码块 8 所示),因为它只接受 MyPublisher 类型的对象(而不是纯粹的 Object)作为发送者。实际上,.NET 已经在 System 命名空间中定义了一般样式的 EventHandler

public void delegate EventHandler(object sender,A args) where A : EventArgs;

泛型和反射

在 .NET 2.0 中,扩展了反射以支持一般类型参数。类型 Type 现在可以表示带有特定类型实参(称为绑定类型)或未指定(未绑定)类型的一般类型。像 C# 1.1 中一样,您可以通过使用 typeof 运算符或者通过调用每个类型支持的 GetType() 方法来获得任何类型的 Type。不管您选择哪种方式,都会产生相同的 Type。例如,在以下代码示例中,type1 与 type2 完全相同。

LinkedList list = new LinkedList();

Type type1 = typeof(LinkedList);
Type type2 = list.GetType();
Debug.Assert(type1 == type2);

typeofGetType() 都可以对一般类型参数进行操作:

public class MyClass 
{
   public void SomeMethod(T t)
   {
      Type type = typeof(T);
      Debug.Assert(type == t.GetType());
   }
}

此外,typeof 运算符还可以对未绑定的一般类型进行操作。例如:

public class MyClass 
{}
Type unboundedType = typeof(MyClass<>);
Trace.WriteLine(unboundedType.ToString());
//Writes: MyClass`1[T]

所追踪的数字 1 是所使用的一般类型的一般类型参数的数量。请注意空 <> 的用法。要对带有多个类型参数的未绑定一般类型进行操作,请在 <> 中使用“,”:

public class LinkedList 
{...}
Type unboundedList = typeof(LinkedList<,>);
Trace.WriteLine(unboundedList.ToString());
//Writes: LinkedList`2[K,T]

Type 具有新的方法和属性,用于提供有关该类型的一般方面的反射信息。代码块 9 显示了新方法。

代码块 9. Type 的一般反射成员

public abstract class Type : //Base types
{
   public virtual bool ContainsGenericParameters{get;}
   public virtual int GenericParameterPosition{get;}
   public virtual bool HasGenericArguments{get;}
   public virtual bool IsGenericParameter{get;}
   public virtual bool IsGenericTypeDefinition{get;}
   public virtual Type BindGenericParameters(Type[] typeArgs);
   public virtual Type[] GetGenericArguments();
   public virtual Type GetGenericTypeDefinition();
   //Rest of the members
}

上述新成员中最有用的是 HasGenericArguments 属性,以及 GetGenericArguments()GetGenericTypeDefinition() 方法。Type 的其余新成员用于高级的且有点深奥的方案,这些方案超出了本文的范围。

正如它的名称所指示的那样,如果由 Type 对象表示的类型使用一般类型参数,则 HasGenericArguments 被设置为 true。GetGenericArguments() 返回与所使用的类型参数相对应的 Type 数组。GetGenericTypeDefinition() 返回一个表示基础类型的一般形式的 Type。代码块 10 演示如何使用上述一般处理 Type 成员获得有关代码块 3 中的 LinkedList 的一般反射信息。

代码块 10. 使用 Type 进行一般反射

LinkedList list = new LinkedList();

Type boundedType = list.GetType();
Trace.WriteLine(boundedType.ToString());
//Writes: LinkedList`2[System.Int32,System.String]

Debug.Assert(boundedType.HasGenericArguments);

Type[] parameters = boundedType.GetGenericArguments();

Debug.Assert(parameters.Length == 2);
Debug.Assert(parameters[0] == typeof(int));
Debug.Assert(parameters[1] == typeof(string));

Type unboundedType = boundedType.GetGenericTypeDefinition();
Debug.Assert(unboundedType == typeof(LinkedList<,>));
Trace.WriteLine(unboundedType.ToString());
//Writes: LinkedList`2[K,T]

与 Type 类似,MethodInfo 和它的基类 MethodBase 具有反射一般方法信息的新成员。

与 C# 1.1 中一样,您可以使用 MethodInfo(以及很多其他选项)进行晚期绑定调用。但是,您为晚期绑定传递的参数的类型,必须与取代一般类型参数而使用的绑定类型(如果有)相匹配:

LinkedList list = new LinkedList();
Type type = list.GetType();
MethodInfo methodInfo = type.GetMethod("AddHead");
object[] args = {1,"AAA"};
methodInfo.Invoke(list,args);

属性和泛型

在定义属性时,可以使用枚举 AttributeTargets 的新 GenericParameter 值,通知编译器属性应当以一般类型参数为目标:

[AttributeUsage(AttributeTargets.GenericParameter)] 
public class SomeAttribute : Attribute
{...}

请注意,C# 2.0 不允许定义一般属性。

//Does not compile:
public class SomeAttribute : Attribute 
{...}

然而,属性类可以通过使用一般类型或者定义 Helper 一般方法(像其他任何类型一样)在内部利用泛型:

public class SomeAttribute : Attribute
{
   void SomeMethod(T t)
   {...}
   LinkedList m_List = new LinkedList();
}

泛型和 .NET Framework

为了对本文做一下小结,下面介绍 .NET 中除 C# 本身以外的其他一些领域如何利用泛型或者与泛型交互。

System.Array 和泛型

System.Array 类型通过很多一般静态方法进行了扩展。这些一般静态方法专门用于自动执行和简化处理数组的常见任务,例如,遍历数组并且对每个元素执行操作、扫描数组,以查找匹配某个条件(谓词)的值、对数组进行变换和排序等等。代码块 11 是这些静态方法的部分清单。

代码块 11. System.Array 的一般方法

public abstract class Array
{
   //Partial listing of the static methods:  
   public static IList AsReadOnly(T[] array);
   public static int BinarySearch(T[] array, T value);
   public static int BinarySearch(T[] array, T value,
                                     IComparer comparer);
   public static U[] ConvertAll(T[] array, 
                                Converter converter);
   public static bool Exists(T[] array,Predicate match);
   public static T Find(T[] array,Predicate match);
   public static T[] FindAll(T[] array, Predicate match);
   public static int FindIndex(T[] array, Predicate match);
   public static void ForEach(T[] array, Action action);
   public static int  IndexOf(T[] array, T value);
   public static void Sort(K[] keys, V[] items,
                                IComparer comparer);
   public static void Sort(T[] array,Comparison comparison) 
}

System.Array 的静态一般方法都使用 System 命名空间中定义的下列四个一般委托:

public delegate void Action(T t);
public delegate int Comparison(T x, T y);
public delegate U Converter(T from);
public delegate bool Predicate(T t);

代码块 12 演示如何使用这些一般方法和委托。它用从 1 到 20 的所有整数初始化一个数组。然后,代码通过一个匿名方法和 Action 委托,使用 Array.ForEach() 方法来跟踪这些数字。使用第二个匿名方法和 Predicate 委托,代码通过调用 Array.FindAll() 方法(它返回另一个相同的一般类型的数组),来查找该数组中的所有质数。最后,使用相同的 Action 委托和匿名方法来跟踪这些质数。请注意代码块 12 中类型参数推理的用法。您在使用静态方法时无须指定类型参数。

代码块 12. 使用 System.Array 的一般方法

int[] numbers = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
Action trace =  delegate(int number)
                     {
                        Trace.WriteLine(number);
                     };
Predicate isPrime =   delegate(int number)
                           {
                              switch(number)
                              {
                                case 1:case 2:case 3:case 5:case 7:                                 
                                case 11:case 13:case 17:case 19:
                                    return true;
                                 default:
                                    return false;
                              }
                           };
Array.ForEach(numbers,trace);
int[] primes = Array.FindAll(numbers,isPrime);
Array.ForEach(primes,trace);

System.Collections.Generic 命名空间中定义的类 List 中,也可以得到类似的一般方法。这些方法使用四个相同的一般委托。实际上,您还可以在您的代码中利用这些委托,如以下部分所示。

静态集合类

尽管 System.ArrayList 都提供了能够大大简化自身使用方式的、方便的实用工具方法,但 .NET 没有为其他集合提供这样的支持。为了对此进行补偿,本文随附的源代码包含了静态 Helper 类 Collection,其定义如下所示:

public static class Collection
{
   public static IList AsReadOnly(IEnumerable collection);
   public static U[] ConvertAll(IEnumerable collection,
                                     Converter converter);
   public static bool Contains(IEnumerable collection,T item) 
                                               where T : IComparable;
   public static bool Exists(IEnumerable collection,Predicate); 
   public static T Find(IEnumerable collection,Predicate match);
   public static T[] FindAll(IEnumerable collection,
                                Predicate match);
   public static int FindIndex(IEnumerable collection,T value) 
                                                where T : IComparable;
   public static T FindLast(IEnumerable collection,
                               Predicate match);
   public static int FindLastIndex(IEnumerable collection,T value) 
                                                where T : IComparable;
   public static void ForEach(IEnumerable collection,Action action);
   public static T[] Reverse(IEnumerable collection);
   public static T[] Sort(IEnumerable collection);
   public static T[] ToArray(IEnumerable collection);
   public static bool TrueForAll(IEnumerable collection,
                                    Predicate match);
   //Overloaded versions for IEnumerator 
}

Collection 的实现简单易懂。例如,以下为 ForEach() 方法:

public static void ForEach(IEnumerator iterator,Action action)
{
   /* Some parameter checking here, then: */
   while(iterator.MoveNext())
   {
      action(iterator.Current);
   }
}

Collection 静态类的用法非常类似于 ArrayList,它们都利用相同的一般委托。您可以将 Collection 用于任何集合,只要该集合支持 IEnumerableIEnumerator

Queue queue = new Queue();
//Some code to initialize queue
Action trace =  delegate(int number)
                     {
                        Trace.WriteLine(number);
                     };
Collection.ForEach(queue,trace);

一般集合

System.Collections 中的数据结构全部都是基于 Object 的,因而继承了本文开头描述的两个问题,即性能较差和缺少类型安全。.NET 2.0 在 System.Collections.Generic 命名空间中引入了一组一般集合。例如,有一般的 Stack 类和一般的 Queue 类。Dictionary 数据结构等效于非一般的 HashTable,并且还有一个有点像 SortedListSortedDictionary 类。类 List 类似于非一般的 ArrayList。表 1 将 System.Collections.Generic 的主要类型映射到 System.Collections 中的那些主要类型。

表 1. 将 System.Collections.Generic 映射到 System.Collections
System.Collections.Generic System.Collections

Comparer

Comparer

Dictionary

HashTable

LinkedList

-

List

ArrayList

Queue

Queue

SortedDictionary

SortedList

Stack

Stack

ICollection

ICollection

IComparable

System.IComparable

IDictionary

IDictionary

IEnumerable

IEnumerable

IEnumerator

IEnumerator

IList

IList

System.Collections.Generic 中的所有一般集合还实现了一般的 IEnumerable 接口,该接口的定义如下所示:

public interface IEnumerable
{
   IEnumerator GetEnumerator();
} 
public interface IEnumerator : IDisposable
{
   T Current{get;}
   bool MoveNext();
}
      

简单说来,IEnumerable 提供了对 IEnumerator 迭代器接口的访问,该接口用于对集合进行抽象迭代。所有集合都在嵌套结构上实现了 IEnumerable,其中,一般类型参数 T 是集合存储的类型。

特别有趣的是,词典集合定义它们的迭代器的方式。词典实际上是两个类型(而非一个类型)的一般参数(键和值)集合。System.Collection.Generic 提供了一个名为 KeyValuePair 的一般结构,其定义如下所示:

struct KeyValuePair
{
   public KeyValuePair(K key,V value);
   public K Key(get;set;)
   public V Value(get;set;)
}

KeyValuePair 简单地存储一般键和一般值组成的对。该结构就是词典作为集合进行管理的类型,并且是它用于实现它的 IEnumerable 的类型。Dictionary 类将一般 KeyValuePair 结构指定为 IEnumerableICollection 的项参数:

public class Dictionary : IEnumerable<KEYVALUEPAIR>, 
                               ICollection<KEYVALUEPAIR>, 
                               //More interfaces
{...} 

KeyValuePair 中使用的键和值类型参数当然是词典自己的一般键和值类型参数。您无疑可以在您自己的使用键和值对的一般数据结构中完成同样的工作。例如:

public class LinkedList : IEnumerable<KEYVALUEPAIR> where K : IComparable
{...} 

序列化和泛型

.NET 允许您具有可序列化的一般类型:

[Serializable]
public class MyClass
{...}

在序列化类型时,除了持久保持对象成员的状态以外,.NET 还持久保持有关该对象及其类型的元数据。如果可序列化的类型是一般类型并且它包含绑定类型,则有关该一般类型的元数据还包含有关该绑定类型的类型信息。因此,一般类型的每个带有特定参数类型的变形都被视为唯一的类型。例如,您不能将对象类型 MyClass 序列化(而只能将其反序列化)为 MyClass 类型的对象。序列化一般类型的实例与序列化非一般类型没有什么不同。但是,在反序列化该类型时,您需要通过匹配的特定类型声明变量,并且在向下强制转换从 Deserialize 返回的 Object 时再次指定这些类型。代码块 13 显示了一般类型的序列化和反序列化。

代码块 13. 一般类型的客户端序列化

[Serializable]
public class MyClass
{...}
MyClass obj1 = new MyClass();

IFormatter formatter = new BinaryFormatter();?
Stream stream = new FileStream("obj.bin",FileMode.Create,FileAccess.ReadWrite); 
using(stream)
{
   formatter.Serialize(stream,obj1);
   stream.Seek(0,SeekOrigin.Begin);

   MyClass obj2; 
   obj2 = (MyClass)formatter.Deserialize(stream);
}

请注意,IFormatter 是基于对象的。您可以通过定义 IFormatter 的一般版本进行补偿:

public interface IGenericFormatter
{
   T Deserialize(Stream serializationStream);
   void Serialize(Stream serializationStream,T graph);
}

您可以通过包含一个基于对象的格式化程序来实现 IGenericFormatter

public class GenericFormatter : IGenericFormatter 
                                   where F : IFormatter,new()
{
   IFormatter m_Formatter = new F();
   public T Deserialize(Stream serializationStream)
   {
      return (T)m_Formatter.Deserialize(serializationStream);
   }
   public void Serialize(Stream serializationStream,T graph)
   {
      m_Formatter.Serialize(serializationStream,graph);
   }
}

请注意一般类型参数 F 上的两个约束的用法。尽管可以原样使用 GenericFormatter<F>

   
using GenericBinaryFormatter = GenericFormatter;
using GenericSoapFormatter2  = GenericFormatter;

但是,您还可以将该格式化程序强类型化以使用:

public sealed class GenericBinaryFormatter : GenericFormatter
{}
public sealed class GenericSoapFormatter : GenericFormatter
{}

强类型化定义的优点是可以跨文件和程序集共享它,这与 using 别名相反。

代码块 14代码块 13 相同,唯一的不同之处在于它使用一般格式化程序:

代码块 14. 使用 IGenericFormatter

[Serializable]
public class MyClass
{...}
MyClass obj1 = new MyClass();

IGenericFormatter formatter = new GenericBinaryFormatter();?
Stream stream = new FileStream("obj.bin",FileMode.Create,FileAccess.ReadWrite); 
using(stream)
{
   formatter.Serialize(stream,obj1);
   stream.Seek(0,SeekOrigin.Begin);
   MyClass obj2; 
   obj2 = formatter.Deserialize(stream);
}

泛型和远程处理

可以定义和部署利用泛型的远程类,并且可以使用编程或管理配置。请考虑使用泛型并且派生自 MarshalByRefObject 的类 MyServer

public class MyServer : MarshalByRefObject
{...}

只有当类型参数 T 是可封送的对象时,您才能通过远程处理访问该类。这意味着 T 是可序列化的类型或者派生自 MarshalByRefObject。您可以通过将 T 约束为派生自 MarshalByRefObject 来实施这一要求。

public class MyServer : MarshalByRefObject 
                           where T : MarshalByRefObject
{...}

在使用管理类型注册时,您需要指定要取代一般类型参数而使用的确切类型实参。您必须以与语言无关的方式命名这些类型,并且提供完全限定命名空间。例如,假设类 MyServer 在命名空间 RemoteServer 中的程序集 ServerAssembly 中定义,并且您希望在客户端激活模式下将其与整型而不是一般类型参数 T 一起使用。在该情况下,配置文件中必需的客户端类型注册条目应该是:

<client url="...some url goes here..."> 
   <activated type="RemoteServer.MyServer<b>[[System.Int32]]</b>,ServerAssembly"/>
</client> 

配置文件中的匹配主机端类型注册条目是:

<service> 
   <activated type="RemoteServer.MyServer<b>[[System.Int32]]</b>,ServerAssembly"/> 
</service> 

双方括号用来指定多个类型。例如:

LinkedList[[System.Int32],[System.String]]

在使用编程配置时,您可以用类似于 C# 1.1 的方式配置激活模式和类型注册,不同之处在于,当定义远程对象的类型时,您必须提供类型实参而不是一般类型参数。例如,对于主机端激活模式和类型注册,您可以编写如下代码:

Type serverType = typeof(MyServer);
RemotingConfiguration.RegisterActivatedServiceType(serverType);

对于客户端类型激活模式和位置注册,具有以下代码:

Type serverType = typeof(MyServer);      
string url = ...; //some url initialization 
RemotingConfiguration.RegisterWellKnownClientType(serverType,url);

当实例化远程服务器时,只须提供类型参数,就好像您在使用本地一般类型一样:

MyServer obj;
obj = new MyServer();
//Use obj

除了使用 new 以外,客户端还可以选择使用 Activator 类的方法来连接到远程对象。在使用 Activator.GetObject() 时,您需要提供要使用的类型实参,并且在显式强制转换返回的 Object 时提供实参类型:

string url = ...; //some url initialization 
Type serverType = typeof(MyServer);
MyServer obj;
obj = (MyServer)Activator.GetObject(serverType,url);
//Use obj

您还可以将 Activator.CreateInstance() 与一般类型一起使用:

Type serverType = typeof(MyServer);
MyServer obj;
obj = (MyServer)Activator.CreateInstance(serverType);
//Use obj

实际上,Activator 还提供 CreateInstance() 的一般版本,定义如下:

T CreateInstance<T>();

CreateInstance() 的使用方式类似于非一般方法,只是添加了类型安全的好处:

Type serverType = typeof(MyServer);
MyServer obj;
obj = Activator.CreateInstance(serverType);
//Use obj

泛型无法完成的工作

在 .NET 2.0 下,您不能定义一般 Web 服务,即使用一般类型参数的 Web 方法。原因是没有哪个 Web 服务标准支持一般服务。

您还不能在服务组件上使用一般类型。原因是泛型不能满足 COM 可见性要求,而该要求对于服务组件而言是必需的(就像您无法在 COM 或 COM+ 中使用 C++ 模板一样)。

小结

C# 泛型是开发工具库中的一个无价之宝。它们可以提高性能、类型安全和质量,减少重复性的编程任务,简化总体编程模型,而这一切都是通过优雅的、可读性强的语法完成的。尽管 C# 泛型的根基是 C++ 模板,但 C# 通过提供编译时安全和支持将泛型提高到了一个新水平。C# 利用了两阶段编译、元数据以及诸如约束和一般方法之类的创新性的概念。毫无疑问,C# 的将来版本将继续发展泛型,以便添加新的功能,并且将泛型扩展到诸如数据访问或本地化之类的其他 .NET Framework 领域。

Juval Lowy 是 IDesign(一家咨询和培训公司)的软件架构师和负责人。Juval 是 Microsoft 的 Silicon Valley 地区主管,他与 Microsoft 一起来帮助整个行业采用 .NET。他的最新著作是 Programming .NET Components 2nd Edition (O'Reilly, 2005)。该书专门论述面向组件的编程和设计以及相关的系统问题。Juval 参加了 Microsoft 对 .NET 的将来版本进行的内部设计审查,并且频繁主持开发会议。Microsoft 将 Juval 视为软件传奇人物以及全球顶尖 .NET 专家与行业领导人之一。您可以通过 http://www.idesign.net 与 Juval 联系。

转到原英文页面

posted on 2005-06-28 09:12  Cdo  阅读(444)  评论(0编辑  收藏  举报