泛型类声明
泛型类声明
泛型类声明是一种类的声明,它需要提供类型实参才能构成实际类型。
类声明可以有选择地定义类型形参:
attributesopt class-modifiersopt class identifier type-parameter-listopt class-baseopt
type-parameter-constraints-clausesopt class-body ;opt
只有提供了一个 type-parameter-list,才可以为这个类声明提供 type-parameter-constraints-clauses。
提供了 type-parameter-list 的类声明是一个泛型类声明。此外,任何嵌套在泛型类声明或泛型结构声明中的类本身就是一个泛型类声明,因为必须为包含类型提供类型形参才能创建构造类型。
除了明确指出的地方外,泛型类声明与非泛型类声明遵循相同的规则。泛型类声明可嵌套在非泛型类声明中。
使用构造类型 (constructed type)引用泛型类。给定下面的泛型类声明
List<T>、List<int> 和 List<List<string>> 就是其构造类型的一些示例。使用一个或多个类型形参的构造类型(例如 List<T>)称为开放构造类型 (open constructed type)。不使用类型形参的构造类型(例如 List<int>)称为封闭构造类型 (closed constructed type)。
泛型类型可根据类型形参的数目进行“重载”;这就是说,同一命名空间或外层类型声明中的两个类型声明可以使用相同标识符,只要这两个声明具有不同数目的类型形参即可。
class C<V> {}
struct C<U,V> {} // Error, C with two type parameters defined twice
class C<A,B> {} // Error, C with two type parameters defined twice
类型名称解析、简单名称解析和成员访问、过程中使用的类型查找规则均会考虑类型形参的数目。
泛型类声明的基接口必须满足唯一性规则。
类型形参
类型形参可在类声明中提供。每个类型形参都是一个简单标识符,代表一个为创建构造类型而提供的类型实参的占位符。类型形参是将来提供的类型的形式占位符。而类型实参是在创建构造类型时替换类型形参的实际类型。
< type-parameters >
type-parameters:
attributesopt type-parameter
type-parameters , attributesopt type-parameter
type-parameter:
identifier
类声明中的每个类型形参在该类的声明空间中定义一个名称。因此,它不能与另一个类型形参或该类中声明的成员具有相同的名称。类型形参不能与类型本身具有相同的名称。
类上的类型形参的作用域包括 class-base、type-parameter-constraints-clauses 和 class-body。与类的成员不同,此作用域不会扩展到派生类。在其作用域中,类型形参可用作类型。
value-type
reference-type
type-parameter
由于类型形参可使用许多不同的实际类型实参进行实例化,因此类型形参具有与其他类型稍微不同的操作和限制。这包括:
· 不能直接使用类型形参声明基类或接口
· 类型形参上的成员查找规则取决于应用到该类型形参的约束(如果有)。
· 类型形参的可用转换取决于应用到该类型形参的约束(如果有)。
· 如果事先不知道由类型形参给出的类型是引用类型不能将标识null 转换为该类型。不过,可以改为使用默认值表达式。此外,具有由类型形参给出的类型的值可以使用 == 和 != 与null 进行比较,除非该类型形参具有值类型约束。
· 仅当类型形参受 constructor-constraint 或值类型约束的约束时,才能将 new 表达式与类型形参联合使用。
· 不能在属性中的任何位置上使用类型形参。
· 不能在成员访问或类型名称中使用类型形参标识静态成员或嵌套类型。
· 在不安全代码中,类型形参不能用作 unmanaged-type。
作为类型,类型形参纯粹是一个编译时构造。在运行时,每个类型形参都绑定到一个运行时类型,运行时类型是通过向泛型类型声明提供类型实参来指定的。因此,使用类型形参声明的变量的类型在运行时将是封闭构造类型。涉及类型形参的所有语句和表达式的运行时执行都使用作为该形参的类型实参提供的实际类型。
实例类型
每个类声明都有一个关联的构造类型,即实例类型 (instance type)。对于泛型类声明,实例类型是通过从该类型声明创建构造类型来构成的,所提供的每个类型实参替换对应的类型形参。由于实例类型使用类型形参,因此只能在类型形参的作用域中使用该实例类型;也就是在类声明的内部。对于在类声明中编写的代码,实例类型为 this 的类型。对于非泛型类,实例类型就是所声明的类。下面显示几个类声明以及它们的实例类型:
{
class B {} // instance type: A<T>.B
class C<U> {} // instance type: A<T>.C<U>
}
class D {} // instance type: D
类声明中指定的基类可以是构造类类型。基类本身不能是类型形参,但在其作用域中可以包含类型形参。
泛型类声明不能使用 System.Attribute 作为直接或间接基类。
类声明中指定的基接口可以是构造接口类型。基接口本身不能是类型形参,但在其作用域中可以包含类型形参。下面的代码演示类实现和扩展构造类型的方法:
interface I1<V> {}
class D: C<string,int>, I1<string> {}
class E<T>: C<int,T>, I1<T> {}
泛型类声明的基接口必须满足唯一性规则。
如果类中的方法重写或实现基类或接口中的方法,则必须为特定类型提供相应的方法。下面的代码演示如何重写和实现方法。对此做了进一步的解释。
{
public virtual void M1(U x, List<V> y) {}
}
interface I1<V>
{
V M2(V);
}
class D: C<string,int>, I1<string>
{
public override void M1(string x, List<int> y) {}
public string M2(string x) {}
}
泛型类的所有成员都可以直接或作为构造类型的一部分使用任何包容类 (enclosing class) 中的类型形参。当在运行时使用特定的封闭构造类型时,所出现的每个类型形参都被替换成为该构造类型提供的实际类型实参。例如:
{
public V f1;
public C<V> f2 = null;
public C(V x) {
this.f1 = x;
this.f2 = this;
}
}
class Application
{
static void Main() {
C<int> x1 = new C<int>(1);
Console.WriteLine(x1.f1); // Prints 1
C<double> x2 = new C<double>(3.1415);
Console.WriteLine(x2.f1); // Prints 3.1415
}
}
在实例函数成员中,类型 this 是包含这些成员的声明的实例类型。
除了使用类型形参作为类型以外,泛型类声明中的成员与非泛型类的成员遵循相同的规则。下面几小节将讨论适用于特定种类的成员的附加规则。
泛型类中的静态字段
泛型类声明中的静态变量在相同封闭构造类型的所有实例之间共享,但是不会在不同封闭构造类型的实例之间共享。不管静态变量的类型是否涉及任何类型形参,这些规则都适用。
例如:
class C<V>
{
static int count = 0;
public C() {
count++;
}
public static int Count {
get { return count; }
}
}
class Application
{
static void Main() {
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 1
C<double> x2 = new C<double>();
Console.WriteLine(C<int>.Count); // Prints 1
C<int> x3 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 2
}
}
泛型类中的静态构造函数
{
static int count = 0;
public C() {
count++;
}
public static int Count {
get { return count; }
}
}
class Application
{
static void Main() {
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 1
C<double> x2 = new C<double>();
Console.WriteLine(C<int>.Count); // Prints 1
C<int> x3 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 2
}
}
泛型类中的静态构造函数用于初始化静态字段,并为从该泛型类声明创建的每个不同封闭构造类型执行其他初始化操作。泛型类型声明的类型形参处于作用域中,并且可在静态构造函数的函数体中使用。
新的封闭构造类类型在第一次发生下列任一情况时进行初始化:
· 创建该封闭构造类型的实例。
· 引用该封闭构造类型的任何静态成员。
为了初始化新的封闭构造类类型,需要先为该特定的封闭构造类型创建一组新的静态字段。将其中的每个静态字段初始化为默认值。下一步,为这些静态字段执行静态字段初始值设定项。最后,执行静态构造函数。
由于静态构造函数只为每个封闭构造类类型执行一次,因此对于无法通过约束在编译时进行检查的类型形参来说,此处是进行运行时检查的方便位置。例如,下面的类型使用静态构造函数检查类型实参是否为一个枚举:
class Gen<T> where T: struct
{
static Gen() {
if (!typeof(T).IsEnum) {
throw new ArgumentException("T must be an enum");
}
}
}
访问受保护成员
{
static Gen() {
if (!typeof(T).IsEnum) {
throw new ArgumentException("T must be an enum");
}
}
}
在泛型类声明中,通过从该泛型类构造的任何类类型的实例,可以对继承的受保护实例成员进行访问。具体而言,指定的用于访问 protected 和 protected internal 实例成员的规则通过下面针对泛型的规则进行了扩充:
· 在泛型类 G 中,如果 E 的类型是从 G 构造的类类型或从 G 构造的类类型继承的类类型,则使用 E.M 形式的 primary-expression 访问继承的受保护实例成员 M 是允许的。
在下面的示例中
{
protected T x;
}
class D<T>: C<T>
{
static void F() {
D<T> dt = new D<T>();
D<int> di = new D<int>();
D<string> ds = new D<string>();
dt.x = default(T);
di.x = 123;
ds.x = "test";
}
}
对 x 的三个赋值是允许的,因为它们全都通过从该泛型类型构造的类类型的实例进行。
泛型类中的重载
泛型类声明中的方法、构造函数、索引器和运算符可以被重载。虽然声明的签名必须唯一,但是在替换类型实参时可能会导致出现完全相同的签名。在这样的情况下,重载解析的附加规则将挑选最明确的
成员。
下面的示例根据此规则演示有效和无效的重载:
interface I1<T> {}
interface I2<T> {}
class G1<U>
{
int F1(U u); // Overload resulotion for G<int>.F1
int F1(int i); // will pick non-generic
void F2(I1<U> a); // Valid overload
void F2(I2<U> a);
}
class G2<U,V>
{
void F3(U u, V v); // Valid, but overload resolution for
void F3(V v, U u); // G2<int,int>.F3 will fail
void F4(U u, I1<V> v); // Valid, but overload resolution for
void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail
void F5(U u1, I1<V> v2); // Valid overload
void F5(V v1, U u2);
void F6(ref U u); // valid overload
void F6(out V v);
}
形参数组方法和类型形参
interface I2<T> {}
class G1<U>
{
int F1(U u); // Overload resulotion for G<int>.F1
int F1(int i); // will pick non-generic
void F2(I1<U> a); // Valid overload
void F2(I2<U> a);
}
class G2<U,V>
{
void F3(U u, V v); // Valid, but overload resolution for
void F3(V v, U u); // G2<int,int>.F3 will fail
void F4(U u, I1<V> v); // Valid, but overload resolution for
void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail
void F5(U u1, I1<V> v2); // Valid overload
void F5(V v1, U u2);
void F6(ref U u); // valid overload
void F6(out V v);
}
{
static void F(int x, int y, params V[] args);
}
对该方法的如下展开形式的调用:
C<object>.F(10, 20, 30, 40);
C<string>.F(10, 20, "hello", "goodbye");
完全对应于:
C<int>.F(10, 20, new int[] {});
C<object>.F(10, 20, new object[] {30, 40});
C<string>.F(10, 20, new string[] {"hello", "goodbye"} );
重写和泛型类
C<object>.F(10, 20, new object[] {30, 40});
C<string>.F(10, 20, new string[] {"hello", "goodbye"} );
和往常一样,泛型类中的函数成员可以重写基类中的函数成员。在确定被重写的基成员时,必须通过替换类型实参来确定基类的成员。一旦确定了基类的成员,重写规则就与非泛型类
相同。
下面的示例演示重写规则如何在存在泛型的情况下起作用:
abstract class C<T>
{
public virtual T F() {}
public virtual C<T> G() {}
public virtual void H(C<T> x) {}
}
class D: C<string>
{
public override string F() {} // Ok
public override C<string> G() {} // Ok
public override void H(C<T> x) {} // Error, should be C<string>
}
class E<T,U>: C<U>
{
public override U F() {} // Ok
public override C<U> G() {} // Ok
public override void H(C<T> x) {} // Error, should be C<U>
}
泛型类中的运算符
{
public virtual T F() {}
public virtual C<T> G() {}
public virtual void H(C<T> x) {}
}
class D: C<string>
{
public override string F() {} // Ok
public override C<string> G() {} // Ok
public override void H(C<T> x) {} // Error, should be C<string>
}
class E<T,U>: C<U>
{
public override U F() {} // Ok
public override C<U> G() {} // Ok
public override void H(C<T> x) {} // Error, should be C<U>
}
泛型类声明可以定义运算符,所遵循的规则与非泛型类声明相同。运算符声明中使用类声明的实例类型的方式必须与运算符的正常使用规则类似,具体如下:
· 一元运算符必须以该实例类型的单个参数为操作对象。一元的 ++ 和 -- 运算符必须返回该实例类型或从该实例类型派生的类型。
· 二元运算符的参数中必须至少有一个属于该实例类型。
· 转换运算符的形参类型或返回类型必须属于该实例类型。
下面演示泛型类中的有效运算符声明的一些示例:
{
public static X<T> operator ++(X<T> operand) {}
public static int operator *(X<T> op1, int op2) {}
public static explicit operator X<T>(T value) {}
}
对于从源类型 S 转换到目标类型 T 的转换运算符,在应用指定的规则时,与 S 或 T 关联的任何类型形参都被视为与其他类型没有继承关系的唯一类型,并忽略对那些类型形参的所有约束。
在下面的示例中
class D<T>: C<T>
{
public static implicit operator C<int>(D<T> value) {} // Ok
public static implicit operator C<string>(D<T> value) {} // Ok
public static implicit operator C<T>(D<T> value) {} // Error
}
前两个运算符声明是允许的,T 和 int 以及 string 分别被视为没有关系的唯一类型。但是,第三个运算符是错误的,因为 C<T> 是 D<T> 的基类。
对于某些类型实参,可以声明这样的运算符,即这些运算符指定了已经作为预定义转换而存在的转换。在下面的示例中
{
public static implicit operator Convertible<T>(T value) {}
public static explicit operator T(Convertible<T> value) {}
}
当把类型 object 指定为 T 的类型实参时,第二个运算符将声明一个已经存在的转换(存在从任何类型到类型 object 的隐式转换,因此也存在显式转换)。
在两个类型之间存在预定义转换的情况下,这些类型之间的任何用户定义的转换将被忽略。具体而言:
· 如果存在从类型 S 到类型T 的预定义隐式转换,则从S 到T 的所有用户定义的转换(隐式或显式)将被忽略。
· 如果存在从类型S 到类型T 的预定义显式转换,则从 S 到T 的所有用户定义的显式转换将被忽略。但是,仍然会考虑从 S 到 T 的用户定义的隐式转换。
对于除 object 以外的所有类型,上面的 Convertible<T> 类型声明的运算符都不会与预定义的转换发生冲突。例如:
i = n; // Error
i = (int)n; // User-defined explicit conversion
n = i; // User-defined implicit conversion
n = (Convertible<int>)i; // User-defined implicit conversion
}
但是对于类型 object,除了下面这个特例之外,预定义的转换将在其他所有情况下隐藏用户定义的
转换:
o = n; // Pre-defined boxing conversion
o = (object)n; // Pre-defined boxing conversion
n = o; // User-defined implicit conversion
n = (Convertible<object>)o; // Pre-defined unboxing conversion
}
泛型类中的嵌套类型
泛型类声明可以包含嵌套的类型声明。包容类的类型形参可以在嵌套类型中使用。嵌套类型声明可以包含仅适用于该嵌套类型的附加类型形参。
泛型类声明中包含的每个类型声明都隐式地是泛型类型声明。在编写对嵌套在泛型类型中的类型的引用时,必须指定其包容构造类型(包括其类型实参)。但是可在外层类中不加限定地使用嵌套类型;在构造嵌套类型时可以隐式地使用外层类的实例类型。下面的示例演示三种不同的引用从 Inner 创建的构造类型的正确方法;前两种方法是等效的:
{
class Inner<U>
{
public static void F(T t, U u) {}
}
static void F(T t) {
Outer<T>.Inner<string>.F(t, "abc"); // These two statements have
Inner<string>.F(t, "abc"); // the same effect
Outer<int>.Inner<string>.F(3, "abc"); // This type is different
Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg
}
}
嵌套类型中的类型形参可以隐藏外层类型中声明的成员或类型形参,但这是一种不好的编程风格:
{
class Inner<T> // Valid, hides Outer’s T
{
public T t; // Refers to Inner’s T
}
}