接泛型四 20.6.5语法歧义 在§20.9.3和§20.9.4中简单名字(simple-name)和成员访问(member-access)对于表达式来说容易引起语法歧义。例如,语句 F(G<A,B>(7));
可以被解释为对带有两个参数G<A和B>(7)的F的调用[1]。同样,它还能被解释为对带有一个参数的F的调用,这是一个对带有两个类型实参和一个正式参数的泛型方法G的调用。 如果表达式可以被解析为两种不同的有效方法,那么在“>”能被解析作为运算符的所有或一部分时,或者作为一个类型实参列表,那么紧随“>”之后的标记将会被检查。如果它是如下之一: { } ] > : ; , . ? 那么“>”被解析作为类型实参列表。否则“>”被解析作为一个运算符。 20.6.6对委托使用泛型方法 委托的实例可通过引用一个泛型方法的声明而创建。委托表达式确切的编译时处理,包括引用泛型方法的委托创建表达式,这在§20.9.6中进行了描述。 当通过委托调用一个泛型方法时,所使用的类型实参将在委托实例化时被确定。类型实参可以通过类型实参列表显式给定,或者通过类型推断(§20.6.4)而确定。如果采用类型推断,委托的参数类型将被用作推断处理过程的实参类型。委托的返回类型不用于推断。下面的例子展示了为一个委托实例化表达式提供类型实参的方法。 delegate int D(string s , int i) delegate int E(); class X { public static T F<T>(string s ,T t){…} public static T G<T>(){…} static void Main() { D d1 = new D(F<int>); //ok,类型实参被显式给定 D d2 = new D(F); //ok,int作为类型实参而被推断 E e1 = new E(G<int>); //ok,类型实参被显式给定 E e2 = new E(G); //错误,不能从返回类型推断 } } 在先前的例子中,非泛型委托类型使用泛型方法实例化。你也可以使用泛型方法创建一个构造委托类型的实例。在所有情形下,当委托实例被创建时,类型实参被给定或可以被推断,但委托被调用时,可以不用提供类型实参列表(§15.3)。
20.6.7非泛型属性、事件、索引器或运算符 属性、事件、索引器和运算符他们自身可以没有类型实参(尽管他们可以出现在泛型类中,并且可从一个封闭类中使用类型实参)。如果需要一个类似属性泛型的构件,取而代之的是你必须使用一个泛型方法。 20.7约束 泛型类型和方法声明可以可选的指定类型参数约束,这通过在声明中包含类型参数约束语句就可以做到。 type-parameter-constraints-clauses(类型参数约束语句:) type-parameter-constraints-clause(类型参数约束语句) type-parameter-constraints-clauses type-parameter-constraints-clause(类型参数约束语句 类型参数约束语句) type-parameter-constraints-clause:(类型参数约束语句:) where type-parameter : type-parameter-constraints(where 类型参数:类型参数约束) type-parameter-constraints:(类型参数约束:) class-constraint(类约束) interface-constraints(接口约束) constructor-constraint(构造函数约束) class-constraint , interface-constraints(类约束,接口约束) class-constraint , constructor-constraint(类约束,构造函数约束) interface-constraints , constructor-constraint(接口约束,构造函数约束) class-constraint , interface-constraints , constructor-constraint(类约束,接口约束,构造函数约束) class-constraint:(类约束:) class-type(类类型) interface-constraint:(接口约束:) interface-constraint(接口约束) interface-constraints , interface-constraints(接口约束,接口约束) interface-constraints:(接口约束:) interface-type(接口类型) constructor-constraint:(构造函数约束:) new ( ) 每个类型参数约束语句由标志where 紧接着类型参数的名字,紧接着冒号和类型参数的约束列表。对每个类型参数只能有一个where 语句,但where语句可以以任何顺序列出。与属性访问器中的get和set标志相似,where 语句不是关键字。
在where语句中给定的约束列表可以以这个顺序包含下列组件:一个单一的类约束、一个或多个接口约和构造函数约束new ()。 如果约束是一个类类型或者接口类型,这个类型指定类型参数必须支持的每个类型实参的最小“基类型”。无论什么时候使用一个构造类型或者泛型方法,在编译时对于类型实参的约束建会被检查。所提供的类型实参必须派生于或者实现那个类型参数个定的所有约束。 被指定作为类约束的类型必须遵循下面的规则。 该类型必须是一个类类型。 该类型必须是密封的(sealed)。 该类型不能是如下的类型:System.Array,System.Delegate,System.Enum,或者System.ValueType类型。 该类型不能是object。由于所有类型派生于object,如果容许的话这种约束将不会有什么作用。 至多,对于给定类型参数的约束可以是一个类类型。 作为接口约束而被指定的类型必须满足如下的规则。 该类型必须是一个接口类型。 在一个给定的where语句中相同的类型不能被指定多次。 在很多情况下,约束可以包含任何关联类型的类型参数或者方法声明作为构造类型的一部分,并且可以包括被声明的类型,但约束不能是一个单一的类型参数。 被指定作为类型参数约束的任何类或者接口类型,作为泛型类型或者被声明的方法,必须至少是可访问的(§10.5.4)。 如果一个类型参数的where 语句包括new()形式的构造函数约束,则使用new 运算符创建该类型(§20.8.2)的实例是可能的。用于带有一个构造函数约束的类型参数的任何类型实参必须有一个无参的构造函数(详细情形参看§20.7)。 下面是可能约束的例子 interface IPrintable { void Print(); }
interface IComparable<T> { int CompareTo(T value); } interface IKeyProvider<T> { T GetKey(); } class Printer<T> where T:IPrintable{…} class SortedList<T> where T: IComparable<T>{…} class Dictionary<K,V> where K:IComparable<K> where: V: IPrintable,IKeyProvider<K>,new() { … } 下面的例子是一个错误,因为它试图直接使用一个类型参数作为约束。 class Extend<T , U> where U:T{…}//错误 约束的类型参数类型的值可以被用于访问约束暗示的实例成员。在例子 interface IPrintable { void Print(); } class Printer<T> where T:IPrintable { void PrintOne(T x) { x.Pint(); } } IPrintable的方法可以在x上被直接调用,因为T被约束总是实现IPrintable。 20.7.1遵循约束 无论什么时候使用构造类型或者引用泛型方法,所提供的类型实参都将针对声明在泛型类型,或者方法中的类型参数约束作出检查。对于每个where 语句,对应于命名的类型参数的类型实参A将按如下每个约束作出检查。
如果约束是一个类类型或者接口类型,让C表示提供类型实参的约束,该类型实参将替代出现在约束中的任何类型参数。为了遵循约束,类型A必须按如下方式可别转化为类型C: - 同一转换(§6.1.1) - 隐式引用转换(§6.1.4) - 装箱转换(§6.1.5) - 从类型参数A到C(§20.7.4)的隐式转换。 如果约束是new(),类型实参A不能是abstract并,且必须有一个公有的无参的构造函数。如果如下之一是真实的这将可以得到满足。 - A是一个值类型(如§4.1.2中所描述的,所有值类型都有一个公有默认构造函数)。 - A是一个非abstract类,并且 A包含一个无参公有构造函数。 - A是一个非abstract类,并且有一个默认构造函数(§10.10.4)。 如果一个或多个类型参数的约束通过给定的类型实参不能满足,将会出现编译时错误。 因为类型参数不被继承,同样约束也决不被继承。在下面的例子中,D 必须在其类型参数T上指定约束,以满足由基类B<T>所施加的约束。相反,类E不需要指定约束,因为对于任何T,List<T>实现了IEnumerable接口。 class B<T> where T: IEnumerable{…} class D<T>:B<T> where T:IEnumerable{…} class E<T>:B<List<T>>{…} 20.7.2 在类型参数上的成员查找 在由类型参数T给定的类型中,成员查找的结果取决于为T所指定的约束(如果有的话)。如果T没有约束或者只有new ()约束,在T上的成员查找,像在object上的成员查找一样,返回一组相同的成员。否则,成员查找的第一个阶段,将考虑T所约束的每个类型的所有成员,结果将会被合并,然后隐藏成员将会从合并结果中删除。
在泛型出现之前,成员查找总是返回在类中唯一声明的一组成员,或者一组在接口中唯一声明的成员, 也可能是object类型。在类型参数上的成员查找做出了一些改变。当一个类型参数有一个类约束和一个或多个接口约束时,成员查找可以返回一组成员,这些成员有一些是在类中声明的,还有一些是在接口中声明的。下面的附加规则处理了这种情况。 在成员查找过程(§20.9.2)中,在除了object之外的类中声明的成员隐藏了在接口中声明的成员。 在方法和索引器的重载决策过程中,如果任何可用成员在一个不同于object的类中声明,那么在接口中声明的所有成员都将从被考虑的成员集合中删除。 这些规则只有在将一个类约束和接口约束绑定到类型参数上时才有效。通俗的说法是,在一个类约束中定义的成员,对于在接口约束的成员来说总是首选。 20.7.3 类型参数和装箱 当一个结构类型重写继承于System.Object(Equals , GetHashCode或ToString)的虚拟方法,通过结构类型的实例调用虚拟方法将不会导致装箱。即使当结构被用作一个类型参数,并且调用通过类型参数类型的实例而发生,情况也是如此。例如 using System; struct Counter { int value; public override string ToString() { value++; return value.ToString(); } } class Program { static void Test<T>() where T:new() { T x = new T(); Console.WriteLine(x.ToString()); Console.WriteLine(x.ToString()); Console.WriteLine(x.ToString()); } static void Main() { Test<Counter>(); } }
程序的输出如下 1 2 3 尽管推荐不要让ToString带有附加效果(side effect)[2],但这个例子说明了对于三次x.ToString()的调用不会发生装箱。 当在一个约束的类型参数上访问一个成员时,装箱决不会隐式地发生。例如,假定一个接口ICounter包含了一个方法Increment,它可以被用来修改一个值。如果ICounter被用作一个约束,Increment方法的实现将通过Increment在其上调用的变量的引用而被调用,这个变量不是一个装箱拷贝。 using System; interface ICounter { void Increment(); } struct Counter:ICounter { int value; public override string ToString() { return value.ToString(); } void ICounter.Increment(){ value++; } } class Program { static void Test<T>() where T:new ,ICounter{ T x = new T(); Console.WriteLine(x); x.Increment(); //修改x` Console.WriteLine(x); ((ICounter)x).Increment(); //修改x的装箱拷贝 Console.WriteLine(x); } static void Main() { Test<Counter>(); } }
对变量x的首次调用Increment修改了它的值。这与第二次调用Increment是不等价的,第二次修改的是x装箱后的拷贝,因此程序的输出如下 20.7.4包含类型参数的转换 在类型参数T上允许的转换,取决于为T所指定的约束。所有约束的或非约束的类型参数,都可以有如下转换。 从T到T的隐式同一转换。 从T到object 的隐式转换。在运行时,如果T是一个值类型,这将通过一个装箱转换进行。否则,它将作为一个隐式地引用转换。 从object到T的隐式转换。在运行时,如果T是一个值类型,这将通过一个取消装箱操作而进行。否则它将作为一个显式地引用转换。 从T到任何接口类型的显式转换。在运行时,如果T是一个值类型,这将通过一个装箱转换而进行。否则,它将通过一个显式地引用转换而进行。 从任何接口类型到T的隐式转换。在运行时,如果T是一个值类型,这将通过一个取消装箱操作而进行。否则,它将作为一个显式引用转换而进行。 如果类型参数T指定一个接口I作为约束,将存在下面的附加转换。 从T到I的隐式转换,以及从T到I的任何基接口类型的转换。在运行时,如果T是一个值类型,这将作为一个装箱转换而进行。否则,它将作为一个隐式地引用转换而进行。 如果类型参数T指定类型C作为约束,将存在下面的附加转换: 从T到C的隐式引用转换,从T到任何C从中派生的类,以及从T到任何从其实现的接口。 从C到T的显式引用转换,从C从中派生的类[3]到T,以及C实现的任何接口到T
如果存在从C 到A的隐式用户定义转换,从T到 A的隐式用户定义转换。 如果存在从A 到C的显式用户定义转换,从A到 T的显式用户定义转换。 从null类型到T的隐式引用转换 一个带有元素类型的数组类型T具有object和System.Array之间的相互转换(§6.1.4,§6.2.3)。如果T有作为约束而指定的类类型,将有如下附加规则 从带有元素类型T的数组类型AT到带有元素类型U的数组类型AU的隐式引用转换,并且如果下列二者成立的话,将存在从AU到AT显式引用转换: - AT和AU 有相同数量的维数。 - U是这些之一:C,C从中派生的类型,C所实现的接口,作为在T上的约束而指定的接口I,或I的基接口。 先前的规则不允许从非约束类型参数到非接口类型的直接隐式转换,这可能有点奇怪。其原因是为了防止混淆,并且使得这种转换的语义更明确。例如,考虑下面的声明。 class X<T> { public static long F(T t){ return (long)t; // ok,允许转换 } } 如果t到int的直接显式转换是允许的,你可能很容易以为X<int>.F(7)将返回7L。但实际不是,因为标准的数值转换只有在类型在编译时是已知的时候才被考虑。为了使语义更清楚,先前的例子必须按如下形式编写。 class X<T> { public static long F(T t) { return (long)(object)t; //ok;允许转换 } }
--------------------------------------------------------------------------------
[1] 这种情况下“>”被解释为大于运算符。 [2] 在程序中重写ToString时,一般不推荐添加这种类似的计算逻辑,因为它的这种结果变化不易控制,增加了调试程序的复杂性。 [3] C的基类或其基类的基类等。
|