泛型

 


 

什么是泛型

通过泛型可以定义类型安全类,而不会损害类型安全、性能或工作效率。您只须一次性地将服务器实现为一般服务器,同时可以用任何类型来声明和使用它。为此,需要使用 <> 括号,以便将一般类型参数括起来。例如,可以按如下方式定义和使用一般堆栈:

public class Stack<T> { T[] m_Items; public void Push(T item) {...} public T Pop() {...} } Stack<int> stack = new Stack<int>(); stack.Push(1); stack.Push(2); int number = stack.Pop();

代码块 2 显示一般堆栈的完整实现。将代码块 1代码块 2 进行比较,您会看到,好像 代码块 1 中每个使用Object 的地方在代码块 2 中都被替换成了 T,除了使用一般类型参数 T 定义 Stack 以外:

public class Stack<T> {...}

在使用一般堆栈时,必须通知编译器使用哪个类型来代替一般类型参数 T(无论是在声明变量时,还是在实例化变量时):

Stack<int> stack = new Stack<int>();

编译器和运行库负责完成其余工作。所有接受或返回 T 的方法(或属性)都将改为使用指定的类型(在上述示例中为整型)。

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

泛型的好处

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

应用泛型

因为 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<T> { public T X; public T Y; }

可以使用该一般点来表示整数坐标,例如:

Point<int> point; point.X = 1; point.Y = 2;

或者,可以使用它来表示要求浮点精度的图表坐标:

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

一般约束

使用 C# 泛型,编译器会将一般代码编译为 IL,而不管客户端将使用什么样的类型实参。因此,一般代码可以尝试使用与客户端使用的特定类型实参不兼容的一般类型参数的方法、属性或成员。这是不可接受的,因为它相当于缺少类型安全。在 C# 中,您需要通知编译器客户端指定的类型必须遵守哪些约束,以便使它们能够取代一般类型参数而得到使用。存在三个类型的约束派生约束指示编译器一般类型参数派生自诸如接口或特定基类之类的基类型。默认构造函数约束指示编译器一般类型参数公开了默认的公共构造函数(不带任何参数的公共构造函数)。引用/值类型约束将一般类型参数约束为引用类型或值类型。一般类型可以利用多个约束,您甚至可以在使用一般类型参数时使 IntelliSense 反射这些约束,例如,建议基类型中的方法或成员。

需要注意的是,尽管约束是可选的,但它们在开发一般类型时通常是必不可少的。没有它们,编译器将采取更为保守的类型安全方法,并且只允许在一般类型参数中访问 Object 级别功能。约束是一般类型元数据的一部分,以便客户端编译器也可以利用它们。客户端编译器只允许客户端开发人员使用遵守这些约束的类型,从而实施类型安全。 以下示例将详细说明约束的需要和用法。假设您要向代码块 3 的链表中添加索引功能或按键搜索功能:

public class LinkedList<K,T> { T Find(K key) {...} public T this[K key] { get{return Find(key);} } }

这使客户端可以编写以下代码:

LinkedList<int,string> list = new LinkedList<int,string>(); list.AddHead(123,"AAA"); list.AddHead(456,"BBB"); string item = list[456]; Debug.Assert(item == "BBB");

要实现搜索,您需要扫描列表,将每个节点的键与您要查找的键进行比较,并且返回键匹配的节点的项。问题在于,Find() 的以下实现无法编译:

