C# 编译器只允许将一般类型参数隐式强制转换到 Object 或约束指定的类型,如代码块 5 所示。这样的隐式强制类型转换是类型安全的,因为可以在编译时发现任何不兼容性。
代码块 5. 一般类型参数的隐式强制类型转换
interface ISomeInterface {...} class BaseClass {...} class MyClass where T : BaseClass,ISomeInterface { void SomeMethod(T t) { ISomeInterface obj1 = t; BaseClass obj2 = t; object obj3 = t; } }
编译器允许您将一般类型参数显式强制转换到其他任何接口,但不能将其转换到类:
interface ISomeInterface {...} class SomeClass {...} class MyClass { void SomeMethod(T t) { ISomeInterface obj1 = (ISomeInterface)t;//Compiles SomeClass obj2 = (SomeClass)t; //Does not compile } }
但是,您可以使用临时的 Object 变量,将一般类型参数强制转换到其他任何类型:
class SomeClass {...} class MyClass { 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 { public void SomeMethod(T t) { if(t is int) {...} if(t is LinkedList) {...} string str = t as string; if(str != null) {...} LinkedList list = t as LinkedList; if(list != null) {...} } }
继承和泛型
在从一般基类派生时,必须提供类型实参,而不是该基类的一般类型参数:
public class BaseClass {...} public class SubClass : BaseClass {...}
如果子类是一般的而非具体的类型实参,则可以使用子类一般类型参数作为一般基类的指定类型:
public class SubClass : BaseClass {...}
在使用子类一般类型参数时,必须在子类级别重复在基类级别规定的任何约束。例如,派生约束:
public class BaseClass where T : ISomeInterface {...} public class SubClass : BaseClass where T : ISomeInterface {...}
或构造函数约束:
public class BaseClass where T : new() { public T SomeMethod() { return new T(); } } public class SubClass : BaseClass where T : new() {...}
基类可以定义其签名使用一般类型参数的虚拟方法。在重写它们时,子类必须在方法签名中提供相应的类型:
public class BaseClass { public virtual T SomeMethod() {...} } public class SubClass: BaseClass<int> { public override int SomeMethod() {...} }
如果该子类是一般类型,则它还可以在重写时使用它自己的一般类型参数:
public class SubClass: BaseClass { public override T SomeMethod() {...} }
您可以定义一般接口、一般抽象类,甚至一般抽象方法。这些类型的行为像其他任何一般基类型一样:
public interface ISomeInterface { T SomeMethod(T t); } public abstract class BaseClass { public abstract T SomeMethod(T t); } public class SubClass : BaseClass { public override T SomeMethod(T t) {...) }
一般抽象方法和一般接口有一种有趣的用法。在 C# 2.0 中,不能对一般类型参数使用诸如 + 或 += 之类的运算符。例如,以下代码无法编译,因为 C# 2.0 不具有运算符约束:
public class Calculator { public T Add(T arg1,T arg2) { return arg1 + arg2;//Does not compile } //Rest of the methods }
但是,您可以通过定义一般操作,使用抽象方法(最好使用接口)进行补偿。由于抽象方法的内部不能具有任何代码,因此可以在基类级别指定一般操作,并且在子类级别提供具体的类型和实现:
public abstract class BaseCalculator { 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 { public override int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }
一般接口还可以产生更加干净一些的解决方案:
public interface ICalculator { T Add(T arg1,T arg2); //Rest of the methods } public class MyCalculator : ICalculator { public int Add(int arg1, int arg2) { return arg1 + arg2; } //Rest of the methods }
一般方法
在 C# 2.0 中,方法可以定义特定于其执行范围的一般类型参数:
public class MyClass { public void MyMethod(X x) {...} }
这是一种重要的功能,因为它使您可以每次用不同的类型调用该方法,而这对于实用工具类非常方便。
即使包含类根本不使用泛型,您也可以定义方法特定的一般类型参数:
public class MyClass { public void MyMethod(T t) {...} }
该功能仅适用于方法。属性或索引器只能使用在类的作用范围中定义的一般类型参数。
在调用定义了一般类型参数的方法时,您可以提供要在调用场所使用的类型:
MyClass obj = new MyClass(); obj.MyMethod(3);
因此,当调用该方法时,C# 编译器将足够聪明,从而基于传入的参数的类型推断出正确的类型,并且它允许完全省略类型规范:
MyClass obj = new MyClass(); obj.MyMethod(3);
该功能称为一般类型推理。请注意,编译器无法只根据返回值的类型推断出类型:
public class MyClass { public T MyMethod() {} } MyClass obj = new MyClass(); int number = obj.MyMethod();//Does not compile
当方法定义它自己的一般类型参数时,它还可以定义这些类型的约束:
public class MyClass { public void SomeMethod(T t) where T : IComparable {...} }
但是,您无法为类级别一般类型参数提供方法级别约束。类级别一般类型参数的所有约束都必须在类作用范围中定义。
在重写定义了一般类型参数的虚拟方法时,子类方法必须重新定义该方法特定的一般类型参数:
public class BaseClass { public virtual void SomeMethod(T t) {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) {...} }
子类实现必须重复在基础方法级别出现的所有约束:
public class BaseClass { public virtual void SomeMethod(T t) where T : new() {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) where T : new() {...} }
请注意,方法重写不能定义没有在基础方法中出现的新约束。
此外,如果子类方法调用虚拟方法的基类实现,则它必须指定要代替一般基础方法类型参数使用的类型实参。您可以自己显式指定它,或者依靠类型推理(如果可用):
public class BaseClass { public virtual void SomeMethod(T t) {...} } public class SubClass : BaseClass { public override void SomeMethod(T t) { base.SomeMethod(t); base.SomeMethod(t); } }
一般静态方法
C# 允许定义使用一般类型参数的静态方法。但是,在调用这样的静态方法时,您需要在调用场所为包含类提供具体的类型,如下面的示例所示:
public class MyClass { public static T SomeMethod(T t) {...} } int number = MyClass.SomeMethod(3);
静态方法可以定义方法特定的一般类型参数和约束,就像实例方法一样。在调用这样的方法时,您需要在调用场所提供方法特定的类型 — 可以按如下方式显式提供:
public class MyClass { public static T SomeMethod(T t,X x) {..} } int number = MyClass.SomeMethod(3,"AAA");
或者依靠类型推理(如果可能):
int number = MyClass.SomeMethod(3,"AAA");
一般静态方法遵守施加于它们在类级别使用的一般类型参数的所有约束。就像实例方法一样,您可以为由静态方法定义的一般类型参数提供约束:
public class MyClass { public static T SomeMethod(T t) where T : IComparable {...} }
C# 中的运算符只是静态方法而已,并且 C# 允许您为自己的一般类型重载运算符。假设代码块 3 的一般 LinkedList 提供了用于串联链表的 + 运算符。+ 运算符使您能够编写下面这段优美的代码:
LinkedList list1 = new LinkedList(); LinkedList list2 = new LinkedList(); ... LinkedList list3 = list1+list2;
代码块 7 显示 LinkedList 类上的一般 + 运算符的实现。请注意,运算符不能定义新的一般类型参数。
代码块 7. 实现一般运算符
public class LinkedList { public static LinkedList operator+(LinkedList lhs, LinkedList rhs) { return concatenate(lhs,rhs); } static LinkedList concatenate(LinkedList list1, LinkedList list2) { LinkedList newList = new LinkedList(); Node 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 { public delegate void GenericDelegate(T t); public void SomeMethod(T t) {...} }
在为包含类指定类型时,也会影响到委托:
MyClass obj = new MyClass(); MyClass.GenericDelegate del; del = new MyClass.GenericDelegate(obj.SomeMethod); del(3);
C# 2.0 使您可以将方法引用的直接分配转变为委托变量:
MyClass obj = new MyClass(); MyClass.GenericDelegate del; del = obj.SomeMethod;
我将把该功能称为委托推理。编译器能够推断出您分配到其中的委托的类型,查明目标对象是否具有采用您指定的名称的方法,并且验证该方法的签名匹配。然后,编译器创建所推断出的参数类型(包括正确的类型而不是一般类型参数)的新委托,并且将新委托分配到推断出的委托中。
像类、结构和方法一样,委托也可以定义一般类型参数:
public class MyClass { public delegate void GenericDelegate(T t,X x); }
在类的作用范围外部定义的委托可以使用一般类型参数。在该情况下,在声明和实例化委托时,必须为其提供类型实参:
public delegate void GenericDelegate(T t); public class MyClass { public void SomeMethod(int number) {...} } MyClass obj = new MyClass(); GenericDelegate del; del = new GenericDelegate(obj.SomeMethod); del(3);
另外,还可以在分配委托时使用委托推理:
MyClass obj = new MyClass(); GenericDelegate del; del = obj.SomeMethod;
当然,委托可以定义约束以伴随它的一般类型参数:
public delegate void MyDelegate(T t) where T : IComparable;
委托级别约束只在使用端实施(在声明委托变量和实例化委托对象时),类似于在类型或方法的作用范围中实施的其他任何约束。
一般委托对于事件尤其有用。您可以精确地定义一组有限的一般委托(只按照它们需要的一般类型参数的数量进行区分),并且使用这些委托来满足所有事件处理需要。代码块 8 演示了一般委托和一般事件处理方法的用法。
代码块 8. 一般事件处理
public delegate void GenericEventHandler (S sender,A args); public class MyPublisher { public event GenericEventHandler MyEvent; public void FireEvent() { MyEvent(this,EventArgs.Empty); } } public class MySubscriber //Optional: can be a specific type { public void SomeMethod(MyPublisher sender,A args) {...} } MyPublisher publisher = new MyPublisher(); MySubscriber subscriber = new MySubscriber(); 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 list = new LinkedList(); Type type1 = typeof(LinkedList); Type type2 = list.GetType(); Debug.Assert(type1 == type2);
typeof 和 GetType() 都可以对一般类型参数进行操作:
public class MyClass { public void SomeMethod(T t) { Type type = typeof(T); Debug.Assert(type == t.GetType()); } }
此外,typeof 运算符还可以对未绑定的一般类型进行操作。例如:
public class MyClass {} Type unboundedType = typeof(MyClass<>); Trace.WriteLine(unboundedType.ToString()); //Writes: MyClass`1[T]
所追踪的数字 1 是所使用的一般类型的一般类型参数的数量。请注意空 <> 的用法。要对带有多个类型参数的未绑定一般类型进行操作,请在 <> 中使用“,”:
public class LinkedList {...} 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 list = new LinkedList(); 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 list = new LinkedList(); 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 : Attribute {...}
然而,属性类可以通过使用一般类型或者定义 Helper 一般方法(像其他任何类型一样)在内部利用泛型:
public class SomeAttribute : Attribute { void SomeMethod(T t) {...} LinkedList m_List = new LinkedList(); }