一般方法
在 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();
}
返回页首
泛型和 .NET Framework
为了对本文做一下小结,下面介绍 .NET 中除 C# 本身以外的其他一些领域如何利用泛型或者与泛型交互。
System.Array 和泛型
System.Array 类型通过很多一般静态方法进行了扩展。这些一般静态方法专门用于自动执行和简化处理数组的常见任务,例如,遍历数组并且对每个元素执行操作、扫描数组,以查找匹配某个条件(谓词)的值、对数组进行变换和排序等等。代码块 11 是这些静态方法的部分清单。
代码块 11. System.Array 的一般方法
public abstract class Array
{
//Partial listing of the static methods:
public static IList AsReadOnly(T[] array);
public static int BinarySearch(T[] array, T value);
public static int BinarySearch(T[] array, T value,
IComparer comparer);
public static U[] ConvertAll(T[] array,
Converter converter);
public static bool Exists(T[] array,Predicate match);
public static T Find(T[] array,Predicate match);
public static T[] FindAll(T[] array, Predicate match);
public static int FindIndex(T[] array, Predicate match);
public static void ForEach(T[] array, Action action);
public static int IndexOf(T[] array, T value);
public static void Sort(K[] keys, V[] items,
IComparer comparer);
public static void Sort(T[] array,Comparison comparison)
}
System.Array 的静态一般方法都使用 System 命名空间中定义的下列四个一般委托:
public delegate void Action(T t);
public delegate int Comparison(T x, T y);
public delegate U Converter(T from);
public delegate bool Predicate(T t);
代码块 12 演示如何使用这些一般方法和委托。它用从 1 到 20 的所有整数初始化一个数组。然后,代码通过一个匿名方法和 Action 委托,使用 Array.ForEach() 方法来跟踪这些数字。使用第二个匿名方法和 Predicate 委托,代码通过调用 Array.FindAll() 方法(它返回另一个相同的一般类型的数组),来查找该数组中的所有质数。最后,使用相同的 Action 委托和匿名方法来跟踪这些质数。请注意代码块 12 中类型参数推理的用法。您在使用静态方法时无须指定类型参数。
代码块 12. 使用 System.Array 的一般方法
int[] numbers = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
Action trace = delegate(int number)
{
Trace.WriteLine(number);
};
Predicate isPrime = delegate(int number)
{
switch(number)
{
case 1:case 2:case 3:case 5:case 7:
case 11:case 13:case 17:case 19:
return true;
default:
return false;
}
};
Array.ForEach(numbers,trace);
int[] primes = Array.FindAll(numbers,isPrime);
Array.ForEach(primes,trace);
在 System.Collections.Generic 命名空间中定义的类 List 中,也可以得到类似的一般方法。这些方法使用四个相同的一般委托。实际上,您还可以在您的代码中利用这些委托,如以下部分所示。