扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
泛型是 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 还具有一些泛型特定的语法。例如,请考虑代码块 2 的 pop() 方法。假设您不希望在堆栈为空时引发异常,而是希望返回堆栈中存储的类型的默认值。如果您使用基于 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 {...}
或:
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者