>백엔드 개발 >C#.Net 튜토리얼 >C# 2.0 사양(제네릭 5)

C# 2.0 사양(제네릭 5)

黄舟
黄舟원래의
2017-01-03 10:39:051327검색

일반 4개에 연결

20.6.5 구문 모호성

§20.9.3 및 §20.9.4에서는 simple-name 및 member-access(member-access)가 쉽습니다. 표현에 문법적 모호성을 야기합니다. 예를 들어

F(G<A,B>(7));


문은 두 개의 매개변수 G479fe77c8c9f79972bf2d0b130a87e63(7)[1]을 사용하여 F를 호출하는 것으로 해석될 수 있습니다. 마찬가지로 이는 하나의 인수를 사용하여 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上指定约束,以满足由基类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)!


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.