CLR的范型解秘(来自MSDN中的技术文章)

本文介绍了CLR中的泛型概念,探讨了其带来的灵活性、代码重用和性能提升,并通过示例展示了C#中的泛型语法。此外,还深入讨论了CLR内部如何处理泛型,包括类型约束、泛型接口和委托,以及即将推出的泛型类库。
CLR 中的泛型简介
发布日期 : 1/11/2005 | 更新日期 : 1/11/2005

Jason Clark

在本专栏的 上一期中,我通过 P/Invoke 介绍了 Interop 和非托管代码。此主题通过展示托管代码如何能访问旧有的 Win32® 代码,在某些方面回顾了过去的情况。与此相反,本月我将要通过一个酷炫的全新功能泛型来展望未来,该功能将很快会出现在公共语言运行库 (CLR) 中。我将对泛型进行简要介绍,并讨论其给您的代码所带来的好处。在以后的专栏中,我将深入讨论编译器和运行库如何实现泛型的更多细节。

泛型是对 CLR 类型系统的扩展,它允许开发人员定义那些未指定某些细节的类型。相反,当用户代码引用该代码时,就会指定这些细节。引用泛型的代码填充缺少的细节,并根据其特定需求对类型进行调整。泛型的命名反映了该功能的目标:允许在编写代码时不指定可能限制其使用范围的细节。代码本身就是泛型。稍后,我会对它进行更详细的介绍。

还需要多长时间才能提供泛型?Microsoft 计划在发布 CLR 代码代号为 "Whidbey" 时提供泛型,在本专栏出版之后,应当会发布 Whidbey CLR 的测试版。同时,在 CLR 的测试版中,预计会对语言和编译器进行更新,以便充分利用泛型。最后,为了包含泛型支持,Microsoft 的研究组已经修改了共享源公共语言实现 (CLI) — 代码代号为 "Rotor"。修改后的运行库(即,代码代号为 "Gyro")位于 http://research.microsoft.com/projects/clrgen中。

泛型预览

正如使用任何新技术一样,明白它的好处所在会有所帮助。那些熟悉 C++ 模板的用户将会发现,泛型在托管代码中具有相似的用途。但是,我不愿意对 CLR 泛型和 C++ 模板进行过多比较,因为泛型具有一些额外的好处,它不存在以下两个常见问题:代码臃肿和开发人员混淆。

CLR 泛型具有一些好处,如编译时类型安全、二进制代码重用性能和清晰性。我将简要介绍这些好处,您在阅读本专栏的其余文章时,会更详细地了解它们。例如,假设有两个集合类:SortedList(Object 引用的集合)和 GenericSortedList<T>(任意类型的集合)。

类型安全?当用户向 SortedList 类型的集合内添加 String 时,String 会隐式强制转换为 Object。同样,如果从该列表中检索 String 对象,则它必须在运行时从 Object 引用强制转换到 String 引用。这会造成编译时缺少类型安全,从而使开发人员感到厌烦,并且易于出错。相反,如果使用 GenericSortedList<String>(T 的类型被设置为 String),就会使所有的添加和查找方法使用 String 引用。这允许在编译时(而非运行时)指定和检查元素的类型

二进制代码重用?为了进行维护,开发人员可以选择使用 SortedList ,通过从它派生 SortedListOfString 来实现编译时的类型安全。此方法有一个问题,那就是必须对于每个需要类型安全列表的类型都编写新代码,而这会很快变成非常费力的工作。使用 GenericSortedList<T>,需要执行的全部操作就是将具有所需元素类型的类型实例化为 T。泛型代码还有一个附加价值,那就是它在运行时生成,因此,对于无关元素类型的两个扩展(如 GenericSortedList<String> 和 GenericSortedList<FileStream>)能够重新使用同一个实时 (JIT) 编译代码的大部分。CLR 只是处理细节 — 代码不再臃肿!

性能?关键在于:如果类型检查在编译时间进行,而不是在运行时间进行,则性能增强。在托管代码中,引用和值之间的强制转换既会导致装箱又会导致取消装箱,而且避免这样的强制转换可能会对性能产生同样的负面影响。最近针对一个由一百万个整数组成的数组进行了快速排序法基准测试,结果表明泛型方法比非泛型方法快三倍。这是由于完全避免了对这些值进行装箱。如果针对由字符串引用组成的数组进行同样的排序,则由于无需在运行时执行类型检查,因此使用泛型方法后性能提高了 20%。

