65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 泛型简明指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (8投票s)

2009年12月17日

CPOL

10分钟阅读

viewsIcon

51688

解释 .NET 中的泛型。

本文的灵感

自从 .NET 2.0 引入泛型以来,我一直在网上搜索关于这个重要特性的好文章,但除了少数几篇,我找不到一篇能让我了解泛型大部分内容的文章。因此,我想到写一篇文章,试图将泛型所有重要特性/特点汇集到一页,这样初学者就不必再费力搜索了。

引言

泛型是作为 .NET 2.0 的一部分引入的最重要的特性之一。它在语法上类似于 C++ 模板,但在实现和功能列表上有所不同,最重要的是,它确保了类型安全并为程序员提供了智能感知支持。它们在特性和实现上也有所不同。泛型帮助我们定义类型安全的用户自定义数据类型,而无需指定实际的内部数据类型。这有助于显著提高性能,同时使代码更具可重用性。让我们考虑一个问题陈述,并分析泛型如何解决这个问题。

问题陈述:实现一种链表类型的数据结构,该结构有两个数据成员,并且这两个元素的类型可以根据给定场景而变化。要创建这种类型的数据结构,我们可以按如下方式进行

public class Node
{
    public int item;
    public int key;
    public Node NextNode;
    public Node()
    {
        item=0;
        key=0;    
        NextNode=null;
    }
    public Node(int a, int b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

考虑一个使用此 NodeLinkedList 数据结构

public class LinkedList
{
    Node head;
    public LinkedList()
    {
        head=new Node(1,1,null);
    }
    public void AddNode(int item,int Key)
    {
        Node newNode=new Node(item,Key,head.NextNode);
        head.NextNode=newnode;
    }
}

写完这种类型的数据结构后,很明显我们只能在链表中存储整数类型的数据。为了存储字符串类型的数据,我们必须从头开始创建一个新的链表。该 LinkedListNode 类将如下所示

public class Node
{
    public string item;
    public int key;
    public Node NextNode;
    public Node()
    {
        item="";
        key=0;    
        NextNode=null;
    }
    public Node(string a, int b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

或者另一个解决方案是,不是硬编码 Node 类数据成员的类型,我们可以将它们设为 object,然后到处使用 object 类型。这样就变成

public class Node
{
    public object item;
    public object key;
    public Node NextNode;
    public Node()
    {
        item=null;
        key=null;    
        NextNode=null;
    }
    public Node(object a, object b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

考虑一个使用此 NodeLinkedList 数据结构

public class LinkedList
{
    Node head;
    public LinkedList()
    {
        head=new Node(1,1,null);
    }
    public void AddNode(object item,object Key)
    {
        Node newNode=new Node(item,Key,head.NextNode);
        head.NextNode=newnode;
    }
    public object this[object key]
       {
       get{return Find(key);}
      }
    public object Find(object k)
    {
        //...search and comparison goes here
    }
}

创建了它之后,我们确实不必每次想要存储不同类型的数据时都从头开始编写 LinkedList 的代码,但同时,它也会带来一些缺点。

  • 由于装箱和拆箱的开销导致性能下降。
  • 类型安全问题。

让我们看看这个问题并讨论解决方案。

考虑以下代码:

LinkedList s=new LinkedList();
s.AddNode(12,13);
s.AddNode("Abc",15);

目前,列表中有三个节点。让我们尝试根据键查找一个项。为此,我编写了这段代码

int n;
n=(int)s[13];

object 转换为 int 会降低性能。同时

int n;
n=(int)s[15];

这不具备类型安全性,因为如果存储在特定键处的项是不包含数字的字符串,则会引发运行时错误。

泛型登场

泛型是一种技术,它允许我们定义类型安全的用户定义类型,而不会影响性能或生产力。我们可以定义一次 LinkedList,然后根据我们的需求在后期使用类型组合。现在让我们了解泛型。C# 中的泛型在语法上类似于 C++ 模板,但它们由编译器处理的方式有所不同。在 C++ 中,编译器在指定实际类型之前甚至不会编译泛型代码。当指定实际类型时,编译器会内联插入类型特定信息,然后将代码编译为机器代码。与 C++ 不同,在 C# 中,编译器将泛型代码编译为 IL,并在发现泛型类型 T 的任何地方放置占位符。让我们使用泛型定义上面的 LinkedList

public class Node<T,K>
{
    public T item;
    public K key;
    public Node NextNode;
    public Node()
    {
        item=default(T);
        key=default(K);    
        NextNode=null;
    }
    public Node(T a, K b, Node n)
    {
        item=a;
        key=b;
        NextNode=n;
    }
}

考虑一个使用此 NodeLinkedList 数据结构

public class LinkedList<T,K>
{
    Node<int,string> head;
    public LinkedList()
    {
        head=new Node<int,string>(1,"1",null);
    }
    public void AddNode(T item,K Key)
    {
        Node<int,string> newNode=new Node<int, string>(item, 
                                               Key, head.NextNode);
        head.NextNode=newnode;
    }
    public T this[K key]
       {
       get{return Find(key);}
      }
    public  T Find(K k)
    {
        //...search and comparison goes here
    }
}

在泛型之前,我们会按照我们想要的方式创建 LinkedList 类的对象,并调用 AddNode 方法,传入任何类型的参数。这会造成上面提到的类型转换和类型安全等问题,但有了泛型,我们声明泛型类 LinkedList 对象的方式如下

LinkedList<int,string> s=new LinkedList<int,string>();

通过这样做,我们获得了与拥有 object 类型成员几乎相同的功能,但在这里我们获得了类型安全。我们强制编译器将 item 作为 int 类型,将 key 作为 string 类型。让我们尝试调用 AddNode()Find() 方法,看看它与之前有何不同。

//The complier will not compile, but in earlier case it would have compiled.
s.AddNode(12,1);
s.AddNode(12,"1");// Would compile and run.

现在,让我们调用 Find 方法,根据键值获取存储在特定节点中的值。

string item1;
tem1=s["1"];
// this would not compile as the compiler will give
// cast mismatch error. So programmer is warned at the compile time itself.
// but in earlier case it would have compiled and raised runtime exception,
// and also there is no need of casting.

泛型约束

我们先定义 Find() 方法

public T Find(K k)
{
    Node<T,K> current=head;
    while(current.NextNode!=null)
    {
        if(current.Key==k) //Will not compile
            break;
        else
            current=current.NextNode;
    }
    return current.Item;
}

C# 编译器将代码编译为 IL,具体取决于客户端使用的类型。可能存在泛型字段尝试实现方法、属性或运算符的情况,而这些方法、属性或运算符与特定类型不兼容。考虑上面的 Find() 方法和行 if (current.Key == k)。这将无法编译,因为编译器无法判断 K 或指定给 K 的实际类型是否支持用于相等性检查的 == 运算符。例如,结构体不允许使用此运算符进行相等性检查。为了克服这个问题,我们可以使用 CompareTo() 方法,如下所示

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

但问题依然存在,因为这次代码仍然无法编译,因为编译器将再次无法判断 K 或实际类型是否派生自 IComparable。为了克服这些问题,在 C# 中,我们需要指示编译器客户端指定的类型必须遵守哪些约束,以便它们可以替代泛型类型参数使用。我们借助约束强制执行这些限制。在 C# 中,有三种类型的约束,但可以以五种不同的方式应用。让我们逐一讨论它们。

1. 派生约束

1.1 接口派生

派生约束指示编译器泛型参数派生自接口或类。为了实现这一点,我们使用 C# 的 where 保留字,如下所示

public class LinkedList<T,K> where K:IComparable
{
}

为类编写这种类型的定义后,我们强制调用程序对泛型参数 K 使用派生自 IComparable 的类型。Visual Studio 将为此提供智能感知支持。

1.2 基类派生

除了放置接口,我们还可以指示编译器泛型参数类型必须派生自特定的基类。例如

public class MyBaseClass
{
    .....
    .....
}

public class MyGenericClass<T> where T:MyBaseClass
{
    ....
    ....
}

创建 MyGenericclass 的对象时,我们必须使用派生自 MyBaseClass 的类型。这里需要注意的是,我们不能将 System.DelegateSystem.Array 用作此类型的约束。同时,我们可以同时约束基类和接口,但基类必须写在约束列表中的接口之前。

1.3 泛型类型参数作为约束

C# 允许我们使用泛型参数类型作为约束。例如,在以下代码块中,当提供实际类型时,泛型参数 T 必须派生自泛型参数 U

public class MyClass<T,K> where T:K
{
    ....
    ....
}

在创建 MyClass 的对象时,我们必须小心分配 TK 的类型。分配给 T 的类型必须派生自分配给 K 的类型。如果我们忽略此限制,编译器将抛出编译时错误。

2. 构造函数约束

考虑这样一种情况:我们必须在泛型类本身中初始化泛型参数。在这种情况下,编译器将无法判断为泛型参数提供的实际类型是否具有默认构造函数。请看下面的例子

public class MyClass<T>
{
    int item;
    T t;
    public MyClass()
    {
        item=0;
        t=new T();
    }
}

此代码将无法编译,因为编译器不知道为 T 提供的实际类型是否支持默认的公共构造函数。为了解决这个问题,我们使用构造函数约束,如下所示

public class MyClass<T> where T:new()
{
    int item;
    T t;
    public MyClass()
    {
        item=0;
        t=new T();
    }
}

我们可以将构造函数约束与派生约束结合使用,前提是构造函数约束出现在约束列表的最后。

3. 值类型和引用类型约束

我们可以将泛型类型约束为具有任何值类型的参数,例如 intboolenum 或任何自定义结构,如下所示

public class MyClass<T> where T:enum

这会告诉编译器 T 的类型是 enum。类似地,我们可以将泛型类型参数约束为仅使用引用类型,如下所示

public class MyClass<T> where T:class

注意事项

  1. 我们不能将引用/值类型约束与基类约束用于泛型参数,因为基类约束本身就意味着一个类。
  2. 我们不能将结构体和默认构造函数约束同时应用于泛型参数,因为默认构造函数本身就意味着一个类。

泛型的继承支持

在 C# 中,我们可以从泛型基类派生一个类,但在定义时,我们必须为泛型基类提供一个特定的类型。例如

public class mybaseClass<T>
{...}
public class myDerivedClass:myBaseClass<bool>
{
    ....
}

然而,如果我们想创建泛型派生类,我们可以将派生类的泛型参数传递给基类,如下所示

public class mybaseClass<T>
{...}
public class myDerivedClass<T>:myBaseClass<T>
{
    ....
}

如果基类定义了一些约束,那么在定义派生类时,我们必须以相同的顺序在派生类上再次定义这些约束。例如

public class myBaseClass<T> where T:IList
{...}
public class myDerivedClass<T>:myBaseClass<T> where T:IList
{
    ....
}

如果基类使用泛型虚方法,那么在子类中,当我们尝试覆盖虚方法时,我们必须放置实际类型。例如

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

如果子类是泛型的,它也可以使用自己的泛型类型参数进行覆盖

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

我们可以定义泛型接口、抽象类和泛型抽象方法,它们的行为方式与其他泛型类型一样。

泛型方法

在 C# 2.0 中,我们可以像定义类一样定义泛型方法。它们可以存在于泛型类中,也可以存在于普通类中。泛型方法赋予我们使用不同参数类型集调用函数的灵活性,这在构建工具类时成为一个重要的特性。

public class MyClass<T>
{
    public void SomeMethod<X>(X x)
    {
        ...
    }
}

或者在一个普通类中,比如

public class MyClass
{
    public void SomeMethod<X>(X x)
    {
        ...
    }
}

此功能仅适用于方法。属性或索引器只能使用在类作用域中定义的泛型类型参数。

泛型静态方法

C# 允许我们在泛型类中创建泛型静态方法,但在调用静态方法时,我们必须用实际类型替换泛型类型参数。

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

或在可能时依赖类型推断

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

泛型静态方法受制于它们在类级别使用的泛型类型参数的所有约束。与实例方法一样,您可以为静态方法定义的泛型类型参数提供约束

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

泛型委托

C# 允许我们以声明其他泛型类型相同的方式声明泛型委托。如果我们在泛型类中声明泛型委托,那么委托必须使用类级别指定的类型,如下面的代码片段所示

public class MyClass<T>
{
    public delegate void MyGenericDelegate(T t);
    public void SomeMethod(T t)
    {
        ...
    }
}

在实例化类并为泛型参数提供实际类型时,它也会影响委托。

MyClass<int> s=new MyClass<int>();
MyClass<int>.MyGenericDelegate d;
d=MyClass<int>.MyGenericDelegate(s.SomeMethod);
d(2);//Invoking the method with the halp of deligate variable

C# 2.0 为我们提供了以更简化的方式执行相同操作的另一种方法。

MyClass<int> s = new MyClass<int>();
MyClass<int>.MyGenericDelegate d;
d=s.SomeMethod;
d(2);

编译器能够推断您分配的委托类型,查找目标对象是否具有您指定名称的方法,并验证该方法的签名是否匹配。然后,编译器创建推断参数类型的新委托(包括正确的类型而不是泛型类型参数),并将新委托分配给推断的委托。委托可以定义为在类作用域之外的泛型。在这种情况下,在实例化委托时,我们必须确保为泛型参数提供实际类型,如下例所示

public delegate void MyGenericDelegate<T>(T t);
public class MyClass
{
    
    public void SomeMethod(int n)
    {
        ...
    }
}
//and this is how it is invoked
MyClass s=new MyClass();
MyGenericDelegate<int> d=new MyGenericDelegate<int>(s.SomeMethod);
d(10);

约束也可以应用于泛型委托,方式与我们将其应用于其他地方相同。泛型委托在创建事件时非常有用。我们只需创建一堆泛型委托并在整个应用程序中用于事件处理。

结论

在本文中,我们已经看到了如何以许多不同的方式使用委托,以及它们如何轻松地解决与类型相关的问题,使我们更具生产力并帮助我们创建可重用和高质量的代码。C# 泛型受 C++ 模板启发,与 C++ 模板相比提供了许多优势。我仍然没有涵盖泛型在反射、事件处理、属性和各种其他可能领域中的所有特性。

修订历史

  • 2009-12-23:文章初版。
© . All rights reserved.