T Find(K key) { Node<K,T> 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<K,T> where K : IComparable { T Find(K key) { Node<K,T> 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<T>:

public interface IComparable<T> { int CompareTo(T other); bool Equals(T other); }

您可以约束键类型参数以支持 IComparable<T>,并且使用键的类型作为类型参数;这样,您不仅获得了类型安全,而且消除了在值类型用作键时的装箱操作:

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

实际上,所有支持 .NET 1.1 中的 IComparable的类型都支持 .NET 2.0 中的 IComparable<T>。这使得可以使用常见类型(例如,int、string、GUID、DateTime 等等)的键。

在 C# 2.0 中,所有约束都必须出现在一般类的实际派生列表之后。例如,如果 LinkedList 派生自 IEnumerable<T>接口(以获得迭代器支持),则需要将 where 关键字放在紧跟它后面的位置:

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

通常,只须在需要的级别定义约束。在链表示例中,在节点级别定义 IComparable <T>派生约束是没有意义的,因为节点本身不会比较键。如果您这样做,则您还必须将该约束放在 LinkedList 级别,即使该列表不比较键。这是因为该列表包含一个节点作为成员变量,从而导致编译器坚持要求:在列表级别定义的键类型必须遵守该节点在一般键类型上放置的约束。

换句话说,如果您按如下方式定义该节点:

class Node<K,T> where K : IComparable<K> {...}

则您必须在列表级别重复该约束,即使您不提供 Find() 方法或其他任何与此有关的方法:

public class LinkedList<KeyType,DataType> where KeyType : IComparable<KeyType> { Node<KeyType,DataType> m_Head; }

可以在同一个一般类型参数上约束多个接口(彼此用逗号分隔)。例如:

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

您可以为您的类使用的每个一般类型参数提供约束,例如:

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

您可以具有一个基类约束,这意味着规定一般类型参数派生自特定的基类:

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

但是,在一个约束中最多只能使用一个基类,这是因为 C# 不支持实现的多重继承。显然,您约束的基类不能是密封类或静态类,并且由编译器实施这一限制。此外,您不能将 System.DelegateSystem.Array 约束为基类

您可以同时约束一个基类以及一个或多个接口,但是该基类必须首先出现在派生约束列表中:

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

C# 确实允许您将另一个一般类型参数指定为约束:

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

在处理派生约束时,您可以通过使用基类型本身来满足该约束,而不必非要使用它的严格子类。例如:

public interface IMyInterface {...} public class MyClass<T> where T : IMyInterface {...} MyClass<IMyInterface> obj = new MyClass<IMyInterface>();

或者,您甚至可以:

public class MyOtherClass {...} public class MyClass<T> where T : MyOtherClass {...} MyClass<MyOtherClass> obj = new MyClass<MyOtherClass>();

最后,请注意,在提供派生约束时,您约束的基类型(接口或基类)必须与您定义的一般类型参数具有一致的可见性。例如,以下约束是有效的,因为内部类型可以使用公共类型:

public class MyBaseClass {} internal class MySubClass<T> where T : MyBaseClass {}

但是,如果这两个类的可见性被颠倒,例如: internal class MyBaseClass {} public class MySubClass<T> where T : MyBaseClass {}

则编译器会发出错误,因为程序集外部的任何客户端都无法使用一般类型 MySubClass,从而使得 MySubClass 实际上成为内部类型而不是公共类型。外部客户端无法使用 MySubClass 的原因是,要声明 MySubClass 类型的变量,它们需要使用派生自内部类型 MyBaseClass 的类型。

构造函数约束

假设您要在一般类的内部实例化一个新的一般对象。问题在于,C# 编译器不知道客户端将使用的类型实参是否具有匹配的构造函数,因而它将拒绝编译实例化行。

为了解决该问题,C# 允许约束一般类型参数,以使其必须支持公共默认构造函数。这是使用 new() 约束完成的。例如,以下是一种实现代码块 3 中的一般 Node<K,T> 的默认构造函数的不同方式。

class Node<K,T> where T : new() { public K Key; public T Item; public Node<K,T> NextNode; public Node() { Key = default(K); Item = new T(); NextNode = null; } }

可以将构造函数约束与派生约束组合起来,前提是构造函数约束出现在约束列表中的最后:

public class LinkedList<K,T> where K : IComparable<K>,new() {...}

引用/值类型约束

可以使用 struct 约束将一般类型参数约束为值类型(例如,int、bool 和 enum),或任何自定义结构:

public class MyClass<T> where T : struct {...}

同样,可以使用 class 约束将一般类型参数约束为引用类型(类):

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

不能将引用/值类型约束与基类约束一起使用,因为基类约束涉及到类。同样,不能使用结构和默认构造函数约束,因为默认构造函数约束也涉及到类。虽然您可以使用类和默认构造函数约束,但这样做没有任何价值。可以将引用/值类型约束与接口约束组合起来,前提是引用/值类型约束出现在约束列表的开头。

泛型和强制类型转换

C# 编译器只允许将一般类型参数隐式强制转换到 Object 或约束指定的类型,如代码块 5 所示。这样的隐式强制类型转换是类型安全的,因为可以在编译时发现任何不兼容性。

代码块 5. 一般类型参数的隐式强制类型转换

interface ISomeInterface {...}

class BaseClass {...}

class MyClass<T> where T : BaseClass,ISomeInterface { void SomeMethod(T t) { ISomeInterface obj1 = t; BaseClass obj2 = t; object obj3 = t; } }

编译器允许您将一般类型参数显式强制转换到其他任何接口,但不能将其转换到类

interface ISomeInterface {...}

class SomeClass {...}

class MyClass<T> {

void SomeMethod(T t) {

ISomeInterface obj1 = (ISomeInterface)t;//Compiles

SomeClass obj2 = (SomeClass)t; //Does not compile }

}

但是,您可以使用临时的 Object 变量,将一般类型参数强制转换到其他任何类型:

class SomeClass {...}

class MyClass<T> {

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<T> 
{ 
public void SomeMethod(T t) { 
if(t is int) {...} 
if(t is LinkedList<int,string>) {...} 
string str = t as string; 
if(str != null) {...} 
LinkedList<int,string> list = t as LinkedList<int,string>;
 if(list != null) {...} 
} 
}

继承和泛型

在从一般基类派生时,必须提供类型实参,而不是该基类的一般类型参数:

public class BaseClass<T> {...}

public class SubClass : BaseClass<int> {...}

如果子类是一般的而非具体的类型实参,则可以使用子类一般类型参数作为一般基类的指定类型:

public class SubClass<T> : BaseClass<T> {...}

在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束:

public class BaseClass<T> where T : ISomeInterface {...}

public class SubClass<T> : BaseClass<T> where T : ISomeInterface {...}

或构造函数约束:

public class BaseClass<T> where T : new() { public T SomeMethod() { return new T(); } }

public class SubClass<T> : BaseClass<T> where T : new() {...}

基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型:

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() {...} }

您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样:

public interface ISomeInterface<T> { T SomeMethod(T t); }

public abstract class BaseClass<T> { public abstract T SomeMethod(T t); }

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

一般抽象方法和一般接口有一种有趣的用法。在 C# 2.0 中,不能对一般类型参数使用诸如 + 或 += 之类的运算符。例如,以下代码无法编译,因为 C# 2.0 不具有运算符约束:

public class Calculator<T> { public T Add(T arg1,T arg2) { return arg1 + arg2;//Does not compile } //Rest of the methods }

但是,您可以通过定义一般操作,使用抽象方法(最好使用接口)进行补偿。由于抽象方法的内部不能具有任何代码,因此可以在基类级别指定一般操作,并且在子类级别提供具体的类型和实现:

public abstract class BaseCalculator<T> { 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<int> { public override int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }

一般接口还可以产生更加干净一些的解决方案:

public interface ICalculator<T> { T Add(T arg1,T arg2); //Rest of the methods }

public class MyCalculator : ICalculator<int> { public int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }

一般方法

在 C# 2.0 中,方法可以定义特定于其执行范围的一般类型参数:

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

这是一种重要的功能,因为它使您可以每次用不同的类型调用该方法,而这对于实用工具类非常方便。即使包含类根本不使用泛型,您也可以定义方法特定的一般类型参数:

public class MyClass { public void MyMethod<T>(T t) {...} }

该功能仅适用于方法。属性或索引器只能使用在类的作用范围中定义的一般类型参数

在调用定义了一般类型参数的方法时,您可以提供要在调用场所使用的类型:

MyClass obj = new MyClass(); obj.MyMethod<int>(3);

因此,当调用该方法时,C# 编译器将足够聪明,从而基于传入的参数的类型推断出正确的类型,并且它允许完全省略类型规范:

MyClass obj = new MyClass(); obj.MyMethod(3);

该功能称为一般类型推理。请注意,编译器无法只根据返回值的类型推断出类型

public class MyClass { public T MyMethod<T>() {} } MyClass obj = new MyClass(); int number = obj.MyMethod();//Does not compile

当方法定义它自己的一般类型参数时,它还可以定义这些类型的约束:

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

但是,您无法为类级别一般类型参数提供方法级别约束。类级别一般类型参数的所有约束都必须在类作用范围中定义。

在重写定义了一般类型参数的虚拟方法时,子类方法必须重新定义该方法特定的一般类型参数:

public class BaseClass { public virtual void SomeMethod<T>(T t) {...} }

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

子类实现必须重复在基础方法级别出现的所有约束:

public class BaseClass { public virtual void SomeMethod<T>(T t) where T : new() {...} }

public class SubClass : BaseClass { public override void SomeMethod<T>(T t) where T : new() {...} }

请注意,方法重写不能定义没有在基础方法中出现的新约束。

此外,如果子类方法调用虚拟方法的基类实现,则它必须指定要代替一般基础方法类型参数使用的类型实参。您可以自己显式指定它,或者依靠类型推理(如果可用):

public class BaseClass { public virtual void SomeMethod<T>(T t) {...} }

public class SubClass : BaseClass { public override void SomeMethod<T>(T t) { base.SomeMethod<T>(t); base.SomeMethod(t); } }

一般静态方法

C# 允许定义使用一般类型参数的静态方法。但是,在调用这样的静态方法时,您需要在调用场所为包含类提供具体的类型,如下面的示例所示:

public class MyClass<T> { public static T SomeMethod(T t) {...} } int number = MyClass<int>.SomeMethod(3);

静态方法可以定义方法特定的一般类型参数和约束,就像实例方法一样。在调用这样的方法时,您需要在调用场所提供方法特定的类型 — 可以按如下方式显式提供:

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");

一般静态方法遵守施加于在类级别使用的一般类型参数的所有约束(Generic static methods are subjected to all constraints imposed on the generic type parameterthey use at the class level)。就像实例方法一样,您可以为由静态方法定义的一般类型参数提供约束:

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

C# 中的运算符只是静态方法而已,并且C#允许您为自己的一般类型重载运算符。假设代码块3 的一般 LinkedList 提供了用于串联链表的 + 运算符。+ 运算符使您能够编写下面这段优美的代码:

LinkedList<int,string> list1 = new LinkedList<int,string>(); LinkedList<int,string> list2 = new LinkedList<int,string>(); ... LinkedList<int,string> list3 = list1+list2;

代码块 7 显示 LinkedList 类上的一般 + 运算符的实现。请注意,运算符不能定义新的一般类型参数。

代码块 7. 实现一般运算符

public class LinkedList<K,T> { public static LinkedList<K,T> operator+(LinkedList<K,T> lhs, LinkedList<K,T> rhs) { return concatenate(lhs,rhs); } static LinkedList<K,T> concatenate(LinkedList<K,T> list1, LinkedList<K,T> list2) { LinkedList<K,T> newList = new LinkedList<K,T>(); Node<K,T> 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<T> { public delegate void GenericDelegate(T t); public void SomeMethod(T t) {...} }

在为包含类指定类型时,也会影响到委托:

MyClass<int> obj = new MyClass<int>();

MyClass<int>.GenericDelegate del;

del = new MyClass<int>.GenericDelegate(obj.SomeMethod);

del(3);

C# 2.0 使您可以将方法引用的直接分配转变为委托变量:

MyClass<int> obj = new MyClass<int>(); MyClass<int>.GenericDelegate del; del = obj.SomeMethod;

我将把该功能称为委托推理

像类、结构和方法一样,委托也可以定义一般类型参数:

public class MyClass<T> { public delegate void GenericDelegate<X>(T t,X x); }

在类的作用范围外部定义的委托可以使用一般类型参数。在该情况下,在声明和实例化委托时,必须为其提供类型实参:

public delegate void GenericDelegate<T>(T t);

public class MyClass { public void SomeMethod(int number) {...} }

MyClass obj = new MyClass();

GenericDelegate<int> del;

del = new GenericDelegate<int>(obj.SomeMethod); del(3);

另外,还可以在分配委托时使用委托推理:

MyClass obj = new MyClass(); GenericDelegate<int> del; del = obj.SomeMethod;

当然,委托可以定义约束以伴随它的一般类型参数:

public delegate void MyDelegate<T>(T t) where T : IComparable<T>;

委托级别约束只在使用端实施(在声明委托变量和实例化委托对象时),类似于在类型或方法的作用范围中实施的其他任何约束。

一般委托对于事件尤其有用。您可以精确地定义一组有限的一般委托(只按照它们需要的一般类型参数的数量进行区分),并且使用这些委托来满足所有事件处理需要。代码块 8 演示了一般委托和一般事件处理方法的用法。

代码块 8. 一般事件处理

public delegate void GenericEventHandler<S,A>(S sender,A args); 
public class MyPublisher { 
public event GenericEventHandler<MyPublisher,EventArgs> MyEvent; public void FireEvent() 
{ MyEvent(this,EventArgs.Empty); }

 } 
public class MySubscriber<A> //Optional: can be a specific type
 { public void SomeMethod(MyPublisher sender,A args) {...} } MyPublisher publisher = new MyPublisher();
 MySubscriber<EventArgs> subscriber = new MySubscriber<EventArgs>(); 
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<int,string> list = new LinkedList<int,string>();

Type type1 = typeof(LinkedList<int,string>);

Type type2 = list.GetType();

Debug.Assert(type1 == type2);

typeofGetType() 都可以对一般类型参数进行操作:

public class MyClass<T> { public void SomeMethod(T t) { Type type = typeof(T); Debug.Assert(type == t.GetType()); } }

此外,typeof 运算符还可以对未绑定的一般类型进行操作。例如:

public class MyClass<T> {}

Type unboundedType = typeof(MyClass<>);

Trace.WriteLine(unboundedType.ToString()); //Writes: MyClass`1[T]

所追踪的数字 1 是所使用的一般类型的一般类型参数的数量。请注意空 <> 的用法。要对带有多个类型参数的未绑定一般类型进行操作,请在 <> 中使用“,”:

public class LinkedList<K,T> {...}

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<int,string> list = new LinkedList<int,string>(); 

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<int,string> list = new LinkedList<int,string>();

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<T> : Attribute {...}

然而,属性类可以通过使用一般类型或者定义 Helper 一般方法(像其他任何类型一样)在内部利用泛型:

public class SomeAttribute : Attribute { void SomeMethod<T>(T t) {...} LinkedList<int,string> m_List = new LinkedList<int,string>(); }

小结

C# 泛型是开发工具库中的一个无价之宝。它们可以提高性能、类型安全和质量,减少重复性的编程任务,简化总体编程模型,而这一切都是通过优雅的、可读性强的语法完成的。尽管 C# 泛型的根基是 C++ 模板,但 C# 通过提供编译时安全和支持将泛型提高到了一个新水平。C# 利用了两阶段编译、元数据以及诸如约束和一般方法之类的创新性的概念。毫无疑问,C# 的将来版本将继续发展泛型,以便添加新的功能,并且将泛型扩展到诸如数据访问或本地化之类的其他 .NET Framework 领域。

 

posted @ 2013-04-15 20:39  邪见  阅读(324)  评论(0)    收藏  举报