清晰性?泛型的清晰性体现在许多方面。约束是泛型的一个功能,它们会禁止对泛型代码进行不兼容的扩展;使用泛型,您将不再面临那些困扰 C++ 模板用户的含混不清的编译器错误。在 GenericSortedList<T> 示例中,集合类将有一个约束,该约束使集合类只处理可进行比较并依此进行排序的 T 类型。同样,通常可以使用名为类型推理的功能来调用泛型方法,而无需使用任何特殊语法。当然,编译时类型安全可以使应用程序代码更加清晰。 我将在本专栏中详细介绍约束、类型推理和类型安全。

一个简单的示例

Whidbey CLR 版本将通过类库中的一套泛型集合类来提供这些现成的好处。但是,可通过为应用程序定义其自己的泛型代码,使其进一步受益于泛型。为了解释这是如何完成的,我将首先修改一个简单的链接列表节点类,使其成为泛型类类型。

图 1 中的 Node 类只是包括一些基本内容。它有两个字段:m_data(引用节点的数据)和 m_data(引用链接列表中的下一项)。这两个字段都是由构造函数方法设置的。确实只有两个其他点缀性功能,第一个功能是通过名为 Data 和 Next 的只读属性访问 m_data 和 m_next 字段。第二个功能是对 System.Object 的 ToString 虚拟方法进行重写。

图 1 还显示了使用 Node 类的代码。该引用代码会受到某些限制。问题在于,为了能在许多上下文中使用,其数据必须为最基本的类型,即 System.Object。这意味着使用 Node 时,就会失去任何形式的编译时类型安全。使用 Object 意味着算法或数据结构中的“任意类型”会强迫所使用的代码在 Object 引用和实际数据类型之间进行强制转换。应用程序中的任何类型不匹配错误只有在运行之后才被捕获。如果在运行时尝试进行强制转换,这些错误会采用 InvalidCastException 形式。

此外,如果要向 Object 引用赋予任何基元值(如 Int32),则需要对实例进行装箱。装箱涉及到内存分配和内存复制,以及最后对已装箱值进行的垃圾回收。最后,正如可在 图 1 中看到的那样,从 Object 引用强制转换为值类型(如 Int32)会导致取消装箱(也包括类型检查)。 由于装箱和取消装箱会损害该算法的整体性能,因此您会明白为什么用 Object 就意味着“任何类型”都具有一定的缺点。

使用泛型重写 Node 是解决这些问题的完美方法。让我们看一下 图 2 中的代码,您将发现 Node 类型被重写为 Node<T> 类型。具有泛型行为的类型(如 Node<T>)是参数化类型,并且可被称作 Parameterized Node、Node of T 或泛型Node。稍后我将介绍这个新的 C# 语法;让我们首先深入研究一下 Node<T> 与 Node 有何不同。

Node<T> 类型与 Node 类型在功能和结构上相似。二者均支持为任何给定类型的数据构建链接列表。但是,Node 使用 System.Object 来表示“任意类型”,而 Node<T> 不指定该类型。相反,Node<T> 使用名为 T 且作为类型占位符的类型参数。当使用者代码使用 Node<T> 时,名为 T 的类型参数最终由 Node<T> 的参数来指定。

