C#泛型简介

简介

泛型是 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 为类型实参。

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

应用泛型

因为 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() 方法在堆栈为空时返回零。

泛型的好处

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

上一篇: 网页设计的布局理念
下一篇: C#常用自定义类型转换函数
文章来自: msdn
引用通告: 查看所有引用 | 我要引用此文章
Tags:
最新日志:
评论: 0 | 引用: 0 | 查看次数: 3628
发表评论
登录后再发表评论!