汎用 4 に接続
20.6.5 文法的曖昧さ
§20.9.3 および §20.9.4 では、単純名 (simple-name) とメンバー アクセス (member-access) により、式の文法的曖昧さが容易に生じる可能性があります。たとえば、ステートメント
F(G<A,B>(7));
は、2 つのパラメータ Gc122d950191dccac5fd40197f0afe5d4(7) [1] を持つ F への呼び出しとして解釈できます。同様に、これは 1 つの引数を持つ F の呼び出しとして解釈できます。これは、2 つの型引数と 1 つの仮引数を持つジェネリック メソッド G の呼び出しです。
式が 2 つの異なる有効なメソッドに解析できる場合、「>」が演算子の全体または一部、または型引数リストとして解析できる場合、「>」の直後のマークがチェックされます。次のいずれかの場合:
{ } ] > : ; , . ?
、「>」は型引数リストとして解析されます。それ以外の場合、「>」は演算子として解析されます。
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上指定约束,以满足由基类B8742468051c85b06f0a0af9e3e506b5c所施加的约束。相反,类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的直接显式转换是允许的,你可能很容易以为Xbd43222e33876353aff11e13a7dc75f6.F(7)将返回7L。但实际不是,因为标准的数值转换只有在类型在编译时是已知的时候才被考虑。为了使语义更清楚,先前的例子必须按如下形式编写。
class X<T> { public static long F(T t) { return (long)(object)t; //ok;允许转换 } }
[1] 这种情况下“>”被解释为大于运算符。
[2] 在程序中重写ToString时,一般不推荐添加这种类似的计算逻辑,因为它的这种结果变化不易控制,增加了调试程序的复杂性。
[3] C的基类或其基类的基类等。
以上就是C# 2.0 Specification(泛型五)的内容,更多相关内容请关注PHP中文网(www.php.cn)!