图 3 中的代码使用了具有 32 位带符号整数的 Node<T>,这是通过构造类似类型名称:Node<Int32> 来实现的。在本例中,Int32 是类型参数 T 的类型变量。(顺便说一句,C# 还将接受Node<int>,以便将 T 指示为 Int32。) 如果该代码需要某种其他类型(如 String 引用)的链接列表,则这可通过将它指定为 T 的类型变量来完成,例如:Node<String>。

Node<T> 的好处在于:它的算法行为可被明确定义,而它所操作的数据类型仍保持未指定状态。因此,Node<T> 类型在工作方式上是具体的;而泛型在所处理的内容方面又是具体的。总之,诸如链接列表应当拥有的数据类型等细节最好留给使用 Node<T> 的代码来指定。

在讨论泛型时,最好先明确两种角色:定义代码和引用代码。定义代码包括既声明泛型代码存在又定义类型成员(如方法和字段)的代码。 图 2 中显示的是类型 Node<T> 的定义代码。引用代码是用户代码,它使用预定义的泛型代码,并且该代码还可以内置到另一个程序集中。 图 3 是 Node<T> 的引用代码示例。

考虑定义代码和引用代码非常有用,原因在于这两种角色都在实际的可使用泛型代码构造中起着一定的作用。 图 3 中的引用代码使用 Node<T> 来构造一个名为 Node<T> 的新类型。Node<Int32> 是一个截然不同的类型,它由以下两个关键成分构建而成:Node<T>(由定义代码创建),参数 T 的类型变量 Int32(由引用代码指定)。只有使用这两个成分才能使泛型代码变得完整。

请注意,从面向对象的派生角度看,泛型类型(如 Node<T>)以及从泛型类型构造的类型(如 Node<Int32> 或 Node<String>)并不是相关类型。类型 Node<Int32>、Node<String> 和 Node<T> 类型是同辈,它们都是从 System.Object 直接派生而来。

C# Generic 语法

CLR 支持多种编程语言,因此,CLR 泛型将有多种语法。但是,无论采用哪种语法,用一种面向 CLR 的语言编写的泛型代码将可以由其他面向 CLR 的语言编写的程序使用。我将在本文中介绍 C# 语法,其原因是,在编写本文时,在三种较大的托管语言中,泛型的 C# 语法相当稳定。 但是,没有必要在 Visual Basic®.NET 和 Managed C++ 的 Whidbey 版本中支持泛型。

图 4 显示了泛型定义代码和泛型引用代码的基本 C# 语法。二者的语法区别反映了泛型代码所涉及的双方的不同职责。

目前的计划是让 CLR(从而让 C#)支持泛型类、结构、方法、接口和委托。 图 4 的左侧显示了每种定义代码情况的 C# 语法示例。.请注意,尖括号表示类型参数列表。尖括号紧跟在泛型类型或成员的名称后面。同样,在类型参数列表中有一个或多个类型参数。参数还出现在泛型代码的整个定义中,用来替代特定的 CLR 类型或作为类型构造函数的参数。 图 4 的右侧显示了与之相匹配的引用代码情况的 C# 语法示例。请注意,在此处,类型变量括在尖括号中;泛型标识符和括号构成一个截然不同的新标识符。另外还要注意,类型变量指定在从泛型构造类型或方法时所使用的类型。

让我们花一点时间来定义代码语法。当编译器遇到一个由尖括号分开的类型参数列表时,它可识别出您在定义泛型类型或方法。泛型定义中的尖括号紧跟在所定义的类型或方法的名称后面。

类型-参数列表指出要在泛型代码定义中保持未指定状态的一个或多个类型。类型参数的名称可以是 C# 中任何有效的标识符,它们可用逗号隔开。对于 图 4中“定义代码”部分中的类型参数,需要注意下面一些事项:

  • 在每个代码示例中,可以看到在整个定义中(通常将出现类型名称的位置)均使用了类型参数 T 或 U。

  • 在 IComparable<T> 接口示例中,可以看到同时使用类型参数 T 和常规类型 Int32。在泛型代码的定义中,既可以使用未指定的类型(通过类型参数)又可以使用指定的类型(使用 CLR 类型名称)。

  • 在 Node<T> 示例中,可以看到,类型参数 T 可以像在 m_data 的定义中一样独立使用,还可以像在 m_next 中一样用作另一个类型构造的一部分。用作另一个泛型类型定义的变量的类型参数(如 Node<T>),称作开放式泛型类型。用作类型参数的具体类型(如 Node<System.Byte>),称作封闭式泛型类型。

  • 与任何泛型方法一样, 图 4 中显示的示例泛型方法 Swap<T> 可以是泛型或非泛型类型的一部分,也可以是实例、虚拟或静态方法。

在本专栏中,我对于类型参数使用的是单字符名称(如 T 和 U),这主要是为了使情况更简单。但是,您会发现也可以使用描述性名称。例如,在产品代码中,Node<T> 类型可被等效地定义为 Node<ItemType> 或 Node<dataType>。

在撰写本文时,Microsoft 已经使库代码中的单字符类型参数名称标准化,以有助于区分这些名称与用于普通类型的名称。我个人比较喜欢在产品代码中使用 camelCasing 类型参数,因为这可将它们与代码中的简单类型名称相区分,而同时又具有一定的描述性。

在泛型引用代码中,未指定的类型会变成指定的类型。如果引用代码实际使用泛型代码,则这是十分必要的。如果您查看 图 4 中“Referencing Code”部分中的示例,就会发现在所有情况中,新类型或方法都是通过将 CLR 类型指定为泛型的类型变量,从一个泛型构造的。在泛型语法中,诸如 Node<Byte> 和 Pair<Byte,String> 之类的代码表示从泛型类型定义构造的新类型的类型名称。

在深入介绍该技术本身之前,我将再介绍一个语法细节。在 图 4 中,当代码调用泛型方法(如 Swap<T> 方法)时,完全限定的调用语法包括任何类型变量。但是,有时可以选择将类型变量从调用语法中排除,如下面的两行代码所示:

Decimal d1 = 0, d2 = 2;
Swap(ref d1, ref d2);

这个简化的调用语法依赖一个名为类型推理的 C# 编译器功能,在该功能中,编译器使用传递给方法的参数类型来推导类型变量。在本例中,编译器从 d1 和 d2 的数据类型来推导,类型参数 T 的类型变量应当为 System.Decimal。如果存在多义性,类型推理对于调用方不工作,并且 C# 编译器将会产生一个错误,建议您使用包含尖括号和类型变量的完整调用语法。

间接

我的一个好朋友喜欢指出,大多数完美的编程解决方案都是围绕添加另一间接层次而设计的。指针和引用允许单个函数影响一个数据结构的多个实例。虚拟函数允许单个调用站点将调用传送到一组相似的方法 — 其中一些方法可在以后定义。这两个间接示例非常常见,以至于程序员通常注意不到间接本身。

间接的主要目的是为了提高代码的灵活性。泛型是一种间接形式,在这种形式中,定义不会产生可直接使用的代码。相反,在定义泛型代码中,会创建一个“代码工厂”。随后,引用代码使用该工厂代码来构造可直接使用的代码。

让我们首先从泛型方法来了解这个设计思路。 图 5 中的代码定义并引用了一个名为 CompareHashCodes<T> 的泛型方法。定义代码创建了一个名为 CompareHashCodes<T> 的泛型方法,但是 图 5 中显示的代码都没有直接调用 CompareHashCodes<T>。相反,在 Main 中,引用代码使用 CompareHashCodes<T> 来构造两种不同的方法:CompareHashCodes<Int32> 和 CompareHashCodes<String>。这些构造方法是 CompareHashCodes<T> 的实例,它们是由引用代码来调用的。

通常会在某个方法的定义中直接定义该方法所执行的操作。与之相反,在泛型方法的定义中,会定义它的构造方法实例将执行的操作。除了充当如何构造特定实例的模型以外,泛型方法本身不执行任何操作。CompareHashCodes<T> 是一种泛型方法,通过它可以构造对哈希代码进行比较的方法实例。 构造实例(如 CompareHashCodes<Int32>)执行实际工作;它对整数的哈希代码进行比较。相反,CompareHashCodes<T> 是一个从可调用中删除的间接层。

泛型类型类似于从与其相对应的简单副本中删除的一个间接层。系统使用简单的类型定义(如类或结构)来创建内存中的对象。例如,类库中的 System.Collection.Stack 类型用于在内存中创建堆栈对象。在某种意义上,可以将 C# 中的新关键字或中间语言代码中的 newobj 指令视为一个对象工厂,该对象工厂在创建对象实例时,将托管类型用作每个对象的蓝图。

另一方面,泛型类型用于实例化封闭式类型,而不是对象实例。随后,可以使用从泛型类型构造的类型来创建对象。让我们回顾一下在 图 2 中定义的 Node<T> 类型以及如 图 3所示的它的引用代码。

托管应用程序永远不能创建 Node<T> 类型的对象,即使它是托管类型时也是如此。这是由于 Node<T> 缺乏足够的定义,因此无法被实例化为内存中的对象。但是,在执行应用程序的过程中,Node<T> 可用于实例化另一个类型。

Node<T> 是一个开放式泛型类型,并且只用于创建其他构造类型。如果使用 Node<T> 创建的构造类型是封闭式类型(如 Node<Int32>),则它可用于创建对象。 图 3 中的引用代码使用 Node<Int32> 的方式与使用简单类型时大体相同。它创建 Node<Int32> 类型的对象,在这些对象上调用方法,等等。

泛型类型额外提供一个间接层,此功能非常强大。采用泛型类型的引用代码会产生定制的托管类型。在脑海中将泛型代码想象为从其简单副本中删除的一个间接层,这有助于凭直觉获知 CLR 中泛型的许多行为、规则和用法。

小结

本文介绍了泛型类型的好处 — 如何使用它们改善类型安全、代码重用和性能。本文还讲述了 C# 中的语法以及泛型如何导致另一层间接,从而提高灵活性。请耐心等待,下次我将更深入地剖析泛型。

请将您的问题和评论发送给 Jason,dot-net@microsoft.com

Jason Clark为 Microsoft 和 Wintellect ( http://www.wintellect.com) 提供培训和咨询服务,他以前是 Windows NT 和 Windows 2000 Server 小组的开发人员。他还与人合著有 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。您可以通过 JClark@Wintellect.com 与 Jason 联系。

 

CLR 中的范型详解
发布日期 : 1/24/2005 | 更新日期 : 1/24/2005

Jason Clark

本页内容

编译器如何处理范型? 编译器如何处理范型?
规则和限制 规则和限制
where T : IComparable where T : IComparable
范型接口和委托 范型接口和委托
类库中的范型 类库中的范型
小结 小结

在本专栏 2003 年 9 月一期中,我初步讨论了公共语言运行库 (CLR) 中的范型。我引入了范型的概念,介绍了范型所带来的灵活性和代码重用,探讨了性能和类型安全,并通过一个简单的代码示例展示了 C# 中的范型语法。本月,我将深入讨论与范型有关的 CLR 内部工作机制。我将介绍类型约束、范型类、方法、结构和即将问世的范型类库。

编译器如何处理范型?


C++ 模板和 Java 语言中提议的范型等效都是它们各自编译器的功能。这些编译器在编译时根据对范型或模板类型的引用来构造代码。这会导致代码臃肿并降低结构之间的类型等效(即使类型变量相同)。相反,CLR 范型不采用这种工作方式。

CLR 中的范型是平台本身出类拔萃的功能。要通过这种方式实现它就需要更改整个 CLR(包括新的和修改过的中间语言指令),并更改元数据、类型加载器、实时 (JIT) 编译器、语言编译器等等。这对 CLR 中的运行时扩展有两个重要的好处。

首先,即使范型的每个结构(例如 Node首先,即使范型的每个结构(例如 Node<Form> 和 Node<String>)都有自己独特的类型标识,但 CLR 能够在类型实例化期间重用许多真正的 JIT 编译的代码。这极大地降低了代码膨胀,并且也是切实可行的,因为范型的各种实例化都是在运行时才展开的。在编译时,构造类型的所有内容就是类型引用。当程序集 A 和 B 都引用在第三方程序集中定义的范型时,它们的构造类型就会在运行时展开。这意味着,除了共享 CLR 类型标识(在适当的时候)以外,来自程序集 A 和 B 的类型实例化也共享运行时资源,如本机代码和扩展的元数据。

类型等效是构造类型运行时扩展的第二个好处。以下为一个示例:引用 AssemblyA.dll 中构造 Node <Int32> 的代码和引用 AssemblyB.dll 中构造 Node <Int32> 的代码都会在运行时创建具有相同 CLR 类型的对象。通过这种方式,如果两个程序集由同一个应用程序使用,则它们的 Node <T> 类型的结构会解析为相同的类型,并且它们的对象可以自由交换。应该注意的是,编译时扩展会使得这种逻辑上很简单的等效变得有问题或者无法实现。

在运行库级别(而非编译器级别)实现范型还有其他一些好处。其中一个好处是范型信息会在编译和执行期间保留下来,因此在代码生存期的任何时刻都可以访问它。例如,反射提供对范型元数据的完全访问。另一个好处是 Visual Studio .NET 中丰富的 IntelliSense 支持,以及范型代码所带来的舒心的调试体验。相反,Java 范型和 C++ 模板在运行时会失去它们的范型标识。

另一个好处(也是 CLR 的支柱)是交叉语言使用 — 使用一种托管语言定义的范型可以由用另一种托管语言编写的代码引用。同时,由于许多繁重的工作都是在这个平台上完成的,所以语言供应商在他们的编译器中置入范型支持的可能性与日剧增。

在运行时类型扩展的众多好处中,我最喜欢的那一个就显得微不足道了。范型代码只限明确用于类型构造实例化的操作使用。这种限制的附加好处是,使 CLR 范型比与它们相应的 C++ 模板更好理解,也更加有用。让我们看一下 CLR 中对范型的限制。

规则和限制


一个困扰使用 C++ 模板的编程人员的问题是许多对类型结构所作的特殊尝试都会失败,包括类型参数的类型变量在实现由模板化代码调用的方法时会失败。同时,这些情况下的编译器错误也很令人困扰,而且可能看起来与根本问题不相关。采用构造类型的运行时扩展以后,类似的错误会变成 JIT 编译器错误或类型加载错误,而不是编译时错误。CLR 架构师决定了对于范型来说,这种实现是不可接受的。

相反,他们决定了对于范型(例如 Node <T>)的任何可能的类型实例化,即使类型实例化实际发生在运行时,该范型也必须在编译时被证实为一种有效类型。同样,有问题的类型结构周围的扩展错误不可能出现。为了实现这个目标,架构师通过一组规则和限制来约束范型的功能,从而保证在尝试扩展其中一个范型实例化之前这种范型有效。

有一些规则限制了您通常可以编写的代码的类型。这些规则的本质可以归纳为一句话:范型代码只有在用于范型的每个可能的构造实例时才有效。否则,范型代码无效,并且也不能正确编译(或者可以编译,但无法在运行时通过验证)。

首先,它看起来像是一个限制规则。以下为一个示例:

public class GenericMath {
   public T Min<T>(T item1, T item2) {
      if (item1 < item2) {
         return item1;
      }
      return item2;
   }
}

这段代码在 CLR 范型中是无效的。C# 编译器产生的错误如下所示:

invalid.cs(4,11): error CS0019: Operator '<' cannot be applied to 
operands of type 'T' and 'T'

同时,除了细微的语法区别外,与这基本相同的代码在 C++ 模板中是允许的。为什么对范型有这样的限制呢?原因是:在 C# 中,“<”运算符只能用于特定的类型。然而,前面代码片段中的类型参数 T 可以在运行时扩展为任何 CLR 类型。前面的代码示例不是在运行时被认为是无效的,而是在编译时被认为是无效的。

除了运算符,更多可管理的类型使用(例如方法调用)也应用了相同的限制。以下对 Min <T> 方法的修改也是无效的范型代码:

class GenericMath {
   public T Min<T>(T item1, T item2) {
      if (item1.CompareTo(item2) < 0) {
         return item1;
      }
      return item2;
   }
}

这段代码无效的原因与前面的示例是一样的。虽然类库中的许多类型都实现了 CompareTo 方法,而且该方法也很容易由您的自定义类型实现,但不能保证这个方法适用于可用作 T 的参数的任何可能的类型。

但您也可以看出,范型代码中并非完全禁止方法调用。在 图 1 中,GetHashCode 方法在两个参数化变量中调用,而在 图 2 中,Node<T> 类型在它的参数化 m_data 字段中调用了 ToString 方法。为什么允许 GetHashCode 和 ToString,而不允许 CompareTo 呢?原因在于,GetHashCode 和 ToString 都是在 System.Object 类型中定义的,而每个可能的 CLR 类型也是从这个类型派生的。这意味着,类型 T 的每个可能的扩展都实现了 ToString 和 GetHashCode 成员函数。

如果要使范型可用于集合类以外的任何类,则范型代码需要能够调用由 System.Object 定义的方法以外的方法。不过要记住,只有当用于范型的任何可能的构造实例时,范型代码才有效。有一个办法可以解决这两个看似相互矛盾的要求,那就是 CLR 范型中称为约束的功能。

您应该知道,约束是范型或方法定义的一个可选组件。在可作为变量用于范型代码上的一个类型参数的类型中,一个范型可以定义任意数量的约束,而每个约束可以应用任一个限制。通过限制可在范型结构中使用的类型,对引用受限类型参数的代码的限制就可以放松一些(请参见 图 3)。

因为对类型参数 T 应用了约束,所以 Min <T> 和 Max <T> 在其条目上调用 CompareTo 是有效的。在 图 3 的第三行代码中,您可以看到它引入了如下所示的 where 子句:

where T : IComparable

这个约束表明 Min <T> 方法的任何结构都必须为实现 IComparable 接口的类型的参数 T 提供一个类型变量。这个约束限制了 Min <T> 的可能实例化的种类,但提高了方法中代码的灵活性,比如现在可以在类型 T 的变量上调用 CompareTo。

约束通过允许范型代码调用扩展类型上的任意方法,从而使范型算法成为可能。虽然约束要求使用额外的语法才能定义范型代码,但约束不会改变引用代码的语法。引用代码的唯一区别在于,类型变量必须遵守对范型的约束。例如,以下用于 Max <T> 的引用代码是有效的:

GenericMath.Min(5, 10);

这是因为 5 和 10 都是整数,而且 Int32 类型实现了 IComparable 接口。然而,试图实现以下 Max <T> 结构会产生编译器错误:

GenericMath.Min(new Object(), new Object());

下面是编译器生成的错误:

MinMax.cs(32,7): error CS0309: The type 'object' must be convertible
to 'System.IComparable' in order to use it as parameter 'T' in the 
generic type or method 'GenericMath.Min(T, T)'

对于 T,System.Object 是一个无效的类型变量,因为它没有实现对 T 的约束要求实现的 IComparable 接口。当类型变量与范型代码上的类型参数不兼容时,约束就可能会使编译器产生如前面示例中所示的描述性错误。

目前,范型支持三种类型的约束:接口约束、基类约束和构造函数约束。接口约束指定一个接口,该参数的所有类型变量都必须与这个接口兼容。任意数量的接口约束都可以应用于给定的类型参数。

基类约束与接口约束类似,但每个类型参数只能包含一个基类约束。如果没有为类型参数指定约束,则应用 Object 的隐式基类约束

构造函数约束通过约束实现公共默认构造函数的类型的类型变量,使得范型代码能够创建由类型参数指定的类型的实例。目前,构造函数约束只支持默认或无参数构造函数

where 子句用于为给定的类型参数定义约束或约束列表。每个 where 子句只应用于一个类型参数。范型或方法定义可以没有 where 子句,也可以有与类型参数一样多的 where 子句。一个给定的 where 子句可以包含一个约束,也可以包含由逗号分隔的约束列表 图 4 显示了对范型代码应用约束的各种语法。

范型的基本规则(范型所有可能的实例化都是有效的,或者范型本身是无效的)还有其他一些有趣的副作用。第一个是强制类型转换。在范型代码中,类型参数类型的变量可能只能与它的基类约束类型或基类约束类型的基类进行相互的强制转换。这意味着,如果类型参数 T 没有约束,它就只能与 Object 引用进行相互强制转换。然而,如果将 T 约束为进一步派生的类型(例如 FileStream),则范型定义可以包括 T 与 FileStream 以及与 FileStream 的所有基类(直到 Object)之间的相互强制转换。 图 5 中的代码显示了作用中的这种强制转换规则。

在这个示例中,T 没有约束,并且被视为未绑定。它具有 Object 的隐式基类约束。T 与 Object 类之间的相互强制转换以及与接口类型之间的相互强制转换在编译时是有效的。但编译器不允许未绑定的 T 强制转换为其他类型(如 Int32)。范型的强制转换规则也适用于转换,因此不允许使用强制转换语法来进行类型转换(比如从 Int32 转换成 Int64);范型不支持类似的转换。

下面是另一个有趣的事情。请考虑以下代码:

void Foo<T>() {
   T x = null;  // compiler error when T is unbounded
   •••

虽然像这样赋空值很常使用,但可能存在一个问题。如果将 T 扩展为值类型,会出现什么情况呢?对一个值变量赋空值没有意义。幸运的是,C# 编译器提供了特殊的语法,以保证正确的结果,而不用管 T 的运行时类型

void Foo<T>() { T x = T.default; // OK for any T}

等号右边的表达式称为默认值表达式。如果将 T 扩展为引用类型,则 T.default 会解析为 null。如果将 T 扩展为值类型,则对于该变量,T.default 是所有位都为零的值

如果 T 未绑定,则不允许对参数化变量 T 赋空值,因此可能有人会认为以下语句也是无效的,但事实不是如此:

void Foo<T>(T x) {
   if (x == null) { // Ok
      •••
   }
}

如果此处允许为 null,则将 T 扩展为值类型时会出现什么情况呢?如果 T 是值类型,则前面示例中的 Boolean 表达式会强制为 false。与赋空值的情况不同,对于 T 的任何扩展,空值比较是有意义的

对于任何可能的实例化,在编译时确认范型有效这个前提的确对范型代码有影响。然而,我发现与 C++ 中的模板相比,CLR 中范型周围的附加结构有明显的作用。总之,约束及其周围的基础结构是 CLR 范型中我最喜欢的一个方面。

范型接口和委托


范型类、结构和方法是 CLR 范型的主要功能。范型接口和委托是真正起支持作用的功能。范型接口如果单独使用,则用处有限。但是,当与范型类、结构或方法一起使用时,范型接口(和委托)就会有重要的作用。

图 3 中的 GenericMath.Min <T> 和 GenericMath.Max <T> 方法都将 T 约束为与 IComparable 接口兼容。这使得这些方法可以调用方法的参数化变量上的 CompareTo。然而,在 图 3中实现的这些方法都没有充分利用范型的优势。原因在于,如果接口采用一个或多个对象参数(例如 CompareTo 的 obj 参数),则调用值类型的非范型接口会导致装箱。

对于 图 3 中的 GenericMath.Min <T>,如果该方法的实例化将 T 扩展为值而不是引用,则每次调用这个方法都会导致 CompareTo 方法的参数装箱。在这种时候,范型接口就可以派上用场了。

图 6 中的代码重构了 GenericMath 方法,以通过范型接口 IComparable <T> 来约束 T。现在,如果 Min <T> 或 Max <T> 的实例化使用值类型作为 T 的变量,则对 CompareTo 的接口调用就是接口结构的一部分,它的参数是值类型,而且没有发生装箱。

范型委托具有类似于范型接口的好处,但它是面向方法而非面向类型。

类库中的范型


除了在 CLR 中实现范型外,Microsoft 还计划提供新的范型类库,以作为代号为“Whidbey”的 CLR 版本的类库的一部分。在本专栏出版的时候,应该会推出 Whidbey CLR 的预览版。(有关详细信息,请参阅本专栏的 2003 年 9 月号。)最保守的估计,会提供实现列表、词典、栈和队列的范型集合类。另外,类库还将包含支持的接口类型(例如 IList<T>、ICollection<T> 和 IComparable<T>),它们是 Microsoft .NET Framework 1.0 和 1.1 类库所附带的简单接口的等效范型。

最后,您还会发现,整个类库中的类型都将以新的和现有功能的范型版本参数化。例如,System.Array 类将包含它的 BinarySearch 和 Sort 方法的范型版本,它们利用范型接口 IComparer<T>, 和 IComparable<T>。在撰写这篇文章时,Microsoft® 尚未确定要对现有类库进行多大的改动,以便在下一个运行库版本中包含范型支持。

小结


CLR 范型是一个强大的应用程序和库开发功能。不管您是选择通过使用集合类来应用范型,还是选择通过架构整个应用程序来应用范型,范型都能够使您的代码类型更安全、可维护,而且效率高。在本专栏的这两期中,在托管代码中进行范型编程给我带来了很大的乐趣,我也期待着要发布的产品。看到 CLR 增添了诸如范型这样的重要功能是一件令人兴奋的事情。还可能有许多令人感兴趣的内容。请继续关注我们的工作。

http://blog.youkuaiyun.com/atfield/article/details/1471465 Introduction Shared Source CLI 2.0 (开发代号Rotor) 是微软.NET Framework 2.0的Shared Source实现版本。Shared Source是微软推出的源代码共享计划,可以在一定限制的情况下获得/使用源代码,详情可以参考Microsoft Shared Source Initiative主页:http://www.microsoft.com/resources/sharedsource/。Rotor的代码可以在非商业行为的前提下可以自由修改和发布,只需保留License声明即可。Rotor包含了下面的内容: 1. CLI的运行时(CLR)的符合ECMA标准的实现 2. C# & Jscript编译器 3. .NET Framework中的部分工具,如ilasm, ildasm, cordbg, gacutil, sn等 4. Build工具,如Build, Binplace, NMake等 5. PAL (Platform Adaptation Layer),支持其他平台 6. 自动化回归测试 7. Samples 由此可见,Rotor是研究.NET Framework实现的最佳材料。 Rotor的全部源代码可以在微软的MSDN网站上下载:http://msdn.microsoft.com/net/sscli/ 下载后解压缩,可以看到如下的目录结构: 目录 内容 Binaries.x86*.rotor Build出来的可执行文件和Symbols。这个目录在Build之后才会出现 clr CLI和BCL(Base Class Library)的实现 csharp C#编译器的实现就在这里了 Docs 文档 Env Build时所需的一些文件 Fx 类库的实现 Jscript Jscript编译器的实现 Pal Platform Adaptation Layer的实现。PAL是Rotor对Windows和Unix的一些基本API的封装 Palrt Platform Adaptive Layer Runtime的实现。PAL中的和操作系统平台无关可重用的部分 prebuilt 存放着build所需的一些事先用工具生成好的文件 Samples Samples Tests Regression Test Suite Tools Build工具,包括binplace, build, cppmunge等 Vscommon 公共头文件,主要是微软内部使用的一些产品有关的公用的宏定义 总共大概有150MB左右。 本人先发在优快云上做个备份。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值