泛型接口
没有泛型接口,每次试图使用一个非泛型接口(如IComparable)来操纵一个值类型时,都会进行装箱,而且会丢失编译时的类型安全性。这 会严重限制泛型类型的应用。所以,CLR提供了对泛型接口的支持。一个引用类型或值类型为了实现一个泛型接口,可以具体指定类型实参;另外,一个类型也可 以保持类型实参的未指定状态来实现一个泛型接口。来看一些例子:
以下泛型接口定义是作为FCL的一部分发布的:
public
interface
IEnumerable
<
T
>
: IDisposable, IEnumerator {
T Current {
get
; }
}
下面这个示例实现了上述泛型接口,且指定了类型实参:
internal
sealed
class
Triangle : IEnumerator
<
Point
>
{
private
Point[] m_vertices;
Point Current {
get
{...} }
}
下例实现了相同的泛型接口,但保持类型实参的未指定状态:
internal
sealed
class
ArrayEnumerator
<
T
>
: IEnumerator
<
T
>
{
private
T[] m_array;
//
IEnumerator<T>的Current属性是T类型
T Current {
get
{...} }
}
注意,一个ArrayEnumerator对象可以枚举一系列T对象(其中的T没有指定,允许代码使用泛型ArrayEnumerator类型在以后为T指定一个具体的类型)。
泛型委托
CLR支持泛型委托,目的是保证任何类型的对象都能以一种类型安全的方式传给一个回调方法。此外,泛型委托允许一个值类型实例在传给一个回调方 法时不执行任何装箱处理。所谓委托,实际是提供了4个方法的一个类定义:一个构造器、一个Invoke方法、一个BeginInvoke方法和一个 EndInvoke方法。如果定义的一个委托类型指定了类型参数,编译器会定义好委托类的方法。
假定像下面这样定义了一个泛型委托:
public
delegate
TReturn CallMe
<
TReturn, TKey, TValue
>
{TKey key, TValue value);
编译器会将它转换成一个类,该类在逻辑上可以像下面这样表达:
public
sealed
class
CallMe
<
TReturn, TKey, TValue
>
: MulticastDelegate
{
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还提供了用于事件的一个泛型委托。下面展示了这个委托:
public
delegate
void
EventHandler
<
TEventArgs
>
(
object
sender, TEventArgs e)
where
TEventArgs : EventArgs;
泛型方法
定义一个泛型引用类型、值类型或接口时,这些类型中定义的任何方法都可以引用类型指定的一个类型参数。类型参数可作为方法的参数,作为方法的返 回值,或作为方法内部定义的一个局部变量来使用。然而,CLR还允许一个方法指定它独有的类型参数。这些类型参数可用于参数、返回值或局部变量。如下所 示:
internal
sealed
class
GenericType
<
T
>
{
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;
}
}
为接受out和ref参数的方法使用泛型类型,将非常有趣,因为作为一个out/ref实参传递的变量必须具有与方法参数相同的类型,以避免潜在的对 类型安全性的破坏。事实上,Interlocked类的Exchange和CompareExchange方法正是考虑到这个原因而提供了泛型重载版本:
public
static
class
Interlocked
{
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)。也就是说,编译器在调用一个泛型方法的时候自动判断(或推导)要使用的类型。如以下代码:
private
static
void
Swap
<
T
>
(
ref
T o1,
ref
o2)
{
T temp
=
o1;
o1
=
o2;
o2
=
temp;
}
private
static
void
CallingSwapUsingInference()
{
int
n1
=
1
, n2
=
2
;
Swap(
ref
n1,
ref
n2);
//
调用Swap<int>
string
s1
=
"
a
"
;
object
s2
=
"
b
"
;
Swap(
ref
s1,
ref
s2);
//
错误,不能推导类型
}
一个类型可以定义多个方法,让其中一个方法接受一个具体的数据类型,让另一个方法接受一个泛型参数,如下所示:
private
static
void
Display(
string
s)
{
Console.WriteLine(s);
}
private
static
void
Display
<
T
>
(T o)
{
Display(o.ToString());
//
调用Display(string)
}
下面展示Display方法的一些调用方式:
Display(
"
jeff
"
);
//
调用Display(string)
Display(
123
);
//
调用Display<T>(T)
Display
<
string
>
(
"
ai
"
);
//
调用Display<T>(T)
在C#编译器看来,一个更显式的匹配总是优先于一个泛型匹配的。所以,Display("jeff")生成了对非泛型的Display方法的调 用。对Display的第三个调用明确指定了一个泛型类型实参string,这告诉编译器不要尝试去推导类型实参,直接使用指定的类型实参。
泛型和其他成员
在C#中,属性、索引器、事件、操作符方法、构造器和终结器(finalizer)本身不能有类型参数。然而,它们能在一个泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。
C#之所以不允许这些成员指定它们自己的泛型类型参数,是因为MS的C#开发团队认为其他开发人员很少需要将这些成员作为泛型使用。此外,为这些成员添加泛型支持的代码是相当高的,因为必须为语言设计足够的语法。
可验证证性和限制
我们可以利用“约束”来限制能指定成泛型实参类型的类型变量。通过限制类型数量,我们可以对那些类型执行更多的操作。以下是一个
Min方法,它指定了一个约束:
public
static
T Min
<
T
>
(T o1, T o2)
where
T : IComparable
<
T
>
{
if
(o1.CompareTo(o2)
<
0
)
return
o1;
return
o2;
}
where关键字告诉编译器为T指定的任何类型都必须实现同一个类型(T)的泛型IComparable接口。现在,当代码引用一个泛型类型或方法时,编译器要负责保证类型实参符合指定的约束。如:
private
static
void
CallMin()
{
object
o1
=
"
jeff
"
, o2
=
"
richter
"
;
object
oMin
=
Min
<
Object
>
(o1, o2);
//
Error
}
约束可应用于一个泛型类型的类型参数,或应用于一个泛型方法的类型参数。CLR允许基于类型参数名称或约束来进行重载;只能基于arity来重载类型或方法。示例如下:
//
可定义以下类型
internal
sealed
class
AType {}
internal
sealed
class
AType
<
T
>
{};
internal
sealed
class
AType
<
T1, T2
>
{}
//
error: 与没有约束的AType<T>冲突
internal
sealed
class
AType
<
T
>
where
T : IComparable
<
T
>
{}
//
error:与AType<T1, T2>冲突
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<T>冲突
private
static
void
M
<
T
>
where
T : IComparable
<
T
>
{}
//
error:与M<T1, T2>冲突
private
static
void
M
<
T3, T4
>
{}
}
重写一个virtual泛型方法时,重写过的版本必须指定相同数量的类型参数,且这些类型参数会继承由基类的方法在它们上面指定的约束。事实上,重写的方法根本不准许在它的类型参数上指定任何约束。然而,类型参数的名称是可以改变的。
internal
class
Base
{
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。
指定一个引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的一个类型。如以下示例:
internal
sealed
class
PrimaryConstraintOfStream
<
T
>
where
T : Stream
{
public
void
M(T stream)
{
stream.Close();
}
}
有两个特殊的主要约束:class和struct。其中,class约束向编译器承诺一个指定的类型实参是引用类型。任何类类型、接口类型、委 托类型或数组类型都满足这个约束。struct约束向编译器承诺一个指定的类型实参是值类型,包括枚举在内的任何值类型都满足这个约束。但编译器和CLR 将任何System.Nullable<T>值类型视为一个特殊类型,而可空的类型不能指定这个约束。原因是 Nullable<T>类型将它的类型参数限制为struct,而CLR希望禁止像 Nullable<Nullable<T>>这样的一个递归类型。
次要约束
一个类型参数可以指定零个或多个次要约束,次要约束代表的是一个接口类型。由于能指定多个接口约束,所以为类型实参指定的类型必须实现所有接口约束。
还有一种辅助约束称为“类型参数约束”,有时也称为“裸类型约束”。这种约束的使用比接口约束少得多。它允许一个泛型类型或方法规定在指定的类型实参之间必须存在一个关系。一个类型参数可以应用零个或多个类型参数约束。下面的示例演示了如何使用类型参数约束:
private
static
List
<
TBase
>
ConvertIList
<
T, TBase
>
(IList
<
T
>
list)
where
T : TBase
{
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的合法和非法调用:
private
static
void
CallingConvertIList()
{
//
构造并初始化一个List<string>(它实现了IList<string>)
IList
<
string
>
ls
=
new
List
<
string
>
();
ls.Add(
"
A String
"
);
//
将IList<string>转换为一个IList<Object>
IList
<
object
>
lo
=
ConvertIList
<
string
,
object
>
(ls);
//
将IList<string>转换为一个IList<IComparable>
IList
<
IComparable lc
=
ConvertIList
<
string
, IComparable
>
(ls);
//
将IList<string>转换为一个IList<IComparable<string>>
IList
<
IComparable
<
string
>>
lcs
=
ConvertIList
<
string
, IComparable
<
string
>>
(ls);
//
将IList<String>转换成一个IList<string>
IList
<
string
>
ls2
=
ConvertIList
<
string
,
string
>
(ls);
//
错误:将IList<string>转换成一个IList<Exception>
IList
<
Exception
>
le
=
ConvertIList
<
string
, Exception
>
(ls);
}
构造器约束
一个类型参数可以指定零个或一个构造器约束。指定构造器约束相当于向编译器承诺:一个指定的类型实参是实现了一个public无参构造器的一个 非抽象类型。注意,如果同时指定了构造器约束和struct约束,C#编译器会认为这是一个错误,因为这是多余的:所有值类型都隐式提供了一个 public无参构造器。
下例就使用了构造器约束来限制它的类型参数:
internal
sealed
class
ConstructorConstraint
<
T
>
where
T :
new
()
{
public
static
T Factory()
{
return
new
T();
}
}
泛型类型变量的转型
将一个泛型类型变量转型为另一个类型是非法的,除非将其转换为与一个约束兼容的类型:
private
static
void
CastingAGenericTypeVariable
<
T
>
(T obj)
{
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。如下所示:
private
static
void
SetDefaultValue
<
T
>
()
{
T temp
=
default
(T);
}
T如果为引用类型,temp的值为null;T如果为值类型,temp的值为0。
将一个泛型类型变量与null进行比较
无论泛型类型是否被约束,使用==或!=操作符将一个泛型变量与null进行比较都是合法的:
private
static
void
ComparingWithNull
<
T
>
(T obj)
{
if
(obj
==
null
)
{
//
对于值类型来说,此处永远不会执行。
}
}
如果调用这个方法,并为类型参数传递一个值类型,那么JIT编译器知道if语句永远都不会为true,所以不会为if测试或都大括号内的代码生 成本地代码。如果换用!=操作符,JIT编译器不会为if测试生成代码(因为它肯定为true),但会为if大括号内的代码生成本地代码。
另外,如果T被约束成一个struct,C#编译器会报错。
两个泛型类型变量的相互比较
如果泛型类型参数不是一个引用类型,那么对同一个泛型类型的两个变量进行比较是非法的。
泛型类型变量作为操作数使用
将操作符应用于泛型类型的操作数存在大量问题。编译器在编译时无法确定类型,这意味着不能将任何操作符应用于泛型类型的变量