泛型接口
没有泛型接口,每次试图使用一个非泛型接口(如IComparable)来操纵一个值类型时,都会进行装箱,而且会丢失编译时的类型安全性。这会严重限制泛型类型的应用。所以,CLR提供了对泛型接口的支持。一个引用类型或值类型为了实现一个泛型接口,可以具体指定类型实参;另外,一个类型也可以保持类型实参的未指定状态来实现一个泛型接口。来看一些例子:
以下泛型接口定义是作为FCL的一部分发布的:
T Current { get ; }
}
下面这个示例实现了上述泛型接口,且指定了类型实参:
private Point[] m_vertices;
Point Current { get {...} }
}
下例实现了相同的泛型接口,但保持类型实参的未指定状态:
private T[] m_array;
// IEnumerator
T Current { get {...} }
}
注意,一个ArrayEnumerator对象可以枚举一系列T对象(其中的T没有指定,允许代码使用泛型ArrayEnumerator类型在以后为T指定一个具体的类型)。
泛型委托
CLR支持泛型委托,目的是保证任何类型的对象都能以一种类型安全的方式传给一个回调方法。此外,泛型委托允许一个值类型实例在传给一个回调方法时不执行任何装箱处理。所谓委托,实际是提供了4个方法的一个类定义:一个构造器、一个Invoke方法、一个BeginInvoke方法和一个EndInvoke方法。如果定义的一个委托类型指定了类型参数,编译器会定义好委托类的方法。
假定像下面这样定义了一个泛型委托:
编译器会将它转换成一个类,该类在逻辑上可以像下面这样表达:
{
public CallMe(Object object , IntPtr method);
public TReturn Invoke(TKey key, TValue value);
public IAsyncResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object );
public TReturn EndInvoke(IAsyncResult result);
}
FCL配套提供了许多泛型委托类型,其中大多数都用于集合处理。下面是一些例子:
public delegate void Action < T > (T obj);
// 通常用于比较两个集合项,以进行排序
public delegate Int32 Comparison < T > (T x, T y);
// 通常用于将集合项从一种类型转换成另一种类型
public delegate TOutput Converter < TInput, TOutput > (TInput input);
FCL还提供了用于事件的一个泛型委托。下面展示了这个委托:
泛型方法
定义一个泛型引用类型、值类型或接口时,这些类型中定义的任何方法都可以引用类型指定的一个类型参数。类型参数可作为方法的参数,作为方法的返回值,或作为方法内部定义的一个局部变量来使用。然而,CLR还允许一个方法指定它独有的类型参数。这些类型参数可用于参数、返回值或局部变量。如下所示:
{
private T m_value;
public GenericType(T value) { m_value = value; }
public TOutput Converter < TOutput > ()
{
TOutput result = (TOutput)Convert.ChangeType(m_value, typeof (TOutput));
return reault;
}
}
{
public static T Exchange < T > ( ref T location, T value) where T : class ;
public static T CompareExchange < T > ( ref T location, T value, T comparand) where T : class ;
}
泛型方法和类型接口
C#泛型语法因为涉及大量小于和大于符号,影响到代码的可读性。为增强代码创建、可读性和可维护性,C#编译器支持在调用一个泛型方法时进行“类型推导”(type inference)。也就是说,编译器在调用一个泛型方法的时候自动判断(或推导)要使用的类型。如以下代码:
{
T temp = o1;
o1 = o2;
o2 = temp;
}
private static void CallingSwapUsingInference()
{
int n1 = 1 , n2 = 2 ;
Swap( ref n1, ref n2); // 调用Swap
string s1 = " a " ;
object s2 = " b " ;
Swap( ref s1, ref s2); // 错误,不能推导类型
}
一个类型可以定义多个方法,让其中一个方法接受一个具体的数据类型,让另一个方法接受一个泛型参数,如下所示:
{
Console.WriteLine(s);
}
private static void Display < T > (T o)
{
Display(o.ToString()); // 调用Display(string)
}
下面展示Display方法的一些调用方式:
Display( 123 ); // 调用Display
Display < string > ( " ai " ); // 调用Display
在C#编译器看来,一个更显式的匹配总是优先于一个泛型匹配的。所以,Display("jeff")生成了对非泛型的Display方法的调用。对Display的第三个调用明确指定了一个泛型类型实参string,这告诉编译器不要尝试去推导类型实参,直接使用指定的类型实参。
泛型和其他成员
在C#中,属性、索引器、事件、操作符方法、构造器和终结器(finalizer)本身不能有类型参数。然而,它们能在一个泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。
C#之所以不允许这些成员指定它们自己的泛型类型参数,是因为MS的C#开发团队认为其他开发人员很少需要将这些成员作为泛型使用。此外,为这些成员添加泛型支持的代码是相当高的,因为必须为语言设计足够的语法。
可验证证性和限制
我们可以利用“约束”来限制能指定成泛型实参类型的类型变量。通过限制类型数量,我们可以对那些类型执行更多的操作。以下是一个
Min方法,它指定了一个约束:
{
if (o1.CompareTo(o2) < 0 ) return o1;
return o2;
}
where关键字告诉编译器为T指定的任何类型都必须实现同一个类型(T)的泛型IComparable接口。现在,当代码引用一个泛型类型或方法时,编译器要负责保证类型实参符合指定的约束。如:
{
object o1 = " jeff " , o2 = " richter " ;
object oMin = Min < Object > (o1, o2); // Error
}
internal sealed class AType {}
internal sealed class AType < T > {};
internal sealed class AType < T1, T2 > {}
// error: 与没有约束的AType
internal sealed class AType < T > where T : IComparable < T > {}
// error:与AType
internal sealed class AType < T3, T4 > {}
internal sealed class AnotherType
{
// 可定义以下方法
private static void M() {}
private static void M < T > () {}
private static void M < T1, T2 > () {}
// error:与没有约束的M
private static void M < T > where T : IComparable < T > {}
// error:与M
private static void M < T3, T4 > {}
}
重写一个virtual泛型方法时,重写过的版本必须指定相同数量的类型参数,且这些类型参数会继承由基类的方法在它们上面指定的约束。事实上,重写的方法根本不准许在它的类型参数上指定任何约束。然而,类型参数的名称是可以改变的。
{
public virtual void M < T1, T2 > ()
where T1 : struct
where T2 : class {}
}
internal sealed class Derived : Base
{
public override void M < T3, T4 > ()
where T3 : EventArgs // Error
where T4 : class // Error
{}
}
主要约束
类型参数可以指定零个或一个主要约束。主要约束可以是一个引用类型,它标识了一个没有密封的类。不需要指定以下特殊的引用类型:System.Object、System.Array、System.Delegate、System.MulticastDelegate、System.ValueType、System.Enum或System.Void。
指定一个引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的一个类型。如以下示例:
{
public void M(T stream)
{
stream.Close();
}
}
有两个特殊的主要约束:class和struct。其中,class约束向编译器承诺一个指定的类型实参是引用类型。任何类类型、接口类型、委托类型或数组类型都满足这个约束。struct约束向编译器承诺一个指定的类型实参是值类型,包括枚举在内的任何值类型都满足这个约束。但编译器和CLR将任何System.Nullable
次要约束
一个类型参数可以指定零个或多个次要约束,次要约束代表的是一个接口类型。由于能指定多个接口约束,所以为类型实参指定的类型必须实现所有接口约束。
还有一种辅助约束称为“类型参数约束”,有时也称为“裸类型约束”。这种约束的使用比接口约束少得多。它允许一个泛型类型或方法规定在指定的类型实参之间必须存在一个关系。一个类型参数可以应用零个或多个类型参数约束。下面的示例演示了如何使用类型参数约束:
{
List < Base > baseList = new List < TBase > (list.Count);
for ( int index = 0 ; index < list.Count; index ++ )
{
baseList.Add(list[index]);
}
return baseList;
}
该方法指定了两个类型参数,其中T参数由TBase类型参数约束。也就是说不管为T指定什么类型实参,这个类型都必须兼容于为TBase指定的类型实参。下面的方法展示了对ConvertIList的合法和非法调用:
{
// 构造并初始化一个List
IList < string > ls = new List < string > ();
ls.Add( " A String " );
// 将IList
IList < object > lo = ConvertIList < string , object > (ls);
// 将IList
IList < IComparable lc = ConvertIList < string , IComparable > (ls);
// 将IList
IList < IComparable < string >> lcs = ConvertIList < string , IComparable < string >> (ls);
// 将IList
IList < string > ls2 = ConvertIList < string , string > (ls);
// 错误:将IList
IList < Exception > le = ConvertIList < string , Exception > (ls);
}
构造器约束
一个类型参数可以指定零个或一个构造器约束。指定构造器约束相当于向编译器承诺:一个指定的类型实参是实现了一个public无参构造器的一个非抽象类型。注意,如果同时指定了构造器约束和struct约束,C#编译器会认为这是一个错误,因为这是多余的:所有值类型都隐式提供了一个public无参构造器。
下例就使用了构造器约束来限制它的类型参数:
{
public static T Factory()
{
return new T();
}
}
泛型类型变量的转型
将一个泛型类型变量转型为另一个类型是非法的,除非将其转换为与一个约束兼容的类型:
{
int x = ( int )obj; // 错误
string s = ( string )obj; // 错误
int x2 = ( int )( object )obj; // 无错误
string s2 = ( string )( object )obj; // 无错误
string s3 = obj as string ; // 无错误
}
将一个泛型类型变量设为默认值
将泛型类型变量设为null是非法的,除非将泛型类型约束成一个引用类型。
我们可以使用default关键字将一个引用类型设为null,或将一个值类型设为0。如下所示:
{
T temp = default (T);
}
T如果为引用类型,temp的值为null;T如果为值类型,temp的值为0。
将一个泛型类型变量与null进行比较
无论泛型类型是否被约束,使用==或!=操作符将一个泛型变量与null进行比较都是合法的:
{
if (obj == null )
{
// 对于值类型来说,此处永远不会执行。
}
}
如果调用这个方法,并为类型参数传递一个值类型,那么JIT编译器知道if语句永远都不会为true,所以不会为if测试或都大括号内的代码生成本地代码。如果换用!=操作符,JIT编译器不会为if测试生成代码(因为它肯定为true),但会为if大括号内的代码生成本地代码。
另外,如果T被约束成一个struct,C#编译器会报错。
两个泛型类型变量的相互比较
如果泛型类型参数不是一个引用类型,那么对同一个泛型类型的两个变量进行比较是非法的。
泛型类型变量作为操作数使用
将操作符应用于泛型类型的操作数存在大量问题。编译器在编译时无法确定类型,这意味着不能将任何操作符应用于泛型类型的变量。