现在的位置: 首页 > 综合 > 正文

深入了解泛型

2013年08月11日 ⁄ 综合 ⁄ 共 23455字 ⁄ 字号 评论关闭

C# 泛型简介

Juval Lowy
IDesign

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

下载 GenericsInCSharp.msi 示例文件

本文假定读者熟悉 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 Integernumber = 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:              

抱歉!评论已关闭.