C#6.0语言规范(七) 表达式
表达式是运算符和操作数的序列。本章定义了操作数和运算符的语法,求值顺序以及表达式的含义。
表达式分类
表达式分类为以下之一:
- 一个值。每个值都有一个关联的类型。
- 一个变量。每个变量都有一个关联的类型,即声明的变量类型。
- 命名空间。具有此分类的表达式只能显示为member_access(成员访问)的左侧。在任何其他上下文中,分类为命名空间的表达式会导致编译时错误。
- 一种。具有此分类的表达式只能显示为member_access(成员访问)的左侧,或者作为运算
as
符(作为运算符),is
运算符(运算符)或typeof
运算符(类型运算符)的操作数。在任何其他上下文中,分类为类型的表达式会导致编译时错误。 - 方法组,它是由成员查找(成员查找)产生的一组重载方法。方法组可以具有关联的实例表达式和关联的类型参数列表。调用实例方法时,评估实例表达式的结果将成为
this
(此访问)表示的实例。允许在invocation_expression(调用表达式),delegate_creation_expression(委托创建表达式)中使用方法组,并将其作为is运算符的左侧,并且可以隐式转换为兼容的委托类型(Method group conversions))。在任何其他上下文中,分类为方法组的表达式会导致编译时错误。 - 空文字。具有此分类的表达式可以隐式转换为引用类型或可空类型。
- 一个匿名函数。具有此分类的表达式可以隐式转换为兼容的委托类型或表达式树类型。
- 物业访问。每个属性访问都有一个关联类型,即属性的类型。此外,属性访问可以具有关联的实例表达式。当调用实例属性访问的访问者(块
get
或set
块)时,评估实例表达式的结果将成为this
(此访问)表示的实例。 - 事件访问。每个事件访问都有一个关联的类型,即事件的类型。此外,事件访问可以具有关联的实例表达式。事件访问可能显示为
+=
和-=
运算符的左手操作数(事件分配)。在任何其他上下文中,分类为事件访问的表达式会导致编译时错误。 - 索引器访问。每个索引器访问都有一个关联的类型,即索引器的元素类型。此外,索引器访问具有关联的实例表达式和关联的参数列表。当调用索引器访问的访问者(块
get
或set
块)时,评估实例表达式的结果变为由this
(此访问)表示的实例,并且评估参数列表的结果变为调用的参数列表。 - 没有。当表达式是一个返回类型为的方法的调用时,会发生这种情况
void
。分类为空的表达式仅在statement_expression(Expression语句)的上下文中有效。
表达式的最终结果永远不是命名空间,类型,方法组或事件访问。相反,如上所述,这些类别的表达式是仅在某些上下文中允许的中间构造。
通过执行get访问器或set访问器的调用,始终将属性访问或索引器访问重新分类为值。特定访问器由属性或索引器访问的上下文确定:如果访问是赋值的目标,则调用set访问器以分配新值(简单赋值)。否则,调用get访问器以获取当前值(表达式的值)。
表达式的值
涉及表达式的大多数构造最终需要表达式来表示值。在这种情况下,如果实际表达式表示命名空间,类型,方法组或什么都没有,则会发生编译时错误。但是,如果表达式表示属性访问,索引器访问或变量,则隐式替换属性,索引器或变量的值:
- 变量的值只是当前存储在变量标识的存储位置中的值。在获得其值之前,必须将变量视为明确赋值(Definite赋值),否则会发生编译时错误。
- 通过调用属性的get访问器获取属性访问表达式的值。如果属性没有get访问器,则会发生编译时错误。否则,执行函数成员调用(动态重载决策的编译时检查),并且调用的结果变为属性访问表达式的值。
- 索引器访问表达式的值是通过调用索引器的get访问器获得的。如果索引器没有get访问器,则会发生编译时错误。否则,使用与索引器访问表达式关联的参数列表执行函数成员调用(动态重载解析的编译时检查),并且调用的结果变为索引器访问表达式的值。
静态和动态绑定
基于组成表达式(参数,操作数,接收器)的类型或值来确定操作含义的过程通常被称为绑定。例如,方法调用的含义是根据接收者和参数的类型确定的。运算符的含义是根据其操作数的类型确定的。
在C#中,操作的含义通常在编译时根据其组成表达式的编译时类型确定。同样,如果表达式包含错误,则编译器会检测并报告错误。这种方法称为静态绑定。
但是,如果表达式是动态表达式(即具有类型dynamic
),则表示它参与的任何绑定应基于其运行时类型(即它在运行时表示的对象的实际类型)而不是它在编译时的类型。因此,这种操作的绑定被推迟到在程序运行期间执行操作的时间。这称为动态绑定。
当动态绑定操作时,编译器很少或不执行检查。相反,如果运行时绑定失败,则会在运行时将错误报告为异常。
C#中的以下操作受绑定:
- 成员访问:
e.M
- 方法调用:
e.M(e1, ..., eN)
- 委托调用:
e(e1, ..., eN)
- 元素访问:
e[e1, ..., eN]
- 对象创建:
new C(e1, ..., eN)
- 重载一元运算符:
+
,-
,!
,~
,++
,--
,true
,false
- 重载二元运算符:
+
,-
,*
,/
,%
,&
,&&
,|
,||
,??
,^
,<<
,>>
,==
,!=
,>
,<
,>=
,<=
- 赋值运算符:
=
,+=
,-=
,*=
,/=
,%=
,&=
,|=
,^=
,<<=
,>>=
- 隐式和显式转换
当不涉及动态表达式时,C#默认为静态绑定,这意味着在选择过程中使用组成表达式的编译时类型。但是,当上面列出的操作中的一个组成表达式是动态表达式时,操作将被动态绑定。
绑定时间
静态绑定在编译时发生,而动态绑定在运行时发生。在以下部分中,术语绑定时间是指编译时或运行时,具体取决于绑定发生的时间。
以下示例说明了静态和动态绑定以及绑定时间的概念:
1 object o = 5; 2 dynamic d = 5; 3 4 Console.WriteLine(5); // static binding to Console.WriteLine(int) 5 Console.WriteLine(o); // static binding to Console.WriteLine(object) 6 Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)
前两个调用是静态绑定的:Console.WriteLine
根据其参数的编译时类型选择重载。因此,绑定时间是编译时。
第三个调用是动态绑定的:Console.WriteLine
根据其参数的运行时类型选择重载。这是因为参数是一个动态表达式 - 它的编译时类型是dynamic
。因此,第三次调用的绑定时间是运行时。
动态绑定
动态绑定的目的是允许C#程序与动态对象交互,即不遵循C#类型系统的常规规则的对象。动态对象可以是来自具有不同类型系统的其他编程语言的对象,或者它们可以是以编程方式设置以针对不同操作实现它们自己的绑定语义的对象。
动态对象实现其自身语义的机制是实现定义的。给定的接口 - 再次实现定义 - 由动态对象实现,以向C#运行时发信号通知它们具有特殊语义。因此,每当动态绑定动态对象上的操作时,它们自己的绑定语义就会接管,而不是本文档中指定的C#。
虽然动态绑定的目的是允许与动态对象进行互操作,但C#允许对所有对象进行动态绑定,无论它们是否是动态的。这允许动态对象的更平滑集成,因为对它们的操作结果本身可能不是动态对象,但在编译时仍然是程序员未知的类型。此外,即使没有涉及的对象是动态对象,动态绑定也可以帮助消除容易出错的基于反射的代码。
以下部分描述了应用动态绑定时语言中的每个构造,应用了什么编译时间检查(如果有),以及编译时结果和表达式分类。
组成表达的类型
当操作静态绑定时,组成表达式的类型(例如,接收者,参数,索引或操作数)始终被认为是该表达式的编译时类型。
当动态绑定操作时,组成表达式的类型以不同的方式确定,具体取决于组成表达式的编译时类型:
- 编译时类型的组成表达式
dynamic
被认为具有表达式在运行时评估的实际值的类型 - 编译时类型是类型参数的组成表达式被认为具有类型参数在运行时绑定的类型
- 否则,组成表达式被认为具有其编译时类型。
运算符
表达式由操作数和运算符构成。表达式的运算符指示要应用于操作数的操作。运算符的例子包括+
,-
,*
,/
,和new
。操作数的示例包括文字,字段,局部变量和表达式。
运算符有三种:
- 一元运算符。一元运算符采用一个操作数并使用前缀表示法(例如
--x
)或后缀表示法(例如x++
)。 - 二元运算符。二元运算符采用两个操作数,并且都使用中缀表示法(例如
x + y
)。 - 三元运算符。只
?:
存在一个三元运算符; 它需要三个操作数并使用中缀符号(c ? x : y
)。
表达式中运算符的求值顺序由运算符的优先级和关联性(运算符优先级和关联性)决定。
表达式中的操作数从左到右进行计算。例如,在F(i) + G(i++) * H(i)
,F
使用旧值调用i
方法,然后使用旧值G
调用方法i
,最后,H
使用新值调用方法i
。这与运算符优先级分开并且与运算符优先级无关。
某些运算符可能会超载。运算符重载允许为一个或两个操作数是用户定义的类或结构类型(运算符重载)的操作指定用户定义的运算符实现。
运算符优先级和关联性
当表达式包含多个运算符时,运算符的优先级控制各个运算符的计算顺序。例如,表达式x + y * z
的计算是x + (y * z)
因为*
运算符的优先级高于二元+
运算符。运算符的优先级由其相关语法生成的定义建立。例如,additive_expression由一个序列的multiplicative_expression相隔小号+
或-
运算符,从而给予+
与-
运算符的优先级低于所述*
,/
和%
运算符。
下表按从高到低的优先顺序汇总了所有运算符:
部分 | 类别 | 运算符 |
---|---|---|
主要表达 | 主 | x.y f(x) a[x] x++ x-- new typeof default checked unchecked delegate |
一元运算符 | 一元 | + - ! ~ ++x --x (T)x |
算术运算符 | 乘 | * / % |
算术运算符 | 添加剂 | + - |
转移运算符 | 转移 | << >> |
关系型和类型测试运算符 | 关系和类型测试 | < > <= >= is as |
关系型和类型测试运算符 | 平等 | == != |
逻辑运算符 | 逻辑和 | & |
逻辑运算符 | 逻辑异或 | ^ |
逻辑运算符 | 逻辑或 | ` |
条件逻辑运算符 | 有条件的AND | && |
条件逻辑运算符 | 条件OR | ` |
空合并运算符 | 无法合并 | ?? |
条件运算符 | 条件 | ?: |
赋值运算符,匿名函数表达式 | 赋值和lambda表达式 | = *= /= %= += -= <<= >>= &= ^= ` |
当操作数出现在具有相同优先级的两个运算符之间时,运算符的关联性控制执行操作的顺序:
- 除了赋值运算符和空合并运算符之外,所有二元运算符都是左关联的,这意味着操作从左到右执行。例如,
x + y + z
被评估为(x + y) + z
。 - 赋值运算符,空合并运算符和条件运算符(
?:
)是右关联的,这意味着操作从右到左执行。例如,x = y = z
被评估为x = (y = z)
。
可以使用括号控制优先级和关联性。例如,x + y * z
第一乘y
通过z
,然后将结果加到x
的,但(x + y) * z
首先将x
和y
然后乘以结果z
。
运算符重载
所有一元和二元运算符都具有在任何表达式中自动可用的预定义实现。除了预定义的实现之外,还可以通过operator
在类和结构(操作符)中包含声明来引入用户定义的实现。用户定义的运算符实现始终优先于预定义的运算符实现:仅当不存在适用的用户定义的运算符实现时,才会考虑预定义的运算符实现,如一元运算符重载决策和二元运算符重载决策中所述。
可重载的一元运算符是:
+ - ! ~ ++ -- true false
只有上面列出的运算符可以重载。特别是,它是不可能的重载成员访问,方法调用,或=
,&&
,||
,??
,?:
,=>
,checked
,unchecked
,new
,typeof
,default
,as
,和is
运算符。
当二元运算符重载时,相应的赋值运算符(如果有)也会被隐式重载。例如,运算符*
的过载也是运算符的过载*=
。这在化合物分配中进一步描述。请注意,赋值运算符本身(=
)不能重载。赋值总是将值的简单逐位副本执行到变量中。
(T)x
通过提供用户定义的转换(用户定义的转换),可以使转换操作过载。
元素访问(例如a[x]
)不被视为可重载的运算符。相反,通过索引器(Indexers)支持用户定义的索引。
在表达式中,使用运算符表示法引用运算符,在声明中,使用功能表示法引用运算符。下表显示了一元和二元运算符的运算符和函数符号之间的关系。在第一个条目中,op表示任何可重载的一元前缀运算符。在第二个条目中,op表示一元后缀++
和--
运算符。在第三个条目中,op表示任何可重载的二元运算符。
操作符号 | 功能表示法 |
---|---|
op x |
operator op(x) |
x op |
operator op(x) |
x op y |
operator op(x,y) |
用户定义的运算符声明始终要求至少有一个参数属于包含运算符声明的类或结构类型。因此,用户定义的运算符不可能具有与预定义运算符相同的签名。
用户定义的运算符声明不能修改运算符的语法,优先级或关联性。例如,/
运算符始终是二元运算符,始终具有在运算符优先级和关联性中指定的优先级,并且始终是左关联的。
虽然用户定义的运算符可以执行任何令人满意的计算,但强烈建议不要使用直观预期产生结果的实现。例如,一个实现operator ==
应该比较两个操作数的相等性并返回一个合适的bool
结果。
通过条件逻辑运算符对主表达式中的各个运算符的描述指定运算符的预定义实现以及适用于每个运算符的任何其他规则。描述使用术语一元运算符重载决策,二元运算符重载决策和数字提升,其定义可在以下部分中找到。
一元运算符重载
形式的操作op x
或x op
,其中op
是可重载一元运算符,并且x
是类型的表达式X
,按如下方式处理:
- 该组由提供的候选用户定义操作的
X
用于操作operator op(x)
使用的规则确定候选用户定义的操作符。 - 如果候选用户定义的运算符集不为空,则这将成为该操作的候选运算符集。否则,预定义的一元
operator op
实现(包括其提升的形式)将成为操作的候选运算符集。给定运算符的预定义实现在运算符的描述中指定(主表达式和一元运算符)。 - 重载决策的重载决策规则应用于候选运算符集,以选择关于参数列表的最佳运算符
(x)
,并且此运算符成为重载解析过程的结果。如果重载决策无法选择单个最佳运算符,则会发生绑定时错误。
二元运算符重载
形式的操作x op y
,其中,op
是可重载二进制运算符,x
是类型的表达式X
,并且y
是类型的表达式Y
,如下进行处理:
- 确定由操作提供的
X
和Y
用于操作的候选用户定义的操作符的集合operator op(x,y)
。该集合由提供的候选运算符X
和由其提供的候选运算符组成Y
,每个运算符使用候选用户定义的运算符的规则确定。如果X
和Y
是相同的类型,或者如果X
与Y
从一个共同的基类型导出,然后共享候选运算符只发生在组合设置一次。 - 如果候选用户定义的运算符集不为空,则这将成为该操作的候选运算符集。否则,预定义的二进制
operator op
实现(包括其提升的形式)将成为操作的候选运算符集。给定运算符的预定义实现在运算符的描述中指定(算术运算符通过条件逻辑运算符)。对于预定义的枚举和委托运算符,所考虑的唯一运算符是由枚举或委托类型定义的运算符,它们是其中一个操作数的绑定时类型。 - 重载决策的重载决策规则应用于候选运算符集,以选择关于参数列表的最佳运算符
(x,y)
,并且此运算符成为重载解析过程的结果。如果重载决策无法选择单个最佳运算符,则会发生绑定时错误。
候选用户定义的运算符
给定一个类型T
和操作operator op(A)
,其中op
是一个可重载的运算符并且A
是一个参数列表,由T
for 提供的候选用户定义运算符集合operator op(A)
如下确定:
- 确定类型
T0
。如果T
是可空类型,T0
则是其基础类型,否则T0
等于T
。 - 对于所有
operator op
在声明T0
和解除所有形式,例如运算符的,如果至少一个操作符适用(适用功能构件相对于所述参数列表)A
,则该候选运算符集在于所有此类适用的运算符T0
。 - 否则,如果
T0
是object
,则候选运算符集为空。 - 否则,提供的候选运算符
T0
集合是由直接基类提供的候选运算符集合T0
,或者T0
if 的有效基类T0
是类型参数。
数字提升
数字提升包括自动执行预定义一元和二元数字运算符的操作数的某些隐式转换。数字提升不是一种独特的机制,而是将重载决策应用于预定义运算符的效果。尽管可以实现用户定义的运算符以显示类似的效果,但数字提升特别不会影响用户定义的运算符的评估。
作为数字提升的示例,请考虑二元*
运算符的预定义实现:
1 int operator *(int x, int y); 2 uint operator *(uint x, uint y); 3 long operator *(long x, long y); 4 ulong operator *(ulong x, ulong y); 5 float operator *(float x, float y); 6 double operator *(double x, double y); 7 decimal operator *(decimal x, decimal y);
当重载决策规则(Overload resolution)应用于这组运算符时,效果是从操作数类型中选择存在隐式转换的第一个运算符。例如,对于操作b * s
,b
a byte
和s
a是a short
,重载决策选择operator *(int,int)
为最佳运算符。因此,效果是b
并且s
被转换为int
,结果的类型是int
。同样,对于操作i * d
,i
a int
和d
a 在哪里double
,重载决策选择operator *(double,double)
为最佳运算符。
一元数字提升
一元数值提升是针对预定义的操作数+
,-
和~
一元运算符。一元数值提升仅包括将类型的操作数的sbyte
,byte
,short
,ushort
,或char
键入int
。此外,对于一元运算-
符,一元数字提升将类型的操作数转换uint
为类型long
。
二进制数字提升
二元数值提升是针对预定义的操作数+
,-
,*
,/
,%
,&
,|
,^
,==
,!=
,>
,<
,>=
,和<=
二元运算符。二进制数字提升隐式地将两个操作数转换为公共类型,在非关系运算符的情况下,它也成为操作的结果类型。二进制数字提升包括按照它们在此处显示的顺序应用以下规则:
- 如果任一操作数是类型
decimal
,则另一个操作数转换为类型decimal
,或者如果另一个操作数是类型float
或,则发生绑定时错误double
。 - 否则,如果任一操作数是类型
double
,则另一个操作数转换为类型double
。 - 否则,如果任一操作数是类型
float
,则另一个操作数转换为类型float
。 - 否则,如果操作数是类型的
ulong
,则另一个操作数转换为类型ulong
,或者如果其他操作数的类型的发生结合时错误sbyte
,short
,int
,或long
。 - 否则,如果任一操作数是类型
long
,则另一个操作数转换为类型long
。 - 否则,如果操作数是类型的
uint
,而另一个操作数的类型的sbyte
,short
或者int
,两个操作数都转换为类型long
。 - 否则,如果任一操作数是类型
uint
,则另一个操作数转换为类型uint
。 - 否则,两个操作数都将转换为类型
int
。
请注意,第一个规则禁止将decimal
类型与double
和float
类型混合的任何操作。该规则遵循以下事实:decimal
类型double
和float
类型之间没有隐式转换。
还要注意,ulong
当另一个操作数是有符号整数类型时,操作数不可能是类型。原因是不存在可以表示整个范围ulong
以及有符号整数类型的整数类型。
在上述两种情况中,可以使用强制转换表达式将一个操作数显式转换为与另一个操作数兼容的类型。
在这个例子中
1 decimal AddPercent(decimal x, double percent) { 2 return x * (1.0 + percent / 100.0); 3 }
发生绑定时错误,因为a decimal
不能乘以a double
。通过显式转换第二个操作数来解决错误decimal
,如下所示:
1 decimal AddPercent(decimal x, double percent) { 2 return x * (decimal)(1.0 + percent / 100.0); 3 }
提升运算符
提升的运算符允许在非可空值类型上运行的预定义和用户定义的运算符也可以与这些类型的可空形式一起使用。提升运算符由满足特定要求的预定义和用户定义的运算符构成,如下所述:
-
对于一元运算符
+ ++ - -- ! ~
如果操作数和结果类型都是非可空值类型,则存在提升形式的运算符。通过向?
操作数和结果类型添加单个修饰符来构造提升形式。如果操作数为null,则提升的运算符将生成空值。否则,提升的运算符解包操作数,应用基础运算符,并包装结果。
- 对于二元运算符
+ - * / % & | ^ << >>
如果操作数和结果类型都是非可空值类型,则存在提升形式的运算符。通过向?
每个操作数和结果类型添加单个修饰符来构造提升形式。如果一个或两个操作数为空,则提升的运算符将生成空值(例外情况是该类型的&
和|
运算符bool?
,如布尔逻辑运算符中所述)。否则,提升的运算符解包操作数,应用基础运算符,并包装结果。
- 对于等于运算符
== !=
如果操作数类型都是非可空值类型且结果类型是,则存在提升形式的运算符bool
。通过向?
每个操作数类型添加单个修饰符来构造提升形式。提升的运算符认为两个空值相等,并且空值不等于任何非空值。如果两个操作数都为非null,则提升的运算符将解包操作数并应用基础运算符来生成bool
结果。
- 对于关系运算符
< > <= >=
如果操作数类型都是非可空值类型且结果类型是,则存在提升形式的运算符bool
。通过向?
每个操作数类型添加单个修饰符来构造提升形式。false
如果一个或两个操作数为空,则提升的运算符将生成该值。否则,提升的运算符会解包操作数并应用基础运算符来生成bool
结果。
成员查询
成员查找是确定类型上下文中名称含义的过程。成员查找可以作为评估表达式中的simple_name(简单名称)或member_access(成员访问)的一部分进行。如果simple_name或member_access发生作为primary_expression一个的invocation_expression(方法调用),该构件被说成是援引。
如果成员是方法或事件,或者它是委托类型(代理)或类型dynamic
(动态类型)的常量,字段或属性,则该成员被称为可调用。
成员查找不仅考虑成员的名称,还考虑成员具有的类型参数的数量以及成员是否可访问。出于成员查找的目的,泛型方法和嵌套泛型类型具有在其各自声明中指示的类型参数的数量,并且所有其他成员具有零类型参数。
在类型中N
具有K
类型参数的名称的成员查找T
按如下方式处理:
- 首先,确定一组名为的可访问成员
N
:- 如果
T
是一种类型的参数,则该组是集命名可访问成员的联合N
中的每个指定为主要约束或仲约束(该类型的类型参数约束)为T
,与所述一组命名的可访问成员一起N
在object
。 - 否则,该组由所有可访问(的成员访问命名)的成员
N
中T
,包括继承的成员,并命名为访问成员N
中object
。如果T
是构造类型,则通过替换构造类型的成员中描述的类型参数来获得成员集。包含override
修饰符的成员将从集合中排除。
- 如果
- 接下来,如果
K
为零,则删除其声明包含类型参数的所有嵌套类型。如果K
不为零,则删除具有不同类型参数数量的所有成员。请注意,当K
为零时,不会删除具有类型参数的方法,因为类型推断过程(类型推断)可能能够推断出类型参数。 - 接下来,如果调用该成员,则从集合中删除所有不可调用的成员。
- 接下来,从集合中删除被其他成员隐藏的成员。对于
S.M
集合中的每个成员,声明S
成员的类型在哪里M
,应用以下规则:- 如果
M
是常量,字段,属性,事件或枚举成员,S
则从集合中删除以基本类型声明的所有成员。 - 如果
M
是一个类型声明,然后在碱的类型声明的所有非类型S
从集合中移除,并且具有相同数目的类型参数所有类型声明作为M
在碱型的声明S
从集合中移除。 - 如果
M
是方法,S
则从集合中删除以基本类型声明的所有非方法成员。
- 如果
- 接下来,从集合中删除由类成员隐藏的接口成员。如果
T
是类型参数并且T
同时具有非有效基类object
和非空有效接口集(类型参数约束),则此步骤仅具有效果。对于S.M
集合中的每个成员,声明S
成员的类型在哪里M
,如果S
是类声明,则应用以下规则object
:- 如果
M
是常量,字段,属性,事件,枚举成员或类型声明,则从集合中删除在接口声明中声明的所有成员。 - 如果
M
是方法,则从集合中删除在接口声明中声明的所有非方法成员,并从集合中删除具有与M
接口声明中声明的相同签名的所有方法。
- 如果
- 最后,删除隐藏的成员后,确定查找的结果:
- 如果集合由不是方法的单个成员组成,则此成员是查找的结果。
- 否则,如果集合仅包含方法,则此组方法是查找的结果。
- 否则,查找不明确,并发生绑定时错误。
对于类型参数和接口以外的类型中的成员查找,以及严格单继承的接口中的成员查找(继承链中的每个接口都具有正好零或一个直接基接口),查找规则的效果就是派生成员隐藏具有相同名称或签名的基本成员。这种单继承查找永远不会模糊。接口成员访问中描述了多继承接口中成员查找可能产生的歧义。
基础类型
出于成员查找的目的,类型T
被认为具有以下基本类型:
- 如果
T
是object
,则T
没有基本类型。 - 如果
T
是一个enum_type,的基本类型T
是类的类型System.Enum
,System.ValueType
以及object
。 - 如果
T
是struct_type,则基类型T
是类类型System.ValueType
和object
。 - 如果
T
是class_type,则基类型T
是基类T
,包括类类型object
。 - 如果
T
是interface_type,则基类型T
是类的基接口T
和类类型object
。 - 如果
T
是array_type,则基类型T
是类类型System.Array
和object
。 - 如果
T
是delegate_type,则基类型T
是类类型System.Delegate
和object
。
函数成员
函数成员是包含可执行语句的成员。函数成员始终是类型的成员,不能是名称空间的成员。C#定义了以下类别的函数成员:
- 方法
- 属性
- 活动
- 索引
- 用户定义的运算符
- 实例构造函数
- 静态构造函数
- 驱逐舰
除了析构函数和静态构造函数(无法显式调用)之外,函数成员中包含的语句通过函数成员调用执行。编写函数成员调用的实际语法取决于特定的函数成员类别。
函数成员调用的参数列表(参数列表)为函数成员的参数提供实际值或变量引用。
泛型方法的调用可以使用类型推断来确定要传递给方法的类型参数集。类型推断中描述了此过程。
方法,索引器,运算符和实例构造函数的调用使用重载决策来确定要调用的候选函数成员集中的哪一个。重载决策中描述了此过程。
一旦在绑定时识别了特定的函数成员,可能通过重载解析,在动态重载解析的编译时检查中描述了调用函数成员的实际运行时过程。
下表总结了涉及可以显式调用的六类函数成员的构造中发生的处理。在表中,e
,x
,y
,和value
表示分类为变量或值的表达式,T
表示被分类为类型的表达式,F
是一种方法的简单名称,并且P
是一个属性的简单名称。
构造 | 例 | 描述 |
---|---|---|
方法调用 | F(x,y) |
应用重载决策以选择F 包含类或结构中的最佳方法。使用参数列表调用该方法(x,y) 。如果方法不是static ,则实例表达式为this 。 |
T.F(x,y) |
应用重载分辨率以选择F 类或结构中的最佳方法T 。如果方法不是,则发生绑定时错误static 。使用参数列表调用该方法(x,y) 。 |
|
e.F(x,y) |
应用重载决策以选择类,结构,或通过的类型给定接口的最佳方法F e 。如果方法是,则发生绑定时错误static 。使用实例表达式e 和参数列表调用该方法(x,y) 。 |
|
物业访问 | P |
调用包含类或结构get 中的属性的访问器P 。如果P 是只写,则发生编译时错误。如果P 不是static ,则实例表达式为this 。 |
P = value |
使用参数列表调用包含类或结构set 中的属性的访问器。如果是只读的,则发生编译时错误。如果不是,则实例表达式为。P (value) P P static this |
|
T.P |
调用类或结构中get 属性的访问器。如果不是或者只写,则发生编译时错误。P T P static P |
|
T.P = value |
使用参数列表调用类或结构中set 属性的访问器。如果不是或者只读,则发生编译时错误。P T (value) P static P |
|
e.P |
使用实例表达式调用由类型给定的类,结构或接口get 中的属性的访问器。如果是或者是只写,则发生绑定时错误。P e e P static P |
|
e.P = value |
使用实例表达式和参数列表调用由类型给定的类,结构或接口set 中的属性的访问器。如果是或如果是只读的,则会发生绑定时错误。P e e (value) P static P |
|
活动访问 | E += value |
调用包含类或结构中add 事件的访问器E 。如果E 不是静态的,则实例表达式为this 。 |
E -= value |
调用包含类或结构中remove 事件的访问器E 。如果E 不是静态的,则实例表达式为this 。 |
|
T.E += value |
调用类或结构中add 事件的访问器。如果不是静态的,则会发生绑定时错误。E T E |
|
T.E -= value |
调用类或结构中remove 事件的访问器。如果不是静态的,则会发生绑定时错误。E T E |
|
e.E += value |
使用实例表达式调用由类型给出的类,结构或接口中add 事件的访问器。如果是静态的,则会发生绑定时错误。E e e E |
|
e.E -= value |
使用实例表达式调用由类型给出的类,结构或接口中remove 事件的访问器。如果是静态的,则会发生绑定时错误。E e e E |
|
索引器访问 | e[x,y] |
应用重载分辨率以选择由e类型给出的类,结构或接口中的最佳索引器。get 使用实例表达式e 和参数列表调用索引器的访问器(x,y) 。如果索引器是只写的,则会发生绑定时错误。 |
e[x,y] = value |
应用重载分辨率来选择类型给出的类,结构或接口中的最佳索引器e 。set 使用实例表达式e 和参数列表调用索引器的访问器(x,y,value) 。如果索引器是只读的,则会发生绑定时错误。 |
|
运算符调用 | -x |
应用重载分辨率来选择类型给出的类或结构中的最佳一元运算符x 。使用参数列表调用所选运算符(x) 。 |
x + y |
应用重载分辨率来选择由类型x 和类型给出的类或结构中的最佳二元运算符y 。使用参数列表调用所选运算符(x,y) 。 |
|
实例构造函数调用 | new T(x,y) |
应用重载决策以选择类或结构中的最佳实例构造函数T 。使用参数列表调用实例构造函数(x,y) 。 |
参数列表
每个函数成员和委托调用都包含一个参数列表,该列表为函数成员的参数提供实际值或变量引用。指定函数成员调用的参数列表的语法取决于函数成员类别:
- 对于实例构造函数,方法,索引器和委托,参数被指定为argument_list,如下所述。对于索引器,在调用
set
访问器时,参数列表还包括指定为赋值运算符的右操作数的表达式。 - 对于属性,参数列表在调用
get
访问器时为空,并且包含在调用访问器时指定为赋值运算符的右操作数的表达式set
。 - 对于事件,参数列表由指定为
+=
或-=
运算符的右操作数的表达式组成。 - 对于用户定义的运算符,参数列表由一元运算符的单个操作数或二元运算符的两个操作数组成。
属性(属性),事件(事件)和用户定义的运算符(运算符)的参数始终作为值参数(值参数)传递。索引器(索引器)的参数始终作为值参数(值参数)或参数数组(参数数组)传递。这些类别的功能成员不支持参考和输出参数。
实例构造函数,方法,索引器或委托调用的参数被指定为argument_list:
1 argument_list 2 : argument (',' argument)* 3 ; 4 5 argument 6 : argument_name? argument_value 7 ; 8 9 argument_name 10 : identifier ':' 11 ; 12 13 argument_value 14 : expression 15 | 'ref' variable_reference 16 | 'out' variable_reference 17 ;
一个argument_list中包含的一个或多个参数 S,用逗号分开。每个参数都包含一个可选的argument_name,后跟一个argument_value。一个参数与argument_name被称为一个命名参数,而一个参数没有argument_name是位置参数。它是在一个命名参数后,出现一个位置参数错误argument_list中。
该argument_value可以采取以下形式之一:
- 一个表达式,指示参数作为值参数传递(值参数)。
- 关键字
ref
后跟一个variable_reference(变量引用),表示该参数作为引用参数传递(引用参数)。必须明确赋值变量(定义赋值)才能将其作为引用参数传递。关键字out
后跟一个variable_reference(变量引用),表示该参数作为输出参数传递(输出参数)。在函数成员调用之后,变量被认为是明确赋值的(定义赋值),其中变量作为输出参数传递。
相应参数
对于参数列表中的每个参数,必须在函数成员或被调用的委托中有相应的参数。
以下使用的参数列表确定如下:
- 对于在类中定义的虚方法和索引器,参数列表是从函数成员的最具体的声明或覆盖中选取的,从接收器的静态类型开始,并搜索其基类。
- 对于接口方法和索引器,从成员的最具体定义中选择参数列表,从接口类型开始并搜索基接口。如果未找到唯一参数列表,则构造具有不可访问名称且没有可选参数的参数列表,以便调用不能使用命名参数或省略可选参数。
- 对于部分方法,使用定义的部分方法声明的参数列表。
- 对于所有其他函数成员和委托,只有一个参数列表,即使用的参数列表。
参数或参数的位置定义为参数列表或参数列表中其前面的参数或参数的数量。
函数成员参数的相应参数建立如下:
- 实例构造函数,方法,索引器和委托的argument_list中的参数:
- 固定参数出现在参数列表中相同位置的位置参数对应于该参数。
- 具有以其正常形式调用的参数数组的函数成员的位置参数对应于参数数组,该参数数组必须出现在参数列表中的相同位置。
- 具有以其展开形式调用的参数数组的函数成员的位置参数,其中在参数列表中的相同位置处不发生固定参数,对应于参数数组中的元素。
- 命名参数对应于参数列表中同名参数。
- 对于索引器,在调用
set
访问器时,指定为赋值运算符的右操作数的表达式对应于访问器声明的隐式value
参数set
。
- 对于属性,在调用
get
访问器时没有参数。调用set
访问器时,指定为赋值运算符的右操作数的表达式对应于访问器声明的隐式value
参数set
。 - 对于用户定义的一元运算符(包括转换),单个操作数对应于运算符声明的单个参数。
- 对于用户定义的二元运算符,左操作数对应于第一个参数,右操作数对应于运算符声明的第二个参数。
参数列表运行时处理
在函数成员调用的运行时处理(动态重载决策的编译时检查)期间,参数列表的表达式或变量引用按从左到右的顺序进行评估,如下所示:
- 对于value参数,将计算参数表达式,并执行对相应参数类型的隐式转换(隐式转换)。结果值将成为函数成员调用中value参数的初始值。
- 对于引用或输出参数,将评估变量引用,并且生成的存储位置将成为函数成员调用中参数表示的存储位置。如果给定的作为基准或输出参数变量引用是一个数组元素reference_type,执行运行时检查,以确保数组的元素类型是相同的参数的类型。如果此检查失败,System.ArrayTypeMismatchException则抛出a。
方法,索引器和实例构造函数可以将其最右侧的参数声明为参数数组(参数数组)。这些函数成员可以以其正常形式或以其扩展形式调用,具体取决于哪个适用(适用的函数成员):
- 当以正常形式调用具有参数数组的函数成员时,为参数数组指定的参数必须是可以隐式转换(隐式转换)到参数数组类型的单个表达式。在这种情况下,参数数组的作用与值参数完全相同。
- 当以扩展形式调用具有参数数组的函数成员时,调用必须为参数数组指定零个或多个位置参数,其中每个参数是可隐式转换的表达式(隐式转换)到参数的元素类型阵列。在这种情况下,调用创建参数数组类型的实例,其长度对应于参数的数量,使用给定的参数值初始化数组实例的元素,并使用新创建的数组实例作为实际参数。
参数列表的表达式始终按其编写顺序进行计算。因此,这个例子
1 class Test 2 { 3 static void F(int x, int y = -1, int z = -2) { 4 System.Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z); 5 } 6 7 static void Main() { 8 int i = 0; 9 F(i++, i++, i++); 10 F(z: i++, x: i++); 11 } 12 }
产生输出
1 x = 0, y = 1, z = 2 2 x = 4, y = -1, z = 3
阵列协方差规则(阵列协方差)准许的阵列类型的值A[]
是一个数组类型的实例的引用B[]
,提供了一种隐式引用转换从存在B
到A
。由于这些规则,当reference_type的数组元素作为引用或输出参数传递时,需要运行时检查以确保数组的实际元素类型与参数的实际元素类型相同。在这个例子中
1 class Test 2 { 3 static void F(ref object x) {...} 4 5 static void Main() { 6 object[] a = new object[10]; 7 object[] b = new string[10]; 8 F(ref a[0]); // Ok 9 F(ref b[1]); // ArrayTypeMismatchException 10 } 11 }
第二次调用F
导致a System.ArrayTypeMismatchException
被抛出,因为实际的元素类型b
是string
和不是object
。
当以扩展形式调用具有参数数组的函数成员时,处理调用就像在扩展参数周围插入带有数组初始值设定项(数组创建表达式)的数组创建表达式一样。例如,给出声明
void F(int x, int y, params object[] args);
以下对该方法的扩展形式的调用
1 F(10, 20); 2 F(10, 20, 30, 40); 3 F(10, 20, 1, "hello", 3.0);
完全对应
1 F(10, 20, new object[] {}); 2 F(10, 20, new object[] {30, 40}); 3 F(10, 20, new object[] {1, "hello", 3.0});
特别要注意的是,当为参数数组提供零参数时,会创建一个空数组。
当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。因为它们总是不变的,所以它们的评估不会影响其余参数的评估顺序。
类型推断
在不指定类型参数的情况下调用泛型方法时,类型推断过程会尝试推断调用的类型参数。类型推断的存在允许使用更方便的语法来调用泛型方法,并允许程序员避免指定冗余类型信息。例如,给定方法声明:
1 class Chooser 2 { 3 static Random rand = new Random(); 4 5 public static T Choose<T>(T first, T second) { 6 return (rand.Next(2) == 0)? first: second; 7 } 8 }
可以在Choose
不显式指定类型参数的情况下调用该方法:
1 int i = Chooser.Choose(5, 213); // Calls Choose<int> 2 3 string s = Chooser.Choose("foo", "bar"); // Calls Choose<string>
通过类型推断,类型参数int
和string
从方法的参数确定。
类型推断作为方法调用的绑定时处理(Method invocations)的一部分发生并且在调用的重载解析步骤之前发生。如果在方法调用中指定了特定方法组,并且未将任何类型参数指定为方法调用的一部分,则会将类型推断应用于方法组中的每个泛型方法。如果类型推断成功,则推断的类型参数用于确定后续重载解析的参数类型。如果重载解析选择泛型方法作为要调用的方法,则推断的类型参数将用作调用的实际类型参数。如果特定方法的类型推断失败,则该方法不参与重载解析。类型推断的失败本身并不会导致绑定时错误。然而,
如果提供的参数数量与方法中的参数数量不同,则推断会立即失败。否则,假设泛型方法具有以下签名:
Tr M<X1,...,Xn>(T1 x1, ..., Tm xm)
使用表单M(E1...Em)
的方法调用,类型推断的任务是S1...Sn
为每个类型参数查找唯一的类型参数X1...Xn
,以使调用M<S1...Sn>(E1...Em)
变为有效。
在推理过程中,每个类型参数Xi
要么固定为特定类型,Si
要么与一组关联的边界不固定。每个边界都是某种类型。最初,每个类型变量都不带有一组空边界。T
Xi
类型推断分阶段进行。每个阶段将尝试根据前一阶段的结果推断更多类型变量的类型参数。第一阶段对边界进行一些初步推断,而第二阶段将类型变量固定为特定类型并推断出更多边界。第二阶段可能必须重复多次。
注意:类型推断不仅在调用泛型方法时发生。方法组转换的类型推断在类型推断中描述,用于转换方法组,并且在查找一组表达式的最佳公共类型中描述了查找一组表达式的最佳公共类型。
第一阶段
对于每个方法参数Ei
:
- 如果
Ei
是匿名函数,则从中生成显式参数类型推断(显式参数类型推断)Ei
Ti
- 否则,如果
Ei
有一个类型U
和xi
是一个值参数然后一个下界推断是由从U
到Ti
。 - 否则,如果
Ei
有一个类型U
和xi
是一个ref
或out
参数那么精确推断是由从U
到Ti
。 - 否则,不会对此参数进行推断。
第二阶段
第二阶段进行如下:
- 所有不依赖于(依赖性)的未固定类型变量都是固定的(修复)。
Xi
Xj
- 如果不存在这样的类型变量,则修复所有未固定的类型变量,以下所有变量
Xi
都保持:- 至少有一个
Xj
依赖的类型变量Xi
Xi
有一组非空的边界
- 至少有一个
- 如果不存在此类型变量且仍存在未固定的类型变量,则类型推断将失败。
- 否则,如果不存在其他未固定的类型变量,则类型推断成功。
- 否则,对于
Ei
具有相应参数类型的所有参数,Ti
其中输出类型(输出类型)包含不固定的类型变量Xj
但输入类型(输入类型)不包含,输出类型推断(输出类型推断)由Ei
to生成Ti
。然后重复第二阶段。
输入类型
如果E
是方法组或隐式类型的匿名功能和T
是委托类型或表达式树然后键入所有参数类型T
是输入类型的E
类型 T
。
输出类型
如果E
是方法组或匿名函数,并且T
是委托类型或表达式树类型,则返回类型T
是具有类型 的输出E
类型 T
。
依赖
的未定影的类型变量Xi
直接依赖于未定影类型的变量Xj
,如果由于某种参数Ek
类型为Tk
Xj
在发生输入型的Ek
同型Tk
和Xi
发生在输出型的Ek
带类型Tk
。
Xj
取决于 Xi
是否Xj
直接取决于 Xi
或是否Xi
直接 取决于Xk
并Xk
取决于 Xj
。因此,“取决于”是“直接依赖于”的传递而非反身的封闭。
输出类型推断
一个输出类型推断是由从一个表达式E
到一个类型T
以下列方式:
- 如果
E
是具有推断返回类型U
(推断返回类型)的匿名函数, 并且T
是具有返回类型的委托类型或表达式树类型Tb
,则从中生成下限推理(下限推理)。U
Tb
- 否则,如果
E
是的方法组和T
是委托类型或表达式树类型与参数类型T1...Tk
和返回类型Tb
,以及重载解析E
与所述类型的T1...Tk
产生具有返回类型的单一的方法U
,则下限推断是由从U
到Tb
。 - 否则,如果
E
是与类型的表达式U
,则下限推断是由从U
到T
。 - 否则,不做任何推论。
显式参数类型推断
一个显式参数类型推断是由从一个表达式E
到一个类型T
以下列方式:
- 如果
E
是与参数类型的显式类型匿名功能U1...Uk
和T
是委托类型或表达式树类型与参数类型V1...Vk
则对于每个Ui
的精确推断(精确的推断)由从Ui
到相应Vi
。
精确推断
从类型到类型的精确推断 如下:U
V
-
如果
V
是其中一个未固定的Xi
则U
添加到精确边界的集合中Xi
。 -
否则,设置
V1...Vk
并U1...Uk
通过检查是否适用以下任何一种情况来确定:V
是一种数组类型V1[...]
,U
是一个U1[...]
相同排名的数组类型V
是类型V1?
,U
是类型U1?
V
是一种构造类型C<V1...Vk>
,U
是一种构造类型C<U1...Uk>
如果有这些情况再申请一个精确推断是由从每个
Ui
到对应Vi
。 -
否则不做推论。
下限推断
下限推理 从一个类型U
到一个类型V
被制备如下:
- 如果
V
是其中一个未固定的Xi
则U
添加到下限的集合中Xi
。 - 否则,如果
V
是的类型V1?
和U
是类型U1?
然后下限推理是从制成U1
至V1
。 -
否则,设置
U1...Uk
并V1...Vk
通过检查是否适用以下任何一种情况来确定:V
是一个数组类型V1[...]
,U
是一个相同排名的数组类型U1[...]
(或有效基类型的类型参数U1[...]
)V
是一个IEnumerable<V1>
,ICollection<V1>
或IList<V1>
和U
是一维阵列型U1[]
(或者类型参数,其有效碱型是U1[]
)-
V
是一个构造的类,结构,接口或委托类型,C<V1...Vk>
并且有一个唯一的类型C<U1...Uk>
,使得U
(或者,如果U
是类型参数,它的有效基类或其有效接口集的任何成员)是相同的,继承自(直接或间接地)或实施(直接或间接)C<U1...Uk>
。(“唯一性”限制意味着在案例界面中
C<T> {} class U: C<X>, C<Y> {}
,在推断时不会进行推断U
,C<T>
因为U1
可能是X
或Y
。)
如果任何的这些情况下应用然后推断是由从各
Ui
到相应的Vi
如下:- 如果
Ui
没有已知的引用类型那么精确推断是由 - 否则,如果
U
是一个数组类型则下限推断是由 - 否则,如果
V
是,C<V1...Vk>
那么推断取决于第i个类型参数C
:- 如果它是协变的,则进行下限推断。
- 如果它是逆变的,则进行上限推断。
- 如果它是不变的,则进行精确推断。
- 否则,不做任何推论。
上限推断
从类型到类型的上限推断 如下:U
V
- 如果
V
是其中一个未固定Xi
则将U
其添加到上限集合中Xi
。 -
否则,设置
V1...Vk
并U1...Uk
通过检查是否适用以下任何一种情况来确定:U
是一种数组类型U1[...]
,V
是一个V1[...]
相同排名的数组类型U
是一个IEnumerable<Ue>
,ICollection<Ue>
或IList<Ue>
和V
是一维阵列型Ve[]
U
是类型U1?
,V
是类型V1?
-
U
构造类,结构,接口或委托类型C<U1...Uk>
,V
是类,结构,接口或委托类型,与(直接或间接)继承或直接或间接实现(直接或间接)唯一类型相同C<V1...Vk>
(在“唯一性”的限制意味着,如果我们有
interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{}
,则没有推理是从推断时作C<U1>
到V<Q>
。推论不作出U1
要么X<Q>
或Y<Q>
)。
如果任何的这些情况下应用然后推断是由从各
Ui
到相应的Vi
如下:- 如果
Ui
没有已知的引用类型那么精确推断是由 - 否则,如果
V
是一个数组类型那么上限推断是由 - 否则,如果
U
是,C<U1...Uk>
那么推断取决于第i个类型参数C
:- 如果它是协变的,则进行上限推断。
- 如果它是逆变的,则进行下限推断。
- 如果它是不变的,则进行精确推断。
- 否则,不做任何推论。
修复
带有一组边界的未固定类型变量修复如下:Xi
- 候选类型 集
Uj
开始于边界集中所有类型的集合Xi
。 - 然后,我们依次检查每个边界
Xi
:对于从候选集中移除U
的Xi
所有类型Uj
的不完全相同的每个精确边界U
。对于每一个下界U
的Xi
所有类型Uj
到其中存在不从隐式转换U
从候选集合中去除。对于每一个上界U
的Xi
所有类型Uj
从其中存在不隐式转换到U
被从候选集合中去除。 - 如果在剩余的候选类型中
Uj
存在一种唯一类型V
,其中存在对所有其他候选类型的隐式转换,则Xi
固定为V
。 - 否则,类型推断失败。
推断返回类型
F
在类型推断和重载解析期间使用匿名函数的推断返回类型。只能为已知所有参数类型的匿名函数确定推断的返回类型,因为它们是显式给定的,通过匿名函数转换提供的,或者在封闭泛型方法调用的类型推断期间推断。
该推断结果类型确定如下:
- 如果body
F
是具有类型的表达式,则推断的结果类型F
是该表达式的类型。 - 如果body
F
是一个块,并且块的return
语句中的表达式集具有最佳公共类型T
(查找一组表达式的最佳公共类型),则推断结果类型F
为T
。 - 否则,无法推断结果类型
F
。
所述推断返回类型确定如下:
- 如果
F
是异步并且正文F
是一个被归类为空的表达式(表达式分类),或者一个语句块,其中没有返回语句具有表达式,则推断的返回类型是System.Threading.Tasks.Task
- 如果
F
是异步并且具有推断的结果类型T
,则推断的返回类型为System.Threading.Tasks.Task<T>
。 - 如果
F
是非异步并且具有推断的结果类型T
,则推断的返回类型为T
。 - 否则无法推断返回类型
F
。
作为涉及匿名函数的类型推断的示例,请考虑Select
在System.Linq.Enumerable
类中声明的扩展方法:
1 namespace System.Linq 2 { 3 public static class Enumerable 4 { 5 public static IEnumerable<TResult> Select<TSource,TResult>( 6 this IEnumerable<TSource> source, 7 Func<TSource,TResult> selector) 8 { 9 foreach (TSource element in source) yield return selector(element); 10 } 11 } 12 }
假设System.Linq
命名空间是使用using
子句导入的,并且给定了一个Customer
具有Name
type属性的类string
,该Select
方法可用于选择客户列表的名称:
1 List<Customer> customers = GetCustomerList(); 2 IEnumerable<string> names = customers.Select(c => c.Name);
通过将调用重写为静态方法调用来处理扩展方法调用(Extension方法调用)Select
:
IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);
由于未明确指定类型参数,因此使用类型推断来推断类型参数。首先,customers
参数与参数有关source
,推断T
为Customer
。然后,使用上述匿名函数类型推断过程c
给出类型Customer
,并且表达式c.Name
与selector
参数的返回类型相关,推断S
为string
。因此,调用等同于
Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)
结果是类型IEnumerable<string>
。
以下示例演示匿名函数类型推断如何允许类型信息在泛型方法调用中的参数之间“流动”。鉴于方法:
1 static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2) { 2 return f2(f1(value)); 3 }
调用的类型推断:
double seconds = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalSeconds);
进行如下:首先,参数"1:15:30"
与参数有关value
,推断X
为string
。然后,第一个匿名函数的参数s
被赋予推断类型string
,并且表达式TimeSpan.Parse(s)
与返回类型相关f1
,推断Y
为System.TimeSpan
。最后,第二个匿名函数的参数t
被赋予推断类型System.TimeSpan
,并且表达式t.TotalSeconds
与返回类型相关f2
,推断Z
为double
。因此,调用的结果是类型double
。
用于转换方法组的类型推断
与泛型方法的调用类似,当M
包含泛型方法的方法组转换为给定的委托类型D
(方法组转换)时,也必须应用类型推断。给定一种方法
Tr M<X1...Xn>(T1 x1 ... Tm xm)
并且方法组M
被分配给委托类型D
,类型推断的任务是找到类型参数,S1...Sn
以便表达式:
M<S1...Sn>
变得兼容(委托声明)D
。
与泛型方法调用的类型推断算法不同,在这种情况下,只有参数类型,没有参数表达式。特别是,没有匿名函数,因此不需要多个推理阶段。
相反,所有Xi
被认为是不固定,和一个下限推断是由从每个参数类型Uj
的D
以相应的参数类型Tj
的M
。如果找到任何Xi
无边界,则类型推断失败。否则,所有Xi
都固定为对应的Si
,这是类型推断的结果。
查找一组表达式的最佳常见类型
在某些情况下,需要为一组表达式推断出通用类型。特别是,以这种方式可以找到隐式类型数组的元素类型和具有块体的匿名函数的返回类型。
直观地,给定一组表达式,E1...Em
这种推断应该等同于调用方法
Tr M<X>(X x1 ... X xm)
与Ei
as参数。
更准确地说,推理从一个未固定的类型变量开始X
。输出类型推论然后由从每个Ei
到 X
。最后,X
是固定的,如果成功,则结果类型S
是表达所产生的最常见的类型。如果不S
存在,则表达式没有最佳公共类型。
重载决策
重载决策是一种绑定时机制,用于在给定参数列表和一组候选函数成员的情况下选择要调用的最佳函数成员。重载决策选择要在C#中的以下不同上下文中调用的函数成员:
- 调用invocation_expression(Method invocations)中命名的方法。
- 调用在object_creation_expression(对象创建表达式)中命名的实例构造函数。
- 通过element_access(元素访问)调用索引器访问器。
- 调用表达式中引用的预定义或用户定义的运算符(一元运算符重载决策和二元运算符重载决策)。
这些上下文中的每一个都以其自己独特的方式定义候选函数成员集和参数列表,如上面列出的部分中详细描述的。例如,方法调用的候选集不包括标记的方法override
(成员查找),如果派生类中的任何方法适用(方法调用),则基类中的方法不是候选方法。
一旦确定了候选函数成员和参数列表,最佳函数成员的选择在所有情况下都是相同的:
- 给定一组适用的候选函数成员,找到该集合中的最佳函数成员。如果集合只包含一个函数成员,那么该函数成员是最好的函数成员。否则,最好的函数成员是一个函数成员,它比给定参数列表中的所有其他函数成员更好,只要使用Better函数成员中的规则将每个函数成员与所有其他函数成员进行比较。如果没有一个函数成员优于所有其他函数成员,则函数成员调用不明确并且发生绑定时错误。
以下部分定义了适用的函数成员和更好的函数成员的术语的确切含义。
适用函数成员
当满足以下所有条件时,函数成员被称为关于参数列表的适用函数成员A
:
- 每个参数
A
对应于函数成员声明中的参数,如对应参数中所述,任何无参数对应的参数都是可选参数。 - 对于每个参数
A
,参数的参数传递模式(即valueref
,或out
)与相应参数的参数传递模式相同,并且- 对于值参数或参数数组,从参数到相应参数的类型存在隐式转换(隐式转换),或者
- 对于a
ref
或out
参数,参数的类型与相应参数的类型相同。毕竟,ref
或out
参数是传递的参数的别名。
对于包含参数数组的函数成员,如果函数成员适用于上述规则,则称其适用于其正常形式。如果包含参数数组的函数成员不适用于其正常形式,则函数成员可能适用于其扩展形式:
- 通过使用参数数组的元素类型的零个或多个值参数替换函数成员声明中的参数数组来构造扩展形式,使得参数列表
A
中的参数数量与参数的总数相匹配。如果A
参数少于函数成员声明中固定参数的数量,则无法构造函数成员的展开形式,因此不适用。 - 否则,如果参数
A
的参数传递模式中的每个参数与相应参数的参数传递模式相同,则扩展形式适用,并且- 对于固定值参数或由扩展创建的值参数,存在从参数类型到相应参数类型的隐式转换(隐式转换),或者
- 对于a
ref
或out
参数,参数的类型与相应参数的类型相同。
确定更好的函数成员
为了确定更好的函数成员,构造一个精简的参数列表A,它按照它们在原始参数列表中出现的顺序仅包含参数表达式本身。
每个候选函数成员的参数列表按以下方式构造:
- 如果函数成员仅适用于扩展形式,则使用扩展形式。
- 从参数列表中删除没有相应参数的可选参数
- 重新排序参数,使它们出现在与参数列表中相应参数相同的位置。
给定参数列表A
与一组参数表达式的{E1, E2, ..., En}
和两个适用函数成员Mp
和Mq
与参数类型{P1, P2, ..., Pn}
和{Q1, Q2, ..., Qn}
,Mp
被定义为一个功能更强大构件比Mq
如果
- 为每个参数,从隐式转换
Ex
到Qx
距离比隐式转换不是更好Ex
到Px
,并 - 对于至少一个参数,转换为
Ex
toPx
比转换为Ex
to 更好Qx
。
执行此评估时,如果Mp
或Mq
适用于其扩展形式,则参考Px
或Qx
参考参数列表的扩展形式中的参数。
在情况下,参数型序列{P1, P2, ..., Pn}
和{Q1, Q2, ..., Qn}
等同(即,每个Pi
具有一个标识转换成相应的Qi
),以下平局决胜规则应用中,为了以确定更好的功能部件。
- 如果
Mp
是非泛型方法并且Mq
是通用方法,则Mp
优于Mq
。 - 否则,如果
Mp
适用于其正常形式并且Mq
具有params
阵列并且仅适用于其扩展形式,则Mp
优于Mq
。 - 否则,如果
Mp
声明参数多于Mq
,则Mp
优于Mq
。如果两个方法都有params
数组并且仅适用于它们的扩展形式,则会发生这种情况。 - 否则,如果所有参数
Mp
都具有相应的参数,而默认参数需要替换至少一个可选参数,Mq
则Mp
优于Mq
。 - 否则,如果
Mp
具有比特定参数类型更多的特定参数类型Mq
,则Mp
优于Mq
。让我们{R1, R2, ..., Rn}
和{S1, S2, ..., Sn}
代表非实例化和未展开的参数类型Mp
和Mq
。如果对于每个参数,Mp
参数类型的特定性不是特定的,并且对于至少一个参数,参数类型比以下更具体:Mq
Rx
Sx
Rx
Sx
- 类型参数的特定性不如非类型参数。
- 递归地,如果至少一个类型参数更具体,并且没有类型参数比另一个中的相应类型参数更不具体,则构造类型比另一个构造类型(具有相同数量的类型参数)更具体。
- 如果第一个元素类型的元素类型比第二个元素类型更具体,则数组类型比另一个数组类型(具有相同数量的维度)更具体。
- 否则,如果一个成员是非提升的运算符而另一个是提升的运算符,则非提升的运算符更好。
- 否则,功能成员都不会更好。
更好地从表达转换
给定的隐式转换C1
,从一个表达式转换E
到类型T1
,以及隐式转换C2
,从一个表达式转换E
到类型T2
,C1
是一个更好的转换比C2
,如果E
不完全匹配T2
和以下中的至少一个成立:
E
完全匹配T1
(完全匹配表达式)T1
是一个比T2
(更好的转换目标)更好的转换目标
完全匹配表达式
给定表达式E
和类型T
,如果满足下列条件之一,则E
完全匹配T
:
E
具有类型S
和标识转换从存在S
于T
E
是一个匿名函数,T
是委托类型D
或表达式树类型,Expression<D>
并且具有以下之一:- 推断的返回类型
X
存在E
在的参数列表的上下文D
(推断返回类型),以及标识转换从存在X
到的返回类型D
- 要么
E
是非异步并且D
具有返回类型,Y
要么E
是异步并且D
具有返回类型Task<Y>
,并且具有以下之一:- 身体
E
是一个完全匹配的表达Y
- body
E
是一个语句块,其中每个return语句返回一个完全匹配的表达式Y
- 身体
- 推断的返回类型
更好的转换目标
由于两种不同类型的T1
和T2
,T1
是一种较好的转换目标相比T2
,如果从没有隐式转换T2
到T1
存在,下面的至少一人持有:
- 隐式转换
T1
为T2
存在 T1
是委托类型D1
或表达式树类型Expression<D1>
,T2
是委托类型D2
还是表达式树类型Expression<D2>
,D1
具有返回类型S1
和以下之一:D2
没有回来D2
有一个返回类型S2
,S1
是一个更好的转换目标S2
T1
是Task<S1>
,T2
是Task<S2>
,S1
是一个更好的转换目标S2
T1
是S1
或S1?
其中S1
是有符号整数类型,并且T2
是S2
或S2?
其中S2
是一个无符号整数类型。特别:S1
是sbyte
和S2
是byte
,ushort
,uint
,或ulong
S1
是short
和S2
是ushort
,uint
,或ulong
S1
是int
和S2
是uint
,或ulong
S1
是long
和S2
是ulong
在泛型类中重载
虽然声明的签名必须是唯一的,但是类型参数的替换可能会产生相同的签名。在这种情况下,上述超载解决方案的打破平局规则将选择最具体的成员。
以下示例显示根据此规则有效且无效的重载:
1 interface I1<T> {...} 2 3 interface I2<T> {...} 4 5 class G1<U> 6 { 7 int F1(U u); // Overload resolution for G<int>.F1 8 int F1(int i); // will pick non-generic 9 10 void F2(I1<U> a); // Valid overload 11 void F2(I2<U> a); 12 } 13 14 class G2<U,V> 15 { 16 void F3(U u, V v); // Valid, but overload resolution for 17 void F3(V v, U u); // G2<int,int>.F3 will fail 18 19 void F4(U u, I1<V> v); // Valid, but overload resolution for 20 void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail 21 22 void F5(U u1, I1<V> v2); // Valid overload 23 void F5(V v1, U u2); 24 25 void F6(ref U u); // valid overload 26 void F6(out V v); 27 }
动态重载决策的编译时检查
对于大多数动态绑定操作,在编译时未知可能的解析候选集。但在某些情况下,候选集在编译时是已知的:
- 使用动态参数调用静态方法
- 实例方法调用接收器不是动态表达式
- 索引器调用接收器不是动态表达式的位置
- 构造函数使用动态参数调用
在这些情况下,会对每个候选项执行有限的编译时检查,以查看它们中是否有任何一个可能在运行时应用。此检查包括以下步骤:
- 部分类型推断:
dynamic
使用类型推断规则推断不直接或间接依赖于类型参数的任何类型参数。其余的类型参数未知。 - 部分适用性检查:根据适用的功能成员检查适用性,但忽略类型未知的参数。
- 如果没有候选者通过此测试,则发生编译时错误。
函数成员调用
本节描述在运行时调用特定函数成员的过程。假设绑定时进程已经确定了要调用的特定成员,可能是通过将重载解析应用于一组候选函数成员。
为了描述调用过程,函数成员分为两类:
- 静态功能成员。这些是实例构造函数,静态方法,静态属性访问器和用户定义的运算符。静态函数成员始终是非虚拟的。
- 实例功能成员。这些是实例方法,实例属性访问器和索引器访问器。实例函数成员是非虚拟或虚拟的,并且始终在特定实例上调用。该实例由实例表达式计算,并且在函数成员中可以作为
this
(此访问)访问。
函数成员调用的运行时处理包括以下步骤,其中M
是函数成员,如果M
是实例成员,E
则是实例表达式:
-
如果
M
是静态函数成员:- 参数列表按参数列表中的描述进行评估。
M
被调用。
-
如果
M
是在value_type中声明的实例函数成员:E
被评估。如果此评估导致异常,则不执行进一步的步骤。- 如果
E
未归类为变量,则E
创建“s”类型的临时局部变量,并将值E
赋值给该变量。E
然后重新分类为对该临时局部变量的引用。临时变量可以在this
内部访问M
,但不能以任何其他方式访问。因此,只有当E
是一个真正的变量是有可能呼叫者观察到的变化M
使得对this
。 - 参数列表按参数列表中的描述进行评估。
M
被调用。引用E
的变量成为引用的变量this
。
-
如果
M
是在reference_type中声明的实例函数成员:E
被评估。如果此评估导致异常,则不执行进一步的步骤。- 参数列表按参数列表中的描述进行评估。
- 如果类型
E
是value_type,则执行装箱转换(Boxing conversions)以转换E
为类型object
,并且在以下步骤中E
被认为是类型object
。在这种情况下,M
只能是一个成员System.Object
。 E
检查值是否有效。如果值E
是null
,一个System.NullReferenceException
被抛出,并且不再执行进一步的步骤。- 确定要调用的函数成员实现:
- 如果绑定时类型
E
是接口,则要调用的函数成员是M
由引用的实例的运行时类型提供的实现E
。此函数成员通过应用接口映射规则(接口映射)来确定M
由引用的实例的运行时类型提供的实现E
。 - 否则,如果
M
是虚函数成员,则要调用的函数成员是M
由引用的实例的运行时类型提供的实现E
。通过应用规则来确定此函数成员,该规则用于确定关于所引用的实例的运行时类型的最多派生实现(虚方法)。M
E
- 否则,
M
是非虚函数成员,并且要调用的函数成员M
本身。
- 如果绑定时类型
- 调用在上面的步骤中确定的函数成员实现。引用
E
的对象成为引用的对象this
。
对装箱实例的调用
在以下情况下,可以通过该value_type的装箱实例调用value_type中实现的函数成员:
- 当函数成员是
override
从类型继承的方法的一个,object
并通过类型的实例表达式调用object
。 - 当函数成员是一个接口功能部件的实现,并通过一个实例表达调用其中interface_type。
- 通过委托调用函数成员时。
在这些情况下,装箱实例被认为包含value_type的变量,并且此变量成为this
函数成员调用中引用的变量。特别是,这意味着当在装箱实例上调用函数成员时,函数成员可以修改装箱实例中包含的值。
主要表达
主要表达式包括最简单的表达形式。
1 primary_expression 2 : primary_no_array_creation_expression 3 | array_creation_expression 4 ; 5 6 primary_no_array_creation_expression 7 : literal 8 | interpolated_string_expression 9 | simple_name 10 | parenthesized_expression 11 | member_access 12 | invocation_expression 13 | element_access 14 | this_access 15 | base_access 16 | post_increment_expression 17 | post_decrement_expression 18 | object_creation_expression 19 | delegate_creation_expression 20 | anonymous_object_creation_expression 21 | typeof_expression 22 | checked_expression 23 | unchecked_expression 24 | default_value_expression 25 | nameof_expression 26 | anonymous_method_expression 27 | primary_no_array_creation_expression_unsafe 28 ;
主表达式在array_creation_expression s和primary_no_array_creation_expression s 之间划分。以这种方式处理数组创建表达式,而不是将其与其他简单表达式表单一起列出,使语法能够禁止可能令人困惑的代码,例如
object o = new int[3][1];
否则将被解释为
object o = (new int[3])[1];
字面
primary_expression,它由一个的文字(字面)被归类为一个值。
插值字符串
一个interpolated_string_expression由一个的$
符号接着是规则的或逐字字符串,其特征在于,孔通过分隔{
和}
,包围表达式和格式规格。插值字符串表达式是interpolated_string_literal的结果,该插值已被分解为单个标记,如插值字符串文字中所述。
1 interpolated_string_expression 2 : '$' interpolated_regular_string 3 | '$' interpolated_verbatim_string 4 ; 5 6 interpolated_regular_string 7 : interpolated_regular_string_whole 8 | interpolated_regular_string_start interpolated_regular_string_body interpolated_regular_string_end 9 ; 10 11 interpolated_regular_string_body 12 : interpolation (interpolated_regular_string_mid interpolation)* 13 ; 14 15 interpolation 16 : expression 17 | expression ',' constant_expression 18 ; 19 20 interpolated_verbatim_string 21 : interpolated_verbatim_string_whole 22 | interpolated_verbatim_string_start interpolated_verbatim_string_body interpolated_verbatim_string_end 23 ; 24 25 interpolated_verbatim_string_body 26 : interpolation (interpolated_verbatim_string_mid interpolation)+ 27 ;
插值中的constant_expression必须具有隐式转换int
。
一个interpolated_string_expression被归类为一个值。如果立即转换为System.IFormattable
或System.FormattableString
使用隐式插值字符串转换(隐式插值字符串转换),则插值字符串表达式具有该类型。否则,它有类型string
。
如果插值字符串的类型是System.IFormattable
或System.FormattableString
,则意思是调用System.Runtime.CompilerServices.FormattableStringFactory.Create
。如果类型是string
,则表达式的含义是对其的调用string.Format
。在这两种情况下,调用的参数列表都包含一个格式字符串文字,每个插值都有占位符,每个表达式的参数对应一个占位符。
格式字符串文字构造如下,其中N
是interpolated_string_expression中的插值数:
- 如果在符号后面有interpolated_regular_string_whole或interpolated_verbatim_string_whole
$
,则格式字符串文字就是该标记。 - 否则,格式字符串文字包括:
- 首先是interpolated_regular_string_start或interpolated_verbatim_string_start
- 然后,每个号码
I
从0
到N-1
:- 十进制表示
I
- 然后,如果相应的插值具有constant_expression,则
,
(逗号)后跟constant_expression的值的十进制表示 - 然后interpolated_regular_string_mid,interpolated_regular_string_end,interpolated_verbatim_string_mid或interpolated_verbatim_string_end紧随对应内插。
- 十进制表示
后面的参数是简单的表达式从所述内插(如果有的话)中,为了。
所有:例子。
simple_name
simple_name包括一个标识符,任选接着进行一个类型参数列表的:
1 simple_name 2 : identifier type_argument_list? 3 ;
simple_name是的形式或者I
或形式的I<A1,...,Ak>
,其中I
是一个单个标识符和<A1,...,Ak>
是一个可选type_argument_list。如果未指定type_argument_list,则认为K
为零。所述simple_name的计算和分类如下:
- 如果
K
为零且simple_name出现在块中,并且块的(或封闭块的)局部变量声明空间(声明)包含局部变量,参数或带名称的常量I
,则simple_name引用该局部变量,参数或常数,并分类为变量或值。 - 如果
K
为零且simple_name出现在泛型方法声明的主体内,并且该声明包含带有name的类型参数I
,则simple_name引用该类型参数。 -
否则,对于每个实例类型
T
(实例类型),从直接封闭的类型声明的实例类型开始,并继续每个封闭类或结构声明的实例类型(如果有):- 如果
K
为零且声明T
包含带有name的类型参数I
,则simple_name引用该类型参数。 - 否则,如果一个成员查找(成员查找的)
I
在T
与K
类型参数产生一个匹配:- 如果
T
是直接包含的类或结构类型的实例类型,并且查找标识一个或多个方法,则结果是具有关联实例表达式的方法组this
。如果指定了类型参数列表,则它用于调用泛型方法(Method invocations)。 - 否则,如果
T
是直接封闭的类或结构类型的实例类型,如果查找标识实例成员,并且引用发生在实例构造函数,实例方法或实例访问器的主体内,则结果为与表单的成员访问(成员访问)相同this.I
。这只能在K
零时发生。 - 否则,结果是一样的成员访问(成员访问的形式的)
T.I
或T.I<A1,...,Ak>
。在这种情况下,simple_name引用实例成员是绑定时错误。
- 如果
- 如果
-
否则,对于每个命名空间
N
,从发生simple_name的命名空间开始,继续使用每个封闭的命名空间(如果有),并以全局命名空间结束,将评估以下步骤,直到找到实体:- 如果
K
为零并且I
是名称空间的名称N
,则:- 如果出现simple_name的位置由名称空间声明括起来,
N
并且名称空间声明包含将名称与名称空间或类型相关联的extern_alias_directive或using_alias_directiveI
,则simple_name不明确并且发生编译时错误。 - 否则,simple_name指命名的命名空间
I
在N
。
- 如果出现simple_name的位置由名称空间声明括起来,
- 否则,如果
N
包含具有名称I
和K
类型参数的可访问类型,则:- 如果
K
为零,并且发生simple_name的位置由名称空间声明括起,N
并且名称空间声明包含将名称与名称空间或类型相关联的extern_alias_directive或using_alias_directiveI
,则simple_name不明确并且发生编译时错误。 - 否则,namespace_or_type_name引用使用给定类型参数构造的类型。
- 如果
- 否则,如果出现simple_name的位置被命名空间声明括起来
N
:- 如果
K
为零且名称空间声明包含将名称与导入的名称空间或类型相关联的extern_alias_directive或using_alias_directiveI
,则simple_name引用该名称空间或类型。 - 否则,如果由名称空间声明的using_namespace_directive和using_static_directive s 导入的名称空间和类型声明只包含一个可访问类型或具有名称
I
和K
类型参数的非扩展静态成员,则simple_name引用使用给定构造的类型或成员类型参数。 - 否则,如果命名空间声明的using_namespace_directive导入的命名空间和类型包含多个具有name
I
和K
type参数的可访问类型或非扩展方法静态成员,则simple_name不明确并且发生错误。
- 如果
请注意,整个步骤与处理namespace_or_type_name(命名空间和类型名称)中的相应步骤完全平行。
- 如果
-
否则,simple_name未定义,并发生编译时错误。
带括号的表达式
parenthesized_expression由一个的表达用括号括起来。
1 parenthesized_expression 2 : '(' expression ')' 3 ;
parenthesized_expression通过评估评估表达的括号内。如果表达的括号内表示命名空间或类型,发生编译时间错误。否则,parenthesized_expression的结果是包含表达式的评估结果。
成员访问权限
member_access由一个的primary_expression,一个predefined_type或qualified_alias_member,接着是“ .
”标记,随后是识别符,任选接着type_argument_list。
1 member_access 2 : primary_expression '.' identifier type_argument_list? 3 | predefined_type '.' identifier type_argument_list? 4 | qualified_alias_member '.' identifier 5 ; 6 7 predefined_type 8 : 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int' | 'long' 9 | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong' | 'ushort' 10 ;
所述qualified_alias_member生产中所定义命名空间的别名限定符。
member_access是的形式或者E.I
或形式的E.I<A1, ..., Ak>
,其中E
是一个基本表达式,I
是单个标识符和<A1, ..., Ak>
是一个可选type_argument_list。如果未指定type_argument_list,则认为K
为零。
member_access与primary_expression类型dynamic
是动态绑定(动态绑定)。在这种情况下,编译器将成员访问权限分类为类型的属性访问权限dynamic
。然后,使用运行时类型而不是primary_expression的编译时类型,在运行时应用以下用于确定member_access含义的规则。如果这个运行时间分类导致方法组,则该成员访问必须是primary_expression一个的invocation_expression。
所述member_access的计算和分类如下:
- 如果
K
为零并且E
是名称空间并且E
包含带名称的嵌套名称空间I
,那么结果就是该名称空间。 - 否则,如果
E
是名称空间并E
包含具有名称I
和K
类型参数的可访问类型,则结果是使用给定类型参数构造的类型。 - 如果
E
是一个predefined_type或primary_expression分类为类型,如果E
不是一个类型参数,并且如果一个成员查找(成员查找的)I
在E
与K
类型参数产生一个匹配,则E.I
计算和分类如下:- 如果
I
标识类型,则结果是使用给定类型参数构造的类型。 - 如果
I
标识一个或多个方法,则结果是没有关联实例表达式的方法组。如果指定了类型参数列表,则它用于调用泛型方法(Method invocations)。 - 如果
I
标识static
属性,则结果是没有关联实例表达式的属性访问。 - 如果
I
标识static
字段:- 如果该字段是
readonly
和引用发生在该领域被声明的类或结构的静态构造函数外,则结果是一个值,该静磁场的即值I
中E
。 - 否则,结果为变量,即静态字段
I
在E
。
- 如果该字段是
- 如果
I
识别static
事件:- 如果引用发生在声明事件的类或结构中,并且事件是在没有event_accessor_declarations(事件)的情况下声明的,那么
E.I
处理时就像I
是静态字段一样。 - 否则,结果是没有关联实例表达式的事件访问。
- 如果引用发生在声明事件的类或结构中,并且事件是在没有event_accessor_declarations(事件)的情况下声明的,那么
- 如果
I
标识常量,则结果为值,即该常量的值。- 如果
I
标识枚举成员,则结果是值,即该枚举成员的值。 - 否则,
E.I
是无效的成员引用,并发生编译时错误。
- 如果
- 如果
- 如果
E
是一个属性访问,索引访问,变量或值,其中是的类型T
,和一个成员查找(成员查找的)I
在T
与K
类型参数产生一个匹配,则E.I
计算和分类如下:- 首先,如果
E
是属性或索引器访问,则获取属性或索引器访问的值(表达式的值)并E
重新分类为值。 - 如果
I
标识一个或多个方法,则结果是具有关联实例表达式的方法组E
。如果指定了类型参数列表,则它用于调用泛型方法(Method invocations)。 - 如果
I
标识实例属性,- 如果
E
是this
,则I
在没有setter的情况下标识自动实现的属性(自动实现的属性),并且引用发生在类或结构类型的实例构造函数中T
,则结果是变量,即给定的自动属性的隐藏后备字段通过I
在实例中T
给出this
。 - 否则,结果是具有关联实例表达式的属性访问
E
。
- 如果
- 如果
T
是class_type和I
标识的实例字段class_type:- 如果值
E
是null
,则System.NullReferenceException
抛出。 - 否则,如果该字段是
readonly
并且引用发生在声明该字段的类的实例构造函数之外,则结果是一个值,即I
引用的对象中的字段的值E
。 - 否则,结果是一个变量,即
I
引用的对象中的字段E
。
- 如果值
- 如果
T
是struct_type和I
标识的实例字段struct_type:- 如果
E
是值,或者字段是readonly
并且引用发生在声明字段的结构的实例构造函数之外,那么结果是一个值,即I
由给定的struct实例中的字段的值E
。 - 否则,结果是一个变量,即由
I
给定的struct实例中的字段E
。
- 如果
- 如果
I
标识实例事件:- 如果引用发生在声明事件的类或结构中,并且事件是在没有event_accessor_declarations(事件)的情况下声明的,并且引用不是作为
+=
或-=
运算符的左侧发生的,则E.I
处理完全如同I
是一个实例字段。 - 否则,结果是具有关联实例表达式的事件访问
E
。
- 如果引用发生在声明事件的类或结构中,并且事件是在没有event_accessor_declarations(事件)的情况下声明的,并且引用不是作为
- 首先,如果
- 否则,尝试
E.I
作为扩展方法调用进行处理(扩展方法调用)。如果失败,E.I
则是无效的成员引用,并发生绑定时错误。
相同的简单名称和类型名称
在以下形式的成员访问E.I
,如果E
是单个标识符,并且如果的含义E
为simple_name(简单的名称)是一个常数,字段,属性,局部变量或参数与相同类型的含义E
为TYPE_NAME(命名空间和类型名称),然后E
允许两种可能的含义。两种可能的含义E.I
永远不会模糊,因为在两种情况下都I
必须是该类型的成员E
。换句话说,该规则只允许访问静态成员和嵌套类型,E
否则将发生编译时错误。例如:
1 struct Color 2 { 3 public static readonly Color White = new Color(...); 4 public static readonly Color Black = new Color(...); 5 6 public Color Complement() {...} 7 } 8 9 class A 10 { 11 public Color Color; // Field Color of type Color 12 13 void F() { 14 Color = Color.Black; // References Color.Black static member 15 Color = Color.Complement(); // Invokes Complement() on Color field 16 } 17 18 static void G() { 19 Color c = Color.White; // References Color.White static member 20 } 21 }
语法歧义
对于生产simple_name(简单名称)和member_access(成员访问)可引起歧义的语法的表达式。例如,声明:
F(G<A,B>(7));
可以被解释为对F
两个参数的调用,G < A
和B > (7)
。或者,它可以被解释为对F
一个参数的调用,这是对G
具有两个类型参数和一个常规参数的泛型方法的调用。
如果标记序列可以(在上下文中)被解析为一个simple_name(简单名称),member_access(成员访问),或pointer_member_access(指针成员访问了结尾)type_argument_list(类型参数),之后收盘立即令牌>
令牌检查。如果它是其中之一
( ) ] } : ; , . ? == != | ^
然后type_argument_list作为simple_name,member_access或pointer_member_access的一部分保留,并且丢弃令牌序列的任何其他可能的解析。否则,type_argument_list不被视为simple_name,member_access或pointer_member_access的一部分,即使没有其他可能的标记序列解析。注解析时,这些规则不适用type_argument_list在namespace_or_type_name(命名空间和类型名称)。该声明
F(G<A,B>(7));
根据此规则,将被解释为对F
一个参数的调用,该参数是对G
具有两个类型参数和一个常规参数的泛型方法的调用。声明
1 F(G < A, B > 7); 2 F(G < A, B >> 7);
将每个都解释为对F
两个参数的调用。该声明
x = F < A > +y;
令牌C<T>
被解释为带有type_argument_list的namespace_or_type_name。
调用表达式
一个invocation_expression用于调用的方法。
1 invocation_expression 2 : primary_expression '(' argument_list? ')' 3 ;
一个invocation_expression动态绑定(动态绑定)如果以下中的至少一个成立:
- 该primary_expression具有编译时类型
dynamic
。 - 可选argument_list的至少一个参数具有编译时类型
dynamic
,而primary_expression没有委托类型。
在这种情况下,编译器将invocation_expression分类为type的值dynamic
。然后使用运行时类型而不是primary_expression的编译时类型和具有编译时类型的参数,在运行时应用以下用于确定invocation_expression含义的规则。如果primary_expression没有编译时类型,则方法调用将经历有限的编译时检查,如编译时检查动态重载决策所述。dynamic
dynamic
所述primary_expression一个的invocation_expression必须是一个方法基或的值delegate_type。如果primary_expression是方法组,则invocation_expression是方法调用(Method invocations)。如果primary_expression是delegate_type的值,则invocation_expression是委托调用(Delegate invocations)。如果primary_expression既不是方法组也不是delegate_type的值,则会发生绑定时错误。
可选的argument_list(Argument lists)为方法的参数提供值或变量引用。
评估invocation_expression的结果分类如下:
- 如果invocation_expression调用返回的方法或委托
void
,则结果为空。只有在statement_expression(表达式语句)的上下文中或者作为lambda_expression(匿名函数表达式)的主体才允许被分类为空的表达式。否则会发生绑定时错误。 - 否则,结果是方法或委托返回的类型的值。
方法调用
对于方法调用中,primary_expression所述的invocation_expression必须是一个方法组。方法组标识要调用的一个方法或从中选择要调用的特定方法的重载方法集。在后一种情况下,确定要调用的特定方法是基于argument_list中参数类型提供的上下文。
表单的方法调用的绑定时处理M(A)
,其中M
是方法组(可能包括type_argument_list),并且A
是可选的argument_list,包括以下步骤:
- 构造了方法调用的候选方法集。对于
F
与方法组关联的每个方法M
:- 如果
F
是非通用的,F
则在以下情况下是候选人:M
没有类型参数列表,和F
适用于A
(适用的功能成员)。
- 如果
F
是通用的并且M
没有类型参数列表,F
则在以下情况下是候选者:- 类型推断(类型推断)成功,推断出调用的类型参数列表,以及
- 一旦推断的类型参数替换相应的方法类型参数,F的参数列表中的所有构造的类型满足它们的约束(满足约束),并且参数列表
F
适用于A
(适用的函数成员)。
- 如果
F
是通用的并且M
包含类型参数列表,F
则在以下情况下是候选者:F
具有与类型参数列表中提供的方法类型参数相同的数量,并且- 一旦类型参数替换相应的方法类型参数,F的参数列表中的所有构造类型满足其约束(满足约束),并且参数列表
F
适用于A
(适用的函数成员)。
- 如果
- 候选方法集合被简化为仅包含来自大多数派生类型的方法:对于
C.F
集合中的每个方法,声明C
方法的类型在哪里F
,C
从集合中删除在基本类型中声明的所有方法。此外,如果C
是类型以外object
的类型,则从集合中删除在接口类型中声明的所有方法。(后一条规则仅在方法组是对具有除object之外的有效基类和非空有效接口集的类型参数进行成员查找的结果时才会生效。) - 如果得到的候选方法集合为空,则放弃沿着以下步骤的进一步处理,而是尝试将调用作为扩展方法调用(扩展方法调用)进行处理。如果失败,则不存在适用的方法,并发生绑定时错误。
- 使用Overload resolution的重载决策规则来识别候选方法集的最佳方法。如果无法识别单个最佳方法,则方法调用不明确,并发生绑定时错误。执行重载解析时,在将类型参数(提供或推断)替换为相应的方法类型参数之后,将考虑泛型方法的参数。
- 执行所选最佳方法的最终验证:
- 该方法在方法组的上下文中进行验证:如果最佳方法是静态方法,则方法组必须由simple_name或member_access通过类型生成。如果该最佳方法是一个实例方法,该方法组必须从simple_name,一个member_access通过一个变量或值,或一个base_access。如果这些要求都不成立,则会发生绑定时错误。
- 如果最佳方法是泛型方法,则根据泛型方法上声明的约束(满足约束)检查类型参数(提供或推断)。如果任何类型参数不满足类型参数上的相应约束,则会发生绑定时错误。
通过上述步骤在绑定时选择并验证方法后,将根据动态重载决策的编译时检查中描述的函数成员调用规则处理实际运行时调用。
上面描述的解析规则的直观效果如下:要定位由方法调用调用的特定方法,请从方法调用指示的类型开始,然后继续继承链,直到至少一个适用的,可访问的,非覆盖为止找到方法声明。然后对在该类型中声明的适用的,可访问的,非重写方法集执行类型推断和重载解析,并调用由此选择的方法。如果未找到任何方法,请尝试将调用作为扩展方法调用进行处理。
扩展方法调用
在其中一个表单的方法调用(对装箱实例进行调用)中
1 expr . identifier ( ) 2 3 expr . identifier ( args ) 4 5 expr . identifier < typeargs > ( ) 6 7 expr . identifier < typeargs > ( args )
如果调用的正常处理找不到适用的方法,则尝试将该构造作为扩展方法调用进行处理。如果expr或任何args具有编译时类型dynamic
,则扩展方法将不适用。
目标是找到最佳的type_name C
,以便可以进行相应的静态方法调用:
1 C . identifier ( expr ) 2 3 C . identifier ( expr , args ) 4 5 C . identifier < typeargs > ( expr ) 6 7 C . identifier < typeargs > ( expr , args )
扩展方法Ci.Mj
是符合条件的,如果:
Ci
是一个非泛型的非嵌套类- 名称
Mj
是标识符 Mj
当作为静态方法应用于参数时,可访问且适用,如上所示- 从expr到第一个参数的类型存在隐式标识,引用或装箱转换
Mj
。
C
收益搜索如下:
- 从最近的封闭命名空间声明开始,继续每个封闭的命名空间声明,并以包含的编译单元结束,连续尝试查找一组候选扩展方法:
- 如果给定的命名空间或编译单元直接包含
Ci
具有合格扩展方法的非泛型类型声明Mj
,则这些扩展方法的集合是候选集。 - 如果using_static_declarations
Ci
导入并在给定命名空间或编译单元中由using_namespace_directive s 导入的名称空间中直接声明的类型直接包含符合条件的扩展方法,则这些扩展方法的集合是候选集。Mj
- 如果给定的命名空间或编译单元直接包含
- 如果在任何封闭的名称空间声明或编译单元中找不到候选集,则会发生编译时错误。
- 否则,如(过载分辨率)中所述,将过载分辨率应用于候选集。如果找不到单个最佳方法,则会发生编译时错误。
C
是将最佳方法声明为扩展方法的类型。
使用C
方法调用作为目标,然后作为静态方法调用(动态重载决策的编译时检查)处理。
前面的规则意味着实例方法优先于扩展方法,内部命名空间声明中可用的扩展方法优先于外部命名空间声明中可用的扩展方法,并且直接在命名空间中声明的扩展方法优先于导入到该命名空间中的扩展方法。带有using namespace指令的命名空间。例如:
1 public static class E 2 { 3 public static void F(this object obj, int i) { } 4 5 public static void F(this object obj, string s) { } 6 } 7 8 class A { } 9 10 class B 11 { 12 public void F(int i) { } 13 } 14 15 class C 16 { 17 public void F(object obj) { } 18 } 19 20 class X 21 { 22 static void Test(A a, B b, C c) { 23 a.F(1); // E.F(object, int) 24 a.F("hello"); // E.F(object, string) 25 26 b.F(1); // B.F(int) 27 b.F("hello"); // E.F(object, string) 28 29 c.F(1); // C.F(object) 30 c.F("hello"); // C.F(object) 31 } 32 }
在该示例中,B
s方法优先于第一个扩展方法,并且C
s方法优先于两个扩展方法。
1 public static class C 2 { 3 public static void F(this int i) { Console.WriteLine("C.F({0})", i); } 4 public static void G(this int i) { Console.WriteLine("C.G({0})", i); } 5 public static void H(this int i) { Console.WriteLine("C.H({0})", i); } 6 } 7 8 namespace N1 9 { 10 public static class D 11 { 12 public static void F(this int i) { Console.WriteLine("D.F({0})", i); } 13 public static void G(this int i) { Console.WriteLine("D.G({0})", i); } 14 } 15 } 16 17 namespace N2 18 { 19 using N1; 20 21 public static class E 22 { 23 public static void F(this int i) { Console.WriteLine("E.F({0})", i); } 24 } 25 26 class Test 27 { 28 static void Main(string[] args) 29 { 30 1.F(); 31 2.G(); 32 3.H(); 33 } 34 } 35 }
这个例子的输出是:
1 E.F(1) 2 D.G(2) 3 C.H(3)
D.G
优先于C.G
,E.F
优先于两者D.F
和C.F
。
委托调用
对于委托调用,该primary_expression中的invocation_expression必须是一个值delegate_type。此外,考虑到delegate_type是具有与delegate_type相同的参数列表的函数成员,delegate_type必须适用于invocation_expression的argument_list(适用的函数成员)。
以下形式的委托调用的运行时处理D(A)
,其中D
是一个primary_expression一个的delegate_type和A
是一个可选argument_list中,由以下步骤组成:
D
被评估。如果此评估导致异常,则不执行进一步的步骤。D
检查值是否有效。如果值D
是null
,一个System.NullReferenceException
被抛出,并且不再执行进一步的步骤。- 否则,
D
是对委托实例的引用。在委托的调用列表中的每个可调用实体上执行函数成员调用(动态重载解析的编译时检查)。对于由实例和实例方法组成的可调用实体,调用的实例是可调用实体中包含的实例。
元素访问
一个element_access由一个的primary_no_array_creation_expression,接着是“ [
”标记,接着是argument_list中,随后是“ ]
”令牌。所述argument_list中包含的一个或多个参数 S,用逗号分开。
1 element_access 2 : primary_no_array_creation_expression '[' expression_list ']' 3 ;
该argument_list中的的element_access不得含有ref
或out
参数。
一个element_access动态绑定(动态绑定)如果以下中的至少一个成立:
- 该primary_no_array_creation_expression具有编译时类型
dynamic
。 - argument_list的至少一个表达式具有编译时类型
dynamic
,而primary_no_array_creation_expression没有数组类型。
在这种情况下,编译器将element_access分类为type的值dynamic
。然后,使用运行时类型而不是具有编译时类型的primary_no_array_creation_expression和argument_list表达式的编译时类型,在运行时应用以下用于确定element_access含义的规则。如果primary_no_array_creation_expression没有编译时类型,则元素访问将经历有限的编译时检查,如编译时检查动态重载决策所述。dynamic
dynamic
如果primary_no_array_creation_expression一个的element_access是一个的值ARRAY_TYPE,所述element_access是一个数组访问(数组访问)。否则,primary_no_array_creation_expression必须是具有一个或多个索引器成员的类,结构或接口类型的变量或值,在这种情况下,element_access是索引器访问(索引器访问)。
数组访问
用于阵列存取时,primary_no_array_creation_expression所述的element_access必须是一个的值ARRAY_TYPE。此外,argument_list中的阵列访问不允许包含名为arguments.The在表达式数argument_list中必须是相同的秩ARRAY_TYPE,并且每个表达式的类型必须是int
,uint
,long
,ulong
,或必须是隐式转换一种或多种这些类型。
评估数组访问的结果是数组的元素类型的变量,即由argument_list中的表达式的值选择的数组元素。
的形式的一个数组访问运行时处理P[A]
,其中P
是一个primary_no_array_creation_expression一个的ARRAY_TYPE和A
是argument_list中,由以下步骤组成:
P
被评估。如果此评估导致异常,则不执行进一步的步骤。- argument_list的索引表达式按从左到右的顺序进行计算。以下每个索引表达的评估,一个隐式转换(隐式转换)为以下类型中的一种进行:
int
,uint
,long
,ulong
。选择此列表中存在隐式转换的第一种类型。例如,如果索引表达式的类型是short
再到的隐式转换int
被执行,由于从隐式转换short
到int
和从short
到long
是可能的。如果对索引表达式的评估或后续的隐式转换导致异常,则不会评估其他索引表达式,也不会执行其他步骤。 P
检查值是否有效。如果值P
是null
,一个System.NullReferenceException
被抛出,并且不再执行进一步的步骤。- argument_list中每个表达式的值将根据引用的数组实例的每个维度的实际边界进行检查
P
。如果一个或多个值超出范围,System.IndexOutOfRangeException
则抛出a并且不执行进一步的步骤。 - 计算由索引表达式给出的数组元素的位置,并且该位置成为数组访问的结果。
索引器访问
对于一个索引访问,所述primary_no_array_creation_expression所述的element_access必须是类,结构或接口类型的变量或值,并且该类型必须实现一个或多个适用的索引器相对于所述argument_list中所述的element_access。
表单的索引器访问的绑定时处理P[A]
,其中P
是类,结构或接口类型的primary_no_array_creation_expressionT
,并且A
是argument_list,包括以下步骤:
- 提供了由提供的索引器集
T
。该集由在声明的所有索引的T
或基本类型T
不在override
声明的,并且在当前上下文(访问成员访问)。 - 该集合被简化为适用的索引器,而不是其他索引器隐藏的索引器。以下规则适用
S.I
于集合中的每个索引器,其中S
索引器I
的声明类型:- 如果
I
不适用于A
(适用的功能成员),I
则从集合中删除。 - 如果
I
适用于A
(适用的函数成员),S
则从集合中删除以基本类型声明的所有索引器。 - 如果
I
适用于A
(适用的函数成员)并且S
是类型以外的类型object
,则从集合中删除在接口中声明的所有索引器。
- 如果
- 如果生成的候选索引器集合为空,则不存在适用的索引器,并且发生绑定时错误。
- 使用Overload resolution的重载决策规则来识别候选索引器集的最佳索引器。如果无法识别单个最佳索引器,则索引器访问不明确,并且发生绑定时错误。
- argument_list的索引表达式按从左到右的顺序进行计算。处理索引器访问的结果是被分类为索引器访问的表达式。索引器访问表达式引用在上面的步骤中确定的索引器,并且具有关联的实例表达式
P
和关联的参数列表A
。
根据索引器访问的上下文,索引器访问会导致调用get访问器或索引器的set访问器。如果索引器访问是赋值的目标,则调用set访问器以分配新值(简单赋值)。在所有其他情况下,调用get访问器以获取当前值(表达式的值)。
this访问
一个THIS_ACCESS由保留字的this
。
1 this_access 2 : 'this' 3 ;
THIS_ACCESS仅在允许块实例构造,实例方法,或实例访问的。它具有以下含义之一:
- 当
this
在类的实例构造函数中的primary_expression中使用时,它被分类为值。值的类型是发生使用的类的实例类型(实例类型),值是对正在构造的对象的引用。 - 当在类的实例方法或实例访问器
this
中的primary_expression中使用时,它被分类为值。值的类型是发生使用的类的实例类型(实例类型),值是对调用方法或访问器的对象的引用。 - 当
this
在结构的实例构造函数中的primary_expression中使用时,它被归类为变量。变量的类型是发生用法的结构的实例类型(实例类型),变量表示正在构造的结构。this
结构的实例构造函数的变量out
与结构类型的参数完全相同- 特别是,这意味着必须在实例构造函数的每个执行路径中明确赋值变量。 - 当在结构的实例方法或实例访问器
this
中的primary_expression中使用时,它被归类为变量。变量的类型是发生使用的struct 的实例类型(实例类型)。- 如果方法或访问器不是迭代器(迭代器),则
this
变量表示调用方法或访问器的结构,并且行为ref
与结构类型的参数完全相同。 - 如果方法或访问器是迭代器,则该
this
变量表示为其调用方法或访问器的结构的副本,并且其行为与结构类型的值参数完全相同。
- 如果方法或访问器不是迭代器(迭代器),则
使用this
在primary_expression比上面列出的其他一个方面是编译时错误。特别是,不可能this
在静态方法,静态属性访问器或字段声明的variable_initializer中引用。
基类访问
base_access由保留字的base
后跟一个“ .
”令牌和标识符或argument_list中在方括号内:
1 base_access 2 : 'base' '.' identifier 3 | 'base' '[' expression_list ']' 4 ;
甲base_access用于访问由类似命名的元件在当前类或结构隐藏基类成员。甲base_access仅在允许块实例构造,实例方法,或实例访问的。当base.I
发生在类或结构中时,I
必须表示该类或结构的基类的成员。同样,当base[E]
在类中发生时,基类中必须存在适用的索引器。
在绑定时,表单的base_access表达式base.I
和base[E]
它们的编写方式完全相同,((B)this).I
并且((B)this)[E]
在哪里B
是构造发生的类或结构的基类。因此,base.I
并且base[E]
对应于this.I
和this[E]
,除了this
被视为基类的实例之外。
当base_access引用虚函数成员(方法,属性或索引器)时,确定在运行时调用哪个函数成员(动态重载决策的编译时检查)。调用的函数成员是通过查找函数成员的最多派生实现(虚方法)来确定的B
(而不是相对于this
非基本访问中通常的运行时类型)。因此,内的override
一个的virtual
功能部件,base_access可以用来调用继承执行功能部件。如果函数成员由base_access引用 是抽象的,发生绑定时错误。
后缀增量和减量运算符
1 post_increment_expression 2 : primary_expression '++' 3 ; 4 5 post_decrement_expression 6 : primary_expression '--' 7 ;
后缀增量或减量操作的操作数必须是分类为变量,属性访问或索引器访问的表达式。操作的结果是与操作数相同类型的值。
如果primary_expression具有编译时类型,dynamic
则运算符是动态绑定的(动态绑定),post_increment_expression或post_decrement_expression具有编译时类型,dynamic
并且使用primary_expression的运行时类型在运行时应用以下规则。
如果后缀增量或减量操作的操作数是属性或索引器访问,则属性或索引器必须同时具有a get
和set
访问者。如果不是这种情况,则会发生绑定时错误。
应用一元运算符重载决策(一元运算符重载决策)来选择特定的运算符实现。预定义++
和--
运算符存在以下几种类型:sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,和任何枚举类型。预定义++
运算符返回通过向操作数加1产生的值,预定义--
运算符返回通过从操作数中减去1产生的值。在checked
上下文中,如果此加法或减法的结果超出结果类型的范围,并且结果类型是整数类型或枚举类型,System.OverflowException
则抛出a。
运行时处理表单的后缀增量或减量操作x++
或x--
由以下步骤组成:
- 如果
x
被归类为变量:x
被评估以产生变量。x
保存的值。- 调用所选运算符,并将保存的值
x
作为其参数。 - 运算符返回的值存储在评估给定的位置
x
。 - 保存的值
x
成为操作的结果。
- 如果
x
被归类为属性或索引器访问:- 将评估实例表达式(如果
x
不是static
)和x
与之关联的参数列表(如果是索引器访问权限)x
,并在后续get
和set
访问者调用中使用结果。 - 该
get
的访问x
被调用和返回值被保存。 - 调用所选运算符,并将保存的值
x
作为其参数。 - 该
set
访问器x
调用由运算符作为其返回的值value
参数。 - 保存的值
x
成为操作的结果。
- 将评估实例表达式(如果
在++
和--
运算符也支持前缀表示法(前缀增量和减量运算)。通常,结果x++
或者x--
是x
操作之前的值,而结果是++x
或者--x
是x
操作之后的值。在任何一种情况下,x
操作后本身都具有相同的值。
一个operator ++
或operator --
实现可以使用任何后缀或前缀符号来调用。这两种表示法不可能有单独的运算符实现。
new运算符
该new
操作符用于创建类型的新实例。
有三种new
表达形式:
- 对象创建表达式用于创建类类型和值类型的新实例。
- 数组创建表达式用于创建数组类型的新实例。
- 委托创建表达式用于创建委托类型的新实例。
该new
运算符意味着一个类型的实例的创建,但并不一定意味着动态分配的内存。特别是,值类型的实例除了它们所在的变量之外不需要额外的内存,并且在new
用于创建值类型的实例时不会发生动态分配。
对象创建表达式
一个object_creation_expression被用于创建的新实例class_type或VALUE_TYPE。
1 object_creation_expression 2 : 'new' type '(' argument_list? ')' object_or_collection_initializer? 3 | 'new' type object_or_collection_initializer 4 ; 5 6 object_or_collection_initializer 7 : object_initializer 8 | collection_initializer 9 ;
该类型的的object_creation_expression必须是class_type,一个VALUE_TYPE或type_parameter。该类型不能是abstract
class_type。
仅当类型为class_type或struct_type时,才允许使用可选的argument_list(Argument lists)。
对象创建表达式可以省略构造函数参数列表并括起括号,前提是它包含对象初始值设定项或集合初始值设定项。省略构造函数参数列表并括起括号等效于指定空参数列表。
处理包括对象初始值设定项或集合初始值设定项的对象创建表达式包括首先处理实例构造函数,然后处理由对象初始值设定项(对象初始值设定项)或集合初始值设定项(集合初始值设定项)指定的成员或元素初始化。
如果任何在可选的参数argument_list中具有编译时类型dynamic
则object_creation_expression动态绑定(动态绑定)和下述规则使用的那些参数的运行时类型在运行时施加argument_list中具有的编译时间类型dynamic
。但是,对象创建经历了有限的编译时间检查,如动态重载分辨率的编译时检查中所述。
表单的object_creation_expression的绑定时处理new T(A)
,其中T
是class_type或value_type并且A
是可选的argument_list,包括以下步骤:
- 如果
T
是VALUE_TYPE并且A
不存在:- 该object_creation_expression是一个默认的构造函数调用。object_creation_expression的结果是type的值
T
,即System.ValueType类型中T
定义的默认值。
- 该object_creation_expression是一个默认的构造函数调用。object_creation_expression的结果是type的值
- 否则,如果
T
是type_parameter并且A
不存在:- 如果未指定值类型约束或构造函数约束(类型参数约束)
T
,则会发生绑定时错误。 - object_creation_expression的结果是type参数绑定的运行时类型的值,即调用该类型的默认构造函数的结果。运行时类型可以是引用类型或值类型。
- 如果未指定值类型约束或构造函数约束(类型参数约束)
- 否则,如果
T
是class_type或struct_type:- 如果
T
是abstract
class_type,则发生编译时错误。 - 要调用的实例构造函数是使用Overload resolution的重载决策规则确定的。候选实例构造函数集包含所有可访问的实例构造函数,
T
其中适用于A
(适用的函数成员)。如果候选实例构造函数集为空,或者无法识别单个最佳实例构造函数,则会发生绑定时错误。 - object_creation_expression的结果是type
T
的值,即通过调用上面步骤中确定的实例构造函数生成的值。
- 如果
- 否则,object_creation_expression无效,并发生绑定时错误。
即使object_creation_expression是动态绑定的,编译时类型仍然是T
。
表单的object_creation_expression的运行时处理new T(A)
,其中T
是class_type或struct_type并且A
是可选的argument_list,包括以下步骤:
- 如果
T
是class_type:T
分配了一个新的类实例。如果没有足够的可用内存来分配新实例,System.OutOfMemoryException
则抛出a并且不执行进一步的步骤。- 新实例的所有字段都初始化为其默认值(默认值)。
- 根据函数成员调用的规则(动态重载决策的编译时检查)调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,并且可以从该构造函数中访问该实例
this
。
- 如果
T
是struct_type:T
通过分配临时局部变量来创建类型的实例。由于struct_type的实例构造函数需要明确地为正在创建的实例的每个字段分配值,因此不需要初始化临时变量。- 根据函数成员调用的规则(动态重载决策的编译时检查)调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,并且可以从该构造函数中访问该实例
this
。
对象初始化器
一个对象初始化指定零个或多个字段,属性或对象的索引元素值。
1 object_initializer 2 : '{' member_initializer_list? '}' 3 | '{' member_initializer_list ',' '}' 4 ; 5 6 member_initializer_list 7 : member_initializer (',' member_initializer)* 8 ; 9 10 member_initializer 11 : initializer_target '=' initializer_value 12 ; 13 14 initializer_target 15 : identifier 16 | '[' argument_list ']' 17 ; 18 19 initializer_value 20 : expression 21 | object_or_collection_initializer 22 ;
对象初始值设定项由一系列成员初始值设定项组成,由括号{
和}
标记括起并用逗号分隔。每个member_initializer指定初始化的目标。一个标识符必须命名对象的可访问字段或属性被初始化,而一个argument_list中在方括号内必须为在物体上的可访问索引被初始化指定参数。对象初始值设定项为同一字段或属性包含多个成员初始值设定项是错误的。
每个initializer_target后跟一个等号和一个表达式,一个对象初始值设定项或一个集合初始值设定项。对象初始值设定项中的表达式不可能引用它正在初始化的新创建的对象。
成员初始值设定项指定等号后的表达式,其处理方式与对目标的赋值(简单赋值)相同。
在等号后面指定对象初始值设定项的成员初始值设定项是嵌套对象初始值设定项,即嵌入对象的初始化。而不是为字段或属性分配新值,而是将嵌套对象初始值设定项中的赋值视为对字段或属性成员的赋值。嵌套对象初始值设定项不能应用于具有值类型的属性,也不能应用于具有值类型的只读字段。
在等号后面指定集合初始值设定项的成员初始值设定项是嵌入式集合的初始化。不是将新集合分配给目标字段,属性或索引器,而是将初始化程序中给出的元素添加到目标引用的集合中。目标必须是满足Collection初始值设定项中指定的要求的集合类型。
索引初始值设定项的参数将始终只计算一次。因此,即使最终的参数永远不会被使用(例如,由于空的嵌套初始化器),它们将被评估它们的副作用。
以下类表示具有两个坐标的点:
1 public class Point 2 { 3 int x, y; 4 5 public int X { get { return x; } set { x = value; } } 6 public int Y { get { return y; } set { y = value; } } 7 }
Point
可以创建并初始化实例,如下所示:
Point a = new Point { X = 0, Y = 1 };
效果与...相同
1 Point __a = new Point(); 2 __a.X = 0; 3 __a.Y = 1; 4 Point a = __a;
哪个__a
是不可见且无法访问的临时变量。以下类表示从两点创建的矩形:
1 public class Rectangle 2 { 3 Point p1, p2; 4 5 public Point P1 { get { return p1; } set { p1 = value; } } 6 public Point P2 { get { return p2; } set { p2 = value; } } 7 }
Rectangle
可以创建并初始化实例,如下所示:
1 Rectangle r = new Rectangle { 2 P1 = new Point { X = 0, Y = 1 }, 3 P2 = new Point { X = 2, Y = 3 } 4 };
效果与...相同
1 Rectangle __r = new Rectangle(); 2 Point __p1 = new Point(); 3 __p1.X = 0; 4 __p1.Y = 1; 5 __r.P1 = __p1; 6 Point __p2 = new Point(); 7 __p2.X = 2; 8 __p2.Y = 3; 9 __r.P2 = __p2; 10 Rectangle r = __r;
在那里__r
,__p1
并且__p2
是临时变量是不可见的,交通不便。
如果Rectangle
构造函数分配了两个嵌入式Point
实例
1 public class Rectangle 2 { 3 Point p1 = new Point(); 4 Point p2 = new Point(); 5 6 public Point P1 { get { return p1; } } 7 public Point P2 { get { return p2; } } 8 }
以下构造可用于初始化嵌入式Point
实例,而不是分配新实例:
1 Rectangle r = new Rectangle { 2 P1 = { X = 0, Y = 1 }, 3 P2 = { X = 2, Y = 3 } 4 };
效果与...相同
1 Rectangle __r = new Rectangle(); 2 __r.P1.X = 0; 3 __r.P1.Y = 1; 4 __r.P2.X = 2; 5 __r.P2.Y = 3; 6 Rectangle r = __r;
给定C的适当定义,以下示例:
1 var c = new C { 2 x = true, 3 y = { a = "Hello" }, 4 z = { 1, 2, 3 }, 5 ["x"] = 5, 6 [0,0] = { "a", "b" }, 7 [1,2] = {} 8 };
相当于这一系列的任务:
1 C __c = new C(); 2 __c.x = true; 3 __c.y.a = "Hello"; 4 __c.z.Add(1); 5 __c.z.Add(2); 6 __c.z.Add(3); 7 string __i1 = "x"; 8 __c[__i1] = 5; 9 int __i2 = 0, __i3 = 0; 10 __c[__i2,__i3].Add("a"); 11 __c[__i2,__i3].Add("b"); 12 int __i4 = 1, __i5 = 2; 13 var c = __c;
where __c等等,是生成的源代码不可见和不可访问的变量。请注意,参数for [0,0]仅计算一次,并且[1,2]即使它们从未使用过,也会对其参数进行一次计算。
集合初始化器
集合初始值设定项指定集合的元素。
1 collection_initializer 2 : '{' element_initializer_list '}' 3 | '{' element_initializer_list ',' '}' 4 ; 5 6 element_initializer_list 7 : element_initializer (',' element_initializer)* 8 ; 9 10 element_initializer 11 : non_assignment_expression 12 | '{' expression_list '}' 13 ; 14 15 expression_list 16 : expression (',' expression)* 17 ;
集合初始值设定项由一系列元素初始值设定项组成,由括号{
和}
标记括起并用逗号分隔。每个元素初始化指定要添加到被初始化的集合对象的元素,并且由表达式由封闭的列表{
和}
令牌和用逗号分开。可以在没有大括号的情况下编写单表达式元素初始值设定项,但不能成为赋值表达式,以避免使用成员初始值设定项产生歧义。所述non_assignment_expression生产中所定义的表达。
以下是包含集合初始值设定项的对象创建表达式的示例:
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
应用集合初始值设定项的集合对象必须是实现的类型System.Collections.IEnumerable
或发生编译时错误。对于按顺序的每个指定元素,集合初始值设定项调用Add
目标对象上的方法,并将元素初始值设定项的表达式列表作为参数列表,为每个调用应用正常成员查找和重载解析。因此,集合对象必须具有适用的实例或扩展方法,并具有Add
每个元素初始值设定项的名称。
以下类表示具有姓名和电话号码列表的联系人:
1 public class Contact 2 { 3 string name; 4 List<string> phoneNumbers = new List<string>(); 5 6 public string Name { get { return name; } set { name = value; } } 7 8 public List<string> PhoneNumbers { get { return phoneNumbers; } } 9 }
List<Contact>
可以创建和初始化如下:
1 var contacts = new List<Contact> { 2 new Contact { 3 Name = "Chris Smith", 4 PhoneNumbers = { "206-555-0101", "425-882-8080" } 5 }, 6 new Contact { 7 Name = "Bob Harris", 8 PhoneNumbers = { "650-555-0199" } 9 } 10 };
效果与...相同
1 var __clist = new List<Contact>(); 2 Contact __c1 = new Contact(); 3 __c1.Name = "Chris Smith"; 4 __c1.PhoneNumbers.Add("206-555-0101"); 5 __c1.PhoneNumbers.Add("425-882-8080"); 6 __clist.Add(__c1); 7 Contact __c2 = new Contact(); 8 __c2.Name = "Bob Harris"; 9 __c2.PhoneNumbers.Add("650-555-0199"); 10 __clist.Add(__c2); 11 var contacts = __clist;
在那里__clist
,__c1
并且__c2
是临时变量是不可见的,转移不便。
数组创建表达式
一个array_creation_expression被用于创建的新实例ARRAY_TYPE。
1 array_creation_expression 2 : 'new' non_array_type '[' expression_list ']' rank_specifier* array_initializer? 3 | 'new' array_type array_initializer 4 | 'new' rank_specifier array_initializer 5 ;
第一个表单的数组创建表达式分配一个类型的数组实例,该数组实例是从表达式列表中删除每个单独的表达式而得到的。例如,数组创建表达式new int[10,20]
生成一个类型的数组实例,int[,]
数组创建表达式new int[10][,]
生成一个类型的数组int[][,]
。表达式列表中的每个表达式的类型必须是int
,uint
,long
,或ulong
,或隐式转换为一个或多个这些类型的。每个表达式的值确定新分配的数组实例中相应维度的长度。由于数组维度的长度必须是非负的,因此具有constant_expression是编译时错误 表达式列表中的值为负值。
除了不安全的上下文(Unsafe contexts)之外,未指定数组的布局。
如果第一个表单的数组创建表达式包含数组初始值设定项,则表达式列表中的每个表达式必须是常量,并且表达式列表指定的等级和维度长度必须与数组初始值设定项的长度匹配。
在第二种或第三种形式的数组创建表达式中,指定数组类型或等级说明符的等级必须与数组初始值设定项的等级匹配。各个维度长度是从阵列初始化器的每个相应嵌套级别中的元素数量推断的。因此,表达
new int[,] {{0, 1}, {2, 3}, {4, 5}}
完全对应
new int[3, 2] {{0, 1}, {2, 3}, {4, 5}}
第三种形式的数组创建表达式称为隐式类型化数组创建表达式。它与第二种形式类似,不同之处在于未明确给出数组的元素类型,而是确定为数组初始值设定项中表达式集的最佳公共类型(查找一组表达式的最佳公共类型)。对于一个多维阵列,即,一个其中rank_specifier包含至少一个逗号,这个组包括所有表达在嵌套求出的S array_initializer秒。
数组初始化器在Array初始化器中进一步描述。
评估数组创建表达式的结果被分类为值,即对新分配的数组实例的引用。数组创建表达式的运行时处理包括以下步骤:
- 所述的尺寸长度表达式expression_list为了进行评估,从左到右。以下各表达的评估,一个隐式转换(隐式转换)为以下类型中的一种进行:
int
,uint
,long
,ulong
。选择此列表中存在隐式转换的第一种类型。如果对表达式的计算或后续的隐式转换导致异常,则不会评估其他表达式,也不会执行其他步骤。 - 尺寸长度的计算值验证如下。如果一个或多个值小于零,
System.OverflowException
则抛出a并且不执行进一步的步骤。 - 分配具有给定维度长度的数组实例。如果没有足够的可用内存来分配新实例,
System.OutOfMemoryException
则抛出a并且不执行进一步的步骤。 - 新数组实例的所有元素都初始化为其默认值(默认值)。
- 如果数组创建表达式包含数组初始值设定项,则会计算数组初始值设定项中的每个表达式并将其分配给其对应的数组元素。评估和分配按照表达式在数组初始值设定项中的顺序执行 - 换句话说,元素按递增的索引顺序初始化,最右边的维度首先增加。如果对给定表达式的评估或对相应数组元素的后续赋值导致异常,则不会初始化其他元素(因此其余元素将具有其默认值)。
数组创建表达式允许使用数组类型的元素实例化数组,但必须手动初始化此类数组的元素。例如,声明
int[][] a = new int[100][];
创建一个包含100个元素类型的单维数组int[]
。每个元素的初始值是null
。同一数组创建表达式也不可能实例化子数组和语句
int[][] a = new int[100][5]; // Error
导致编译时错误。必须手动执行子阵列的实例化,如
1 int[][] a = new int[100][]; 2 for (int i = 0; i < 100; i++) a[i] = new int[5];
当数组数组具有“矩形”形状时,即当子数组的长度都相同时,使用多维数组会更有效。在上面的示例中,数组数组的实例化创建了101个对象 - 一个外部数组和100个子数组。相反,
int[,] = new int[100, 5];
只创建一个对象,一个二维数组,并在一个语句中完成分配。
以下是隐式类型化数组创建表达式的示例:
1 var a = new[] { 1, 10, 100, 1000 }; // int[] 2 3 var b = new[] { 1, 1.5, 2, 2.5 }; // double[] 4 5 var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,] 6 7 var d = new[] { 1, "one", 2, "two" }; // Error
最后一个表达式会导致编译时错误,因为它既不能int
也不能string
隐式转换为另一个,因此没有最佳的常见类型。在这种情况下,必须使用显式类型的数组创建表达式,例如指定要使用的类型object[]
。或者,可以将其中一个元素强制转换为公共基类型,然后这将成为推断的元素类型。
隐式类型化数组创建表达式可以与匿名对象初始值设定项(匿名对象创建表达式)结合使用,以创建匿名类型化数据结构。例如:
1 var contacts = new[] { 2 new { 3 Name = "Chris Smith", 4 PhoneNumbers = new[] { "206-555-0101", "425-882-8080" } 5 }, 6 new { 7 Name = "Bob Harris", 8 PhoneNumbers = new[] { "650-555-0199" } 9 } 10 };
委托创建表达式
一个delegate_creation_expression被用于创建的新实例delegate_type。
1 delegate_creation_expression 2 : 'new' delegate_type '(' expression ')' 3 ;
委托创建表达式的参数必须是方法组,匿名函数或编译时类型dynamic
或委托类型的值。如果参数是方法组,则它标识方法,对于实例方法,标识要为其创建委托的对象。如果参数是匿名函数,它直接定义委托目标的参数和方法体。如果参数是值,则它标识要为其创建副本的委托实例。
如果表达式具有编译时类型dynamic
,则delegate_creation_expression是动态绑定的(动态绑定),并且使用表达式的运行时类型在运行时应用以下规则。否则,规则将在编译时应用。
表单的delegate_creation_expression的绑定时处理new D(E)
,其中D
是delegate_type并且E
是表达式,包括以下步骤:
- 如果
E
是方法组,则委托创建表达式以相同的方式为方法团转化(处理方法组转换从)E
到D
。 - 如果
E
是匿名函数,则委托创建表达式以相同的方式处理为一个匿名函数转换(匿名函数转换从)E
到D
。 - 如果
E
是值,则E
必须与(与委托声明)兼容D
,并且结果是对新创建的类型的委托的引用,该委托D
引用相同的调用列表E
。如果E
不兼容D
,则发生编译时错误。
表单的delegate_creation_expression的运行时处理new D(E)
,其中D
是delegate_type并且E
是表达式,包括以下步骤:
- 如果
E
是一个方法组,则委托创建表达式被评估为的方法组转换(方法组转换从)E
到D
。 - 如果
E
是匿名函数,则委托创建将被评估为从(匿名函数转换)转换E
为匿名函数。D
- 如果
E
是delegate_type的值:E
被评估。如果此评估导致异常,则不执行进一步的步骤。- 如果值
E
是null
,一个System.NullReferenceException
被抛出,并且不再执行进一步的步骤。 D
分配了委托类型的新实例。如果没有足够的可用内存来分配新实例,System.OutOfMemoryException
则抛出a并且不执行进一步的步骤。- 使用与委托实例相同的调用列表初始化新的委托实例
E
。
在委托实例化时确定委托的调用列表,然后在委托的整个生命周期内保持不变。换句话说,一旦创建了委托的目标可调用实体,就无法对其进行更改。当两个代表合并或一个代表从另一个委托中删除时(委托声明),会产生新的代表; 没有现有的委托更改其内容。
无法创建引用属性,索引器,用户定义的运算符,实例构造函数,析构函数或静态构造函数的委托。
如上所述,当从方法组创建委托时,委托的形式参数列表和返回类型确定要选择哪个重载方法。在这个例子中
1 delegate double DoubleFunc(double x); 2 3 class A 4 { 5 DoubleFunc f = new DoubleFunc(Square); 6 7 static float Square(float x) { 8 return x * x; 9 } 10 11 static double Square(double x) { 12 return x * x; 13 } 14 }
该A.f
字段使用引用第二种Square
方法的委托进行初始化,因为该方法与形式参数列表和返回类型完全匹配DoubleFunc
。如果第二种Square
方法不存在,则会发生编译时错误。
匿名对象创建表达式
一个anonymous_object_creation_expression用于创建匿名类型的一个对象。
1 anonymous_object_creation_expression 2 : 'new' anonymous_object_initializer 3 ; 4 5 anonymous_object_initializer 6 : '{' member_declarator_list? '}' 7 | '{' member_declarator_list ',' '}' 8 ; 9 10 member_declarator_list 11 : member_declarator (',' member_declarator)* 12 ; 13 14 member_declarator 15 : simple_name 16 | member_access 17 | base_access 18 | null_conditional_member_access 19 | identifier '=' expression 20 ;
匿名对象初始值设定项声明匿名类型并返回该类型的实例。匿名类型是直接继承的无名类类型object
。匿名类型的成员是从用于创建该类型实例的匿名对象初始值设定项推断的一系列只读属性。具体来说,是表单的匿名对象初始值设定项
new { p1 = e1, p2 = e2, ..., pn = en }
声明表单的匿名类型
1 class __Anonymous1 2 { 3 private readonly T1 f1; 4 private readonly T2 f2; 5 ... 6 private readonly Tn fn; 7 8 public __Anonymous1(T1 a1, T2 a2, ..., Tn an) { 9 f1 = a1; 10 f2 = a2; 11 ... 12 fn = an; 13 } 14 15 public T1 p1 { get { return f1; } } 16 public T2 p2 { get { return f2; } } 17 ... 18 public Tn pn { get { return fn; } } 19 20 public override bool Equals(object __o) { ... } 21 public override int GetHashCode() { ... } 22 }
其中每个Tx
都是相应表达式的类型ex
。member_declarator中使用的表达式必须具有类型。因此,member_declarator中的表达式为null或匿名函数是编译时错误。表达式具有不安全类型也是编译时错误。
匿名类型的名称及其Equals
方法的参数由编译器自动生成,不能在程序文本中引用。
在同一程序中,两个匿名对象初始值设定项以相同的顺序指定相同名称和编译时类型的属性序列,这将生成相同匿名类型的实例。
在这个例子中
1 var p1 = new { Name = "Lawnmower", Price = 495.00 }; 2 var p2 = new { Name = "Shovel", Price = 26.95 }; 3 p1 = p2;
允许在最后一行上进行分配,因为p1
它p2
具有相同的匿名类型。
匿名类型上的Equals
和GetHashcode
方法重写继承自的方法object
,并根据属性Equals
和GetHashcode
属性进行定义,以便当且仅当所有属性相等时,同一匿名类型的两个实例才相等。
成员声明符可以缩写为简单名称(类型推断),成员访问(动态重载解析的编译时检查),基本访问(基本访问)或空条件成员访问(空条件表达式作为投影)初始化者)。这称为投影初始值设定项,是对具有相同名称的属性进行声明和赋值的简写。具体来说,是表单的成员声明者
1 identifier 2 expr.identifier
正好分别等同于以下内容:
1 identifier = identifier 2 identifier = expr.identifier
因此,在投影初始化器中,标识符选择值和分配值的字段或属性。直观地说,投影初始化器不仅投影值,还投射值的名称。
typeof运算符
typeof
运算符使用,以获得System.Type
对于一种类型的对象。
1 typeof_expression 2 : 'typeof' '(' type ')' 3 | 'typeof' '(' unbound_type_name ')' 4 | 'typeof' '(' 'void' ')' 5 ; 6 7 unbound_type_name 8 : identifier generic_dimension_specifier? 9 | identifier '::' identifier generic_dimension_specifier? 10 | unbound_type_name '.' identifier generic_dimension_specifier? 11 ; 12 13 generic_dimension_specifier 14 : '<' comma* '>' 15 ; 16 17 comma 18 : ',' 19 ;
typeof_expression的第一种形式由一个typeof
关键字后跟一个带括号的类型组成。表达式的结果是System.Type
指定类型的对象。System.Type
任何给定类型只有一个对象。这意味着对于一个类型T
,typeof(T) == typeof(T)
总是如此。该类型不能dynamic
。
typeof_expression的第二种形式由一个typeof
关键字后跟一个带括号的unbound_type_name组成。一个unbound_type_name是非常类似于一个TYPE_NAME(命名空间和类型名称不同之处在于)unbound_type_name包含generic_dimension_specifier s其中一个TYPE_NAME包含type_argument_list秒。当typeof_expression的操作数是满足unbound_type_name和type_name的语法的标记序列时,即它既不包含generic_dimension_specifier也是如此也不是type_argument_list,标记序列被认为是type_name。unbound_type_name的含义确定如下:
- 转换的令牌的序列与TYPE_NAME通过替换每generic_dimension_specifier与type_argument_list具有相同数量的逗号和关键字
object
作为各type_argument。 - 评估生成的type_name,同时忽略所有类型参数约束。
- 所述unbound_type_name解析为与所得到的构造类型(相关联的未结合的通用型结合的和未结合的类型)。
typeof_expression的结果是System.Type
生成的未绑定泛型类型的对象。
typeof_expression的第三种形式由一个typeof
关键字后跟一个带括号的void
关键字组成。表达式的结果是System.Type
表示缺少类型的对象。返回typeof(void)
的类型对象与为任何类型返回的类型对象不同。这个特殊类型对象在允许反射到语言中的方法的类库中很有用,其中这些方法希望有一种方法来表示任何方法的返回类型,包括void方法,其实例为System.Type
。
的typeof
操作者可以在类型参数一起使用。结果是System.Type
绑定到type参数的运行时类型的对象。的typeof
操作者也可以在构造类型或绑定的泛型类型(用于结合和未结合的类型)。所述System.Type
用于绑定的泛型类型的对象是不一样的System.Type
实例类型的对象。实例类型在运行时始终是闭合构造类型,因此其System.Type
对象取决于正在使用的运行时类型参数,而未绑定泛型类型没有类型参数。
这个例子
1 using System; 2 3 class X<T> 4 { 5 public static void PrintTypes() { 6 Type[] t = { 7 typeof(int), 8 typeof(System.Int32), 9 typeof(string), 10 typeof(double[]), 11 typeof(void), 12 typeof(T), 13 typeof(X<T>), 14 typeof(X<X<T>>), 15 typeof(X<>) 16 }; 17 for (int i = 0; i < t.Length; i++) { 18 Console.WriteLine(t[i]); 19 } 20 } 21 } 22 23 class Test 24 { 25 static void Main() { 26 X<int>.PrintTypes(); 27 } 28 }
产生以下输出:
1 System.Int32 2 System.Int32 3 System.String 4 System.Double[] 5 System.Void 6 System.Int32 7 X`1[System.Int32] 8 X`1[X`1[System.Int32]] 9 X`1[T]
请注意,int
并System.Int32
属于同一类型。
还要注意,结果typeof(X<>)
不依赖于类型参数而是依赖于结果typeof(X<T>)
。
已检查和未选中的运算符
checked
和unchecked
运算符都用于控制溢出检查上下文用于整型算术运算和转换。
1 checked_expression 2 : 'checked' '(' expression ')' 3 ; 4 5 unchecked_expression 6 : 'unchecked' '(' expression ')' 7 ;
checked
运算符计算在checked上下文所包含的表达,而unchecked
运算符计算在一个未经检查的上下文中包含的表达式。甲checked_expression或unchecked_expression精确地对应于一个parenthesized_expression(括号的表达式),不同的是包含表达在给定的溢出检查上下文评估。
溢出检查上下文也可以通过checked
和unchecked
语句(已检查和未检查的语句)来控制。
以下操作受checked
和unchecked
运算符和语句建立的溢出检查上下文的影响:
- 当操作数是整数类型时,预定义
++
和--
一元运算符(Postfix递增和递减运算符以及前缀递增和递减运算符)。 - 当操作数是整数类型时,预定义的
-
一元运算符(一元减运算符)。 - 预定义的
+
,-
,*
,和/
二元运算(算术运算符),当两个操作数都是整数类型的。 - 显式数字转换(显式数字转换),从一个整数类型到另一个整数类型,或从
float
或double
到整数类型。
当上述操作之一产生的结果太大而无法在目标类型中表示时,执行操作的上下文控制结果行为:
- 在
checked
上下文中,如果操作是常量表达式(常量表达式),则会发生编译时错误。否则,当在运行时执行操作时,System.OverflowException
抛出a。 - 在
unchecked
上下文中,通过丢弃不适合目标类型的任何高位来截断结果。
对于未由任何checked
或unchecked
运算符或语句包含的非常量表达式(在运行时计算的表达式),unchecked
除非外部因素(例如编译器开关和执行环境配置)要求进行checked
评估,否则缺省溢出检查上下文。
对于常量表达式(可在编译时完全计算的表达式),默认溢出检查上下文始终为checked
。除非在unchecked
上下文中显式放置常量表达式,否则在表达式的编译时评估期间发生的溢出总是会导致编译时错误。
匿名函数的主体不受匿名函数发生的影响checked
或unchecked
上下文影响。
在这个例子中
1 class Test 2 { 3 static readonly int x = 1000000; 4 static readonly int y = 1000000; 5 6 static int F() { 7 return checked(x * y); // Throws OverflowException 8 } 9 10 static int G() { 11 return unchecked(x * y); // Returns -727379968 12 } 13 14 static int H() { 15 return x * y; // Depends on default 16 } 17 }
没有报告编译时错误,因为这两个表达式都不能在编译时计算。在运行时,该F
方法抛出一个System.OverflowException
,并且该G
方法返回-727379968(超出范围结果的低32位)。该H
方法的行为取决于编译的默认溢出检查上下文,但它与...相同F
或相同G
。
在这个例子中
1 class Test 2 { 3 const int x = 1000000; 4 const int y = 1000000; 5 6 static int F() { 7 return checked(x * y); // Compile error, overflow 8 } 9 10 static int G() { 11 return unchecked(x * y); // Returns -727379968 12 } 13 14 static int H() { 15 return x * y; // Compile error, overflow 16 } 17 }
在计算常量表达式时发生的溢出,F
并H
导致报告编译时错误,因为表达式是在checked
上下文中计算的。在计算常量表达式时也会发生溢出G
,但由于评估发生在unchecked
上下文中,因此不会报告溢出。
在checked
与unchecked
运算符只会影响那些以文本包含在“中那些操作的溢出检查上下文(
”和“ )
”标记。运算符对由于计算包含的表达式而调用的函数成员没有影响。在这个例子中
1 class Test 2 { 3 static int Multiply(int x, int y) { 4 return x * y; 5 } 6 7 static int F() { 8 return checked(Multiply(1000000, 1000000)); 9 } 10 }
使用checked
在F
不影响的评估x * y
中Multiply
,因此x * y
在默认溢出检查上下文评估。
unchecked
以十六进制表示法编写带符号整数类型的常量时,操作符很方便。例如:
1 class Test 2 { 3 public const int AllBits = unchecked((int)0xFFFFFFFF); 4 5 public const int HighBit = unchecked((int)0x80000000); 6 }
上面的两个十六进制常量都是类型uint
。因为常量超出了int
范围,所以没有unchecked
运算符,转换int
会产生编译时错误。
在checked
与unchecked
运算符和语句允许程序员控制一些数值计算的某些方面。但是,某些数字运算符的行为取决于它们的操作数的数据类型。例如,即使在显式unchecked
构造中,乘以两个小数也总是导致溢出异常。类似地,即使在显式checked
构造中,乘以两个浮点也不会导致溢出异常。此外,其他运算符永远不会受到检查模式的影响,无论是默认还是显式。
默认值表达式
默认值表达式用于获取类型的默认值(默认值)。通常,默认值表达式用于类型参数,因为可能不知道类型参数是值类型还是引用类型。(null
除非已知类型参数是引用类型,否则从文字到类型参数不存在转换。)
1 default_value_expression 2 : 'default' '(' type ')' 3 ;
如果default_value_expression中的类型在运行时评估为引用类型,则结果将转换为该类型。如果default_value_expression中的类型在运行时评估为值类型,则结果为value_type的默认值(默认构造函数)。null
甲default_value_expression是一个常量表达式(常量表达式)如果类型是引用类型或已知为引用类型(类型参数类型参数约束)。另外,default_value_expression是一个常量表达式如果类型为以下值类型之一:sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,bool
,或任何枚举类型。
表达的名称
nameof_expression被用于获得一个节目实体为一个常数字符串的名称。
1 nameof_expression 2 : 'nameof' '(' named_entity ')' 3 ; 4 5 named_entity 6 : simple_name 7 | named_entity_target '.' identifier type_argument_list? 8 ; 9 10 named_entity_target 11 : 'this' 12 | 'base' 13 | named_entity 14 | predefined_type 15 | qualified_alias_member 16 ;
从语法上讲,named_entity操作数始终是一个表达式。因为nameof
不是保留关键字,所以通过调用简单名称,表达式的名称在语法上总是不明确的nameof
。出于兼容性原因,如果名称的名称查找(简单名称)nameof
成功,则表达式将被视为invocation_expression - 无论调用是否合法。否则它是nameof_expression。
所述的含义named_entity一个的nameof_expression是它作为表达的含义; 也就是说,作为simple_name,base_access或member_access。但是,在简单名称和成员访问中描述的查找导致错误,因为在静态上下文中找到实例成员,nameof_expression不会产生此类错误。
named_entity指定方法组具有type_argument_list是编译时错误。named_entity_target具有该类型是编译时错误dynamic
。
甲nameof_expression是类型的常量表达式string
,并且具有在运行时没有任何影响。具体来说,它的named_entity不会被评估,并且为了明确的赋值分析(简单表达式的一般规则)而被忽略。它的值是可选的最终type_argument_list之前的named_entity的最后一个标识符,按以下方式转换:
@
如果使用前缀“ ”,则将其删除。- 每个unicode_escape_sequence都会转换为相应的Unicode字符。
- 删除任何formatting_characters。
这些是在测试标识符之间的相等性时在标识符中应用的相同转换。
匿名方法表达式
一个anonymous_method_expression是定义匿名函数的两种方式之一。这些在匿名函数表达式中进一步描述。
一元运算符
?
,+
,-
,!
,~
,++
,--
,演员,以及await
运算符被称为一元运算符。
1 unary_expression 2 : primary_expression 3 | null_conditional_expression 4 | '+' unary_expression 5 | '-' unary_expression 6 | '!' unary_expression 7 | '~' unary_expression 8 | pre_increment_expression 9 | pre_decrement_expression 10 | cast_expression 11 | await_expression 12 | unary_expression_unsafe 13 ;
空条件运算符
仅当该操作数为非null时,空条件运算符才将操作列表应用于其操作数。否则应用运算符的结果是null
。
1 null_conditional_expression 2 : primary_expression null_conditional_operations 3 ; 4 5 null_conditional_operations 6 : null_conditional_operations? '?' '.' identifier type_argument_list? 7 | null_conditional_operations? '?' '[' argument_list ']' 8 | null_conditional_operations '.' identifier type_argument_list? 9 | null_conditional_operations '[' argument_list ']' 10 | null_conditional_operations '(' argument_list? ')' 11 ;
操作列表可以包括成员访问和元素访问操作(它们本身可以是空条件的),以及调用。
例如,表达式a.b?[0]?.c()
是null_conditional_expression,具有primary_expression a.b
和null_conditional_operations ?[0]
(空条件元素访问),?.c
(空条件成员访问)和()
(调用)。
对于带有primary_expression的null_conditional_expression ,让我们通过以文本方式从具有一个null_conditional_operations的每个null_conditional_operations中删除前导来获得表达式。从概念上讲,如果s 表示的空检查都没有找到,则表达式将被评估。E
P
E0
?
E
E0
?
null
另外,让我们E1
通过以文本方式?
从第一个null_conditional_operations中删除前导来获得表达式E
。这可能会导致主表达式(如果只有一个?
)或另一个null_conditional_expression。
例如,如果E
是表达式a.b?[0]?.c()
,然后E0
是表达式a.b[0].c()
并且E1
是表达a.b[0]?.c()
。
如果E0
被归类为无,则E
归类为无。否则E被归类为值。
E0
并E1
用于确定以下含义E
:
-
如果
E
作为statement_expression发生,则其含义E
与语句相同
if ((object)P != null) E1;
-
除了P只评估一次。
-
否则,如果
E0
被归类为没有发生编译时错误。 -
否则,让我们
T0
的类型E0
。 -
如果
T0
是未知为引用类型或非可空值类型的类型参数,则会发生编译时错误。 -
如果
T0
是不可为空的值类型,则E
is 的类型T0?
和含义E
相同 -
((object)P == null) ? (T0?)null : E1
除了
P
只评估一次。 - 否则E的类型是T0,E的含义是相同的
-
((object)P == null) ? null : E1
-
除了
P
只评估一次。
如果E1
本身是null_conditional_expression,那么再次应用这些规则,将测试嵌套null
直到没有其他?
的,并且表达式一直减少到primary-expression E0
。
例如,如果表达式a.b?[0]?.c()
作为statement-expression出现,如在语句中:
a.b?[0]?.c();
它的含义相当于:
if (a.b != null) a.b[0]?.c();
除了a.b
和a.b[0]
只计算一次。
如果它出现在使用其值的上下文中,如:
var x = a.b?[0]?.c();
并假设最终调用的类型不是非可空值类型,其含义相当于:
var x = (a.b == null) ? null : (a.b[0] == null) ? null : a.b[0].c();
除了a.b
和a.b[0]
仅评估一次。
空条件表达式作为初始化器
一个空的条件表达式仅仅允许作为member_declarator在anonymous_object_creation_expression(匿名对象创建表达式如果它与一个(任选地空条件)成员访问结束时)。在语法上,这个要求可以表示为:
1 null_conditional_member_access 2 : primary_expression null_conditional_operations? '?' '.' identifier type_argument_list? 3 | primary_expression null_conditional_operations '.' identifier type_argument_list? 4 ;
这是上面null_conditional_expression的语法的特例。该生产member_declarator在匿名对象创建表达式,然后只包括null_conditional_member_access。
空条件表达式作为语句表达式
如果null_条件表达式以调用结束,则它仅允许作为statement_expression(Expression语句)使用。在语法上,这个要求可以表示为:
1 null_conditional_invocation_expression 2 : primary_expression null_conditional_operations '(' argument_list? ')' 3 ;
一元加运算符
对于表单的操作+x
,应用一元运算符重载决策(一元运算符重载决策)来选择特定的运算符实现。操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。预定义的一元加运算符是:
1 int operator +(int x); 2 uint operator +(uint x); 3 long operator +(long x); 4 ulong operator +(ulong x); 5 float operator +(float x); 6 double operator +(double x); 7 decimal operator +(decimal x);
对于每个运算符,结果只是操作数的值。
一元减运算符
对于表单的操作-x
,应用一元运算符重载决策(一元运算符重载决策)来选择特定的运算符实现。操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。预定义的否定运算符是:
-
整数否定:
1 int operator -(int x); 2 long operator -(long x);
结果通过x
从零减去来计算。如果of的x
值是操作数类型的最小可表示值(-2 ^ 31表示int
或-2 ^ 63表示long
),则数学否定x
在操作数类型中不可表示。如果这发生在checked
上下文中,System.OverflowException
则抛出a; 如果它出现在unchecked
上下文中,则结果是操作数的值,并且不报告溢出。如果否定运算符的操作数是类型uint
,则将其转换为类型long
,结果的类型为long
。允许int
值-2147483648(-2 ^ 31)写为十进制整数文字(整数文字)的规则是一个例外。如果否定运算符的操作数是类型ulong
,则发生编译时错误。允许将long
值-9223372036854775808(-2 ^ 63)写为十进制整数文字(整数文字)的规则是一个例外。
-
浮点否定:
1 float operator -(float x); 2 double operator -(double x);
结果是x
其符号的值被反转。如果x
是NaN,结果也是NaN。
- 十进制否定:
decimal operator -(decimal x);
结果通过x
从零减去来计算。十进制否定等效于使用类型的一元减运算符System.Decimal
。
逻辑否定运算符
对于表单的操作!x
,应用一元运算符重载决策(一元运算符重载决策)来选择特定的运算符实现。操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。只存在一个预定义的逻辑否定运算符:
bool operator !(bool x);
此运算符计算操作数的逻辑否定:如果操作数是true
,则结果为false
。如果操作数是false
,结果是true
。
按位补码运算符
对于表单的操作~x
,应用一元运算符重载决策(一元运算符重载决策)来选择特定的运算符实现。操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。预定义的按位补码运算符是:
1 int operator ~(int x); 2 uint operator ~(uint x); 3 long operator ~(long x); 4 ulong operator ~(ulong x);
对于这些运算符中的每一个,运算的结果是按位的补码x
。
每个枚举类型E
隐式提供以下按位补码运算符:
1 E operator ~(E x);
评估的结果~x
,其中x
是具有E
基础类型的枚举类型的表达式,与U
评估完全相同(E)(~(U)x)
,除了转换E
为始终在unchecked
上下文中执行(已检查和未检查的运算符)。
前缀增量和减量运算符
1 pre_increment_expression 2 : '++' unary_expression 3 ; 4 5 pre_decrement_expression 6 : '--' unary_expression 7 ;
前缀增量或减量操作的操作数必须是分类为变量,属性访问或索引器访问的表达式。操作的结果是与操作数相同类型的值。
如果前缀增量或减量操作的操作数是属性或索引器访问,则属性或索引器必须同时具有a get
和set
访问器。如果不是这种情况,则会发生绑定时错误。
应用一元运算符重载决策(一元运算符重载决策)来选择特定的运算符实现。预定义++
和--
运算符存在以下几种类型:sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,和任何枚举类型。预定义++
运算符返回通过向操作数加1产生的值,预定义--
运算符返回通过从操作数中减去1产生的值。在checked
上下文中,如果此加法或减法的结果超出结果类型的范围,并且结果类型是整数类型或枚举类型,System.OverflowException
则抛出a。
运行时处理表单的前缀增量或减量操作++x
或--x
由以下步骤组成:
- 如果
x
被归类为变量:x
被评估以产生变量。- 调用所选运算符,其值为
x
其参数。 - 运算符返回的值存储在评估给定的位置
x
。 - 运算符返回的值将成为操作的结果。
- 如果
x
被归类为属性或索引器访问:- 将评估实例表达式(如果
x
不是static
)和x
与之关联的参数列表(如果是索引器访问权限)x
,并在后续get
和set
访问者调用中使用结果。 - 该
get
的访问x
被调用。 - 调用所选运算符,并将
get
访问器返回的值作为其参数。 - 该
set
访问器x
调用由运算符作为其返回的值value
参数。 - 运算符返回的值将成为操作的结果。
- 将评估实例表达式(如果
在++
和--
运算符也支持后缀表示法(后缀增量和减量运算)。通常,结果x++
或者x--
是x
操作之前的值,而结果是++x
或者--x
是x
操作之后的值。在任何一种情况下,x
操作后本身都具有相同的值。
一个operator++
或operator--
实现可以使用任何后缀或前缀符号来调用。这两种表示法不可能有单独的运算符实现。
转换表达式
cast_expression用于将表达显式转换为给定类型。
1 cast_expression 2 : '(' type ')' unary_expression 3 ;
表单的cast_expression(T)E
,其中T
是一个类型并且E
是一个unary_expression,它执行对type 的值的显式转换(显式转换)。如果没有明确的转换,从存在到,发生绑定时错误。否则,结果是显式转换产生的值。即使表示变量,结果也始终归类为值。E
T
E
T
E
cast_expression的语法会导致某些语法歧义。例如,表达式(x)-y
可以解释为cast_expression(转换-y
为类型x
)或者作为additive_expression与parenthesized_expression(计算值)相结合x - y)
。
要解决cast_expression歧义,存在以下规则:括号中的一个或多个标记 s(空格)的序列仅在至少满足下列条件之一时才被视为cast_expression的开头:
- 令牌序列是一种类型的正确语法,但不适用于表达式。
- 令牌序列是一种类型的正确语法,紧跟在右括号后面的令牌是令牌“
~
”,令牌“!
”,令牌“(
”,标识符(Unicode字符转义序列),文字(Literals),或任何关键字(关键字)除as
和is
。
上面的“正确语法”一词仅表示令牌序列必须符合特定的语法产生。它没有特别考虑任何组成标识符的实际含义。例如,if x
和y
是标识符,那么x.y
类型的语法是正确的,即使x.y
实际上并不表示类型。
从消除歧义规则可以得出,如果x
和y
是标识符,(x)y
,(x)(y)
和(x)(-y)
是cast_expression S,但(x)-y
没有,即使x
识别类型。但是,如果x
是标识预定义类型(例如int
)的关键字,则所有四种形式都是cast_expression(因为这样的关键字本身不可能是表达式)。
await表达式
await运算符用于暂停对封闭异步函数的求值,直到操作数表示的异步操作完成为止。
1 await_expression 2 : 'await' unary_expression 3 ;
一个await_expression只允许在一个异步函数(的主体迭代)。在最近的封闭异步函数中,在这些位置可能不会发生await_expression:
- 嵌套(非异步)匿名函数内部
- 在lock_statement的块内
- 在不安全的背景下
请注意,在query_expression中的大多数位置都不会出现await_expression,因为它们在语法上转换为使用非异步lambda表达式。
在异步函数内部,await
不能用作标识符。因此,await-expressions和涉及标识符的各种表达式之间没有语法歧义。在异步函数之外,await
充当普通标识符。
await_expression的操作数称为任务。它表示在评估await_expression时可能完成或可能不完成的异步操作。await运算符的目的是暂停执行封闭的异步函数,直到await的任务完成,然后获取其结果。
awaitable表达式
一个await表达的任务需要为awaitable。t
如果满足以下条件之一,则表达式是await的:
t
是编译时类型dynamic
t
有一个可访问的实例或扩展方法调用GetAwaiter
没有参数和没有类型参数,以及一个返回类型A
,其中包含以下所有内容:A
实现接口System.Runtime.CompilerServices.INotifyCompletion
(以下简称INotifyCompletion
)A
具有可访问的,可读的实例属性IsCompleted
类型的bool
A
有一个可访问的实例方法GetResult
,没有参数和类型参数
该GetAwaiter
方法的目的是获得该任务的await者。该类型A
称为await表达式的awaiter类型。
该IsCompleted
属性的目的是确定任务是否已完成。如果是这样,则无需暂停评估。
该INotifyCompletion.OnCompleted
方法的目的是为任务注册“继续”; 即一个System.Action
任务完成后将被调用的委托(类型)。
该GetResult
方法的目的是在任务完成后获得任务的结果。该结果可能是成功完成,可能具有结果值,或者它可能是该GetResult
方法抛出的异常。
await表达式的分类
表达式await
的分类方式与表达式相同(t).GetAwaiter().GetResult()
。因此,如果返回类型GetResult
为void
,则await_expression被归类为空。如果它具有非void返回类型T
,则await_expression被分类为type的值T
。
await表达式的运行时评估
在运行时,表达式await t
的计算方法如下:
a
通过评估表达式获得await者(t).GetAwaiter()
。- 阿
bool
b
通过评估表达式获得(a).IsCompleted
。 - 如果
b
是,false
则评估取决于是否a
实现接口System.Runtime.CompilerServices.ICriticalNotifyCompletion
(以下称为ICriticalNotifyCompletion
简洁)。这项检查是在约束时完成的; 即在运行时,如果a
具有编译时类型dynamic
,则在编译时,否则。让我们r
表示恢复代表(迭代者):- 如果
a
未实现ICriticalNotifyCompletion
,则计算表达式(a as (INotifyCompletion)).OnCompleted(r)
。 - 如果
a
实现ICriticalNotifyCompletion
,则计算表达式(a as (ICriticalNotifyCompletion)).UnsafeOnCompleted(r)
。 - 然后暂停评估,并将控制权返回给异步功能的当前调用者。
- 如果
- 紧接着(如果
b
是true
),或者稍后调用恢复委托(如果b
是false
),(a).GetResult()
则计算表达式。如果它返回一个值,那么该值就是await_expression的结果。否则结果什么都没有。
一个awaiter的实现的接口方法INotifyCompletion.OnCompleted
,并ICriticalNotifyCompletion.UnsafeOnCompleted
应引起代表r
将最多一次调用。否则,未定义封闭异步函数的行为。
算术运算符
的*
,/
,%
,+
,和-
运算符称为算术运算符。
1 multiplicative_expression 2 : unary_expression 3 | multiplicative_expression '*' unary_expression 4 | multiplicative_expression '/' unary_expression 5 | multiplicative_expression '%' unary_expression 6 ; 7 8 additive_expression 9 : multiplicative_expression 10 | additive_expression '+' multiplicative_expression 11 | additive_expression '-' multiplicative_expression 12 ;
如果算术运算符的操作数具有编译时类型dynamic
,则表达式是动态绑定的(动态绑定)。在这种情况下,表达式的编译时类型是dynamic
,并且下面描述的解析将在运行时使用具有编译时类型的那些操作数的运行时类型进行dynamic
。
乘法运算符
对于表单的操作x * y
,应用二元运算符重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义的乘法运算符。运算符所有计算的产品x
和y
。
-
整数乘法:
1 int operator *(int x, int y); 2 uint operator *(uint x, uint y); 3 long operator *(long x, long y); 4 ulong operator *(ulong x, ulong y);
-
在
checked
上下文中,如果产品超出结果类型的范围,System.OverflowException
则抛出a。在unchecked
上下文中,不报告溢出,并且丢弃结果类型范围之外的任何重要高位。 -
浮点乘法:
1 float operator *(float x, float y); 2 double operator *(double x, double y);
产品根据IEEE 754算法的规则计算。下表列出了非零有限值,零,无穷大和NaN的所有可能组合的结果。在表中,x
并且y
是正的有限值。z
是结果x * y
。如果结果对于目标类型而言太大,z
则为无穷大。如果结果对于目标类型而言太小,z
则为零。
is zero.
+y | -y | +0 | -0 | +inf | -inf | NaN | |
+x | +z | -z | +0 | -0 | +inf | -inf | NaN |
-x | -z | +z | -0 | +0 | -inf | +inf | NaN |
+0 | +0 | -0 | +0 | -0 | NaN | NaN | NaN |
-0 | -0 | +0 | -0 | +0 | NaN | NaN | NaN |
+inf | +inf | -inf | NaN | NaN | +inf | -inf | NaN |
-inf | -inf | +inf | NaN | NaN | -inf | +inf | NaN |
NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
- 十进制乘法:
decimal operator *(decimal x, decimal y);
如果结果值太大而无法以decimal
格式表示,System.OverflowException
则抛出a。如果结果值太小而无法以decimal
格式表示,则结果为零。在任何舍入之前,结果的比例是两个操作数的比例之和。
十进制乘法等效于使用类型的乘法运算符System.Decimal
。
分部运算符
对于表单的操作x / y
,应用二元运算符重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义的除法运算符。运算符所有计算的商数x
和y
。
- 整数除法:
1 int operator /(int x, int y); 2 uint operator /(uint x, uint y); 3 long operator /(long x, long y); 4 ulong operator /(ulong x, ulong y);
如果右操作数的值为零,System.DivideByZeroException
则抛出a。
该部门将结果舍入为零。因此,结果的绝对值是小于或等于两个操作数的商的绝对值的最大可能整数。当两个操作数具有相同符号时,结果为零或正,当两个操作数具有相反符号时,结果为零或负。
如果左操作数是可表示的最小值int
或long
值且右操作数是-1
,则发生溢出。在checked
上下文中,这会导致System.ArithmeticException
抛出(或其子类)。在unchecked
上下文中,它是实现定义的,是否System.ArithmeticException
抛出(或其子类)或溢出未报告,结果值是左操作数的值。
- 浮点除法:
1 float operator /(float x, float y); 2 double operator /(double x, double y);
根据IEEE 754算法的规则计算商。下表列出了非零有限值,零,无穷大和NaN的所有可能组合的结果。在表中,x
并且y
是正的有限值。z
是结果x / y
。如果结果对于目标类型而言太大,z
则为无穷大。如果结果对于目标类型而言太小,z
则为零。
+y | -y | +0 | -0 | +inf | -inf | NaN | |
+x | +z | -z | +inf | -inf | +0 | -0 | NaN |
-x | -z | +z | -inf | +inf | -0 | +0 | NaN |
+0 | +0 | -0 | NaN | NaN | +0 | -0 | NaN |
-0 | -0 | +0 | NaN | NaN | -0 | +0 | NaN |
+inf | +inf | -inf | +inf | -inf | NaN | NaN | NaN |
-inf | -inf | +inf | -inf | +inf | NaN | NaN | NaN |
NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
- 十进制除法:
decimal operator /(decimal x, decimal y);
求余运算符
对于表单的操作x % y
,应用二元运算符重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义的余数运算符。运算符都计算x
和之间的余数y
。
-
整数余数:
1 int operator %(int x, int y); 2 uint operator %(uint x, uint y); 3 long operator %(long x, long y); 4 ulong operator %(ulong x, ulong y);
结果x % y
是产生的价值x - (x / y) * y
。如果y
为零,System.DivideByZeroException
则抛出a。
如果左操作数是最小值int
或long
值而右操作数是-1
,System.OverflowException
则抛出a。在任何情况下都不会x % y
抛出x / y
不会引发异常的异常。
- 浮点余数:
1 float operator %(float x, float y); 2 double operator %(double x, double y);
下表列出了非零有限值,零,无穷大和NaN的所有可能组合的结果。在表中,x
并且y
是正的有限值。z
是和的结果,x % y
计算为x - n * y
,其中n
是小于或等于的最大可能整数x / y
。这种计算余数的方法类似于用于整数操作数的方法,但不同于IEEE 754定义(其中n
最接近的整数x / y
)。
+y | -y | +0 | -0 | +inf | -inf | NaN | |
+x | +z | +z | NaN | NaN | x | x | NaN |
-x | -z | -z | NaN | NaN | -x | -x | NaN |
+0 | +0 | +0 | NaN | NaN | +0 | +0 | NaN |
-0 | -0 | -0 | NaN | NaN | -0 | -0 | NaN |
+inf | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
-inf | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
- 十进制余数:
decimal operator %(decimal x, decimal y);
如果右操作数的值为零,System.DivideByZeroException
则抛出a。在任何舍入之前,结果的比例是两个操作数的比例中的较大者,并且结果的符号(如果非零)与该操作符的相同x
。
十进制余数等效于使用类型的余数运算符System.Decimal
。
加法运算符
对于表单的操作x + y
,应用二元运算符重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义的加法运算符。对于数字和枚举类型,预定义的加法运算符计算两个操作数的总和。当一个或两个操作数的类型为字符串时,预定义的加法运算符会连接操作数的字符串表示形式。
-
整数加法:
1 int operator +(int x, int y); 2 uint operator +(uint x, uint y); 3 long operator +(long x, long y); 4 ulong operator +(ulong x, ulong y);
在checked
上下文中,如果总和超出结果类型的范围,System.OverflowException
则抛出a。在unchecked
上下文中,不报告溢出,并且丢弃结果类型范围之外的任何重要高位。
- 浮点加法:
1 float operator +(float x, float y); 2 double operator +(double x, double y);
总和根据IEEE 754算法的规则计算。下表列出了非零有限值,零,无穷大和NaN的所有可能组合的结果。在表中,x
并且y
是非零有限值,并且z
是结果x + y
。如果x
且y
具有相同的幅度但相反的符号,z
则为正零。如果x + y
太大而无法在目标类型中表示,z
则为具有相同符号的无穷大x + y
。
y | +0 | -0 | +inf | -inf | NaN | |
x | z | x | x | +inf | -inf | NaN |
+0 | y | +0 | +0 | +inf | -inf | NaN |
-0 | y | +0 | -0 | +inf | -inf | NaN |
+inf | +inf | +inf | +inf | +inf | NaN | NaN |
-inf | -inf | -inf | -inf | NaN | -inf | NaN |
NaN | NaN | NaN | NaN | NaN | NaN | NaN |
- 十进制加法:
decimal operator +(decimal x, decimal y);
如果结果值太大而无法以decimal
格式表示,System.OverflowException
则抛出a。在任何舍入之前,结果的比例是两个操作数的比例中的较大者。
十进制加法等效于使用类型的加法运算符System.Decimal
。
- 枚举加法。每个枚举类型都隐式提供以下预定义运算符,其中
E
是枚举类型,并且U
是以下基础类型E
:
1 E operator +(E x, U y); 2 E operator +(U x, E y);
在运行时,这些运算符的计算结果完全相同(E)((U)x + (U)y)
。
- 字符串连接:
1 string operator +(string x, string y); 2 string operator +(string x, object y); 3 string operator +(object x, string y);
二元+
运算符的这些重载执行字符串连接。如果字符串连接的操作数是null
,则替换空字符串。否则,通过调用ToString
从类型继承的虚方法,将任何非字符串参数转换为其字符串表示形式object
。如果ToString
返回null
,则替换空字符串。
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 string s = null; 7 Console.WriteLine("s = >" + s + "<"); // displays s = >< 8 int i = 1; 9 Console.WriteLine("i = " + i); // displays i = 1 10 float f = 1.2300E+15F; 11 Console.WriteLine("f = " + f); // displays f = 1.23E+15 12 decimal d = 2.900m; 13 Console.WriteLine("d = " + d); // displays d = 2.900 14 } 15 }
字符串连接运算符的结果是一个字符串,其中包含左操作数的字符,后跟右操作数的字符。字符串连接运算符永远不会返回null
值。System.OutOfMemoryException
如果没有足够的可用内存来分配结果字符串,则可能抛出A.
- 代表组合。每个委托类型都隐式提供以下预定义运算符,其中
D
是委托类型:
D operator +(D x, D y);
+
当两个操作数都属于某种委托类型时,二元运算符执行委托组合D
。(如果操作数具有不同的委托类型,则会发生绑定时错误。)如果第一个操作数是null
,则操作的结果是第二个操作数的值(即使它也是null
)。否则,如果第二个操作数是null
,则操作的结果是第一个操作数的值。否则,操作的结果是一个新的委托实例,在调用时,调用第一个操作数,然后调用第二个操作数。有关委托组合的示例,请参阅减法运算符和委托调用。由于System.Delegate
不是委托类型,operator
+
因此未定义委托类型。
减法运算符
对于表单的操作x - y
,应用二元运算符重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义的减法运算符。运算符都减去y
了x
。
-
整数减法:
1 int operator -(int x, int y); 2 uint operator -(uint x, uint y); 3 long operator -(long x, long y); 4 ulong operator -(ulong x, ulong y);
在checked
上下文中,如果差异超出结果类型的范围,System.OverflowException
则抛出a。在unchecked
上下文中,不报告溢出,并且丢弃结果类型范围之外的任何重要高位。
- 浮点减法:
1 float operator -(float x, float y); 2 double operator -(double x, double y);
根据IEEE 754算法的规则计算差异。下表列出了非零有限值,零,无穷大和NaN的所有可能组合的结果。在表中,x
并且y
是非零有限值,并且z
是结果x - y
。如果x
并且y
相等,z
则为正零。如果x - y
太大而无法在目标类型中表示,z
则为具有相同符号的无穷大x - y
。
NaN | y | +0 | -0 | +inf | -inf | NaN |
x | z | x | x | -inf | +inf | NaN |
+0 | -y | +0 | +0 | -inf | +inf | NaN |
-0 | -y | -0 | +0 | -inf | +inf | NaN |
+inf | +inf | +inf | +inf | NaN | +inf | NaN |
-inf | -inf | -inf | -inf | -inf | NaN | NaN |
NaN | NaN | NaN | NaN | NaN | NaN | NaN |
- 十进制减法:
decimal operator -(decimal x, decimal y);
如果结果值太大而无法以decimal
格式表示,System.OverflowException
则抛出a。在任何舍入之前,结果的比例是两个操作数的比例中的较大者。
十进制减法等效于使用类型的减法运算符System.Decimal
。
- 枚举减法。每个枚举类型都隐式提供以下预定义运算符,其中
E
是枚举类型,并且U
是以下基础类型E
:
U operator -(E x, E y);
该运算符的评估完全如下(U)((U)x - (U)y)
。换句话说,操作者计算的序值之间的差x
和y
,并且将结果的类型是基础类型枚举的。
E operator -(E x, U y);
该运算符的评估完全如下(E)((U)x - y)
。换句话说,运算符从枚举的基础类型中减去一个值,产生一个枚举值。
- 代表删除。每个委托类型都隐式提供以下预定义运算符,其中
D
是委托类型:
D operator -(D x, D y);
-
当两个操作数都属于某种委托类型时,二元运算符执行委托删除D
。如果操作数具有不同的委托类型,则会发生绑定时错误。如果第一个操作数是null
,则操作的结果是null
。否则,如果第二个操作数是null
,则操作的结果是第一个操作数的值。否则,两个操作数都表示调用列表(委托声明)具有一个或多个条目,结果是一个新的调用列表,包含第一个操作数的列表,第二个操作数的条目从中删除,前提是第二个操作数的列表是第一个操作数列表的正确连续子列表。(要确定子列表相等性,请将相应的条目与委托相等运算符(委托相等运算符)进行比较。)否则,结果是左操作数的值。在这个过程中,两个操作数的列表都没有改变。如果第二个操作数的列表与第一个操作数列表中的多个连续条目的子列表匹配,则删除连续条目的最右侧匹配子列表。如果删除导致空列表,则结果为null
。例如:
1 delegate void D(int x); 2 3 class C 4 { 5 public static void M1(int i) { /* ... */ } 6 public static void M2(int i) { /* ... */ } 7 } 8 9 class Test 10 { 11 static void Main() { 12 D cd1 = new D(C.M1); 13 D cd2 = new D(C.M2); 14 D cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1 15 cd3 -= cd1; // => M1 + M2 + M2 16 17 cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1 18 cd3 -= cd1 + cd2; // => M2 + M1 19 20 cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1 21 cd3 -= cd2 + cd2; // => M1 + M1 22 23 cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1 24 cd3 -= cd2 + cd1; // => M1 + M2 25 26 cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1 27 cd3 -= cd1 + cd1; // => M1 + M2 + M2 + M1 28 } 29 }
移位运算符
<<
和>>
运算符用于执行比特移位操作。
1 shift_expression 2 : additive_expression 3 | shift_expression '<<' additive_expression 4 | shift_expression right_shift additive_expression 5 ;
如果shift_expression的操作数具有编译时类型dynamic
,则表达式是动态绑定的(动态绑定)。在这种情况下,表达式的编译时类型是dynamic
,并且下面描述的解析将在运行时使用具有编译时类型的那些操作数的运行时类型进行dynamic
。
对于表单的操作,x << count
或应用x >> count
二元运算符重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
声明重载的移位运算符时,第一个操作数的类型必须始终是包含运算符声明的类或结构,并且第二个操作数的类型必须始终为int
。
下面列出了预定义的移位运算符。
-
向左移:
1 int operator <<(int x, int count); 2 uint operator <<(uint x, int count); 3 long operator <<(long x, int count); 4 ulong operator <<(ulong x, int count);
- 右移:
1 int operator >>(int x, int count); 2 uint operator >>(uint x, int count); 3 long operator >>(long x, int count); 4 ulong operator >>(ulong x, int count);
>>
操作者移动x
右通过许多如下所述计算的比特。
当x
类型为int
或者long
,低位比特x
被丢弃时,剩余的比特向右移位,并且如果x
非负数则将高阶空比特位置设置为零,如果是负数则设置为1 x
。
当x
类型为uint
或者ulong
,低位比特x
被丢弃时,剩余的比特向右移位,并且高阶空比特位置被设置为零。
对于预定义的运算符,要移位的位数计算如下:
- 当类型
x
为int
或时uint
,移位计数由低位五位给出count
。换句话说,移位计数是从count & 0x1F
。 - 当类型
x
为long
或时ulong
,移位计数由低位六位给出count
。换句话说,移位计数是从count & 0x3F
。
如果得到的移位计数为零,则移位运算符只返回值x
。
移位操作不会导致溢出,并产生相同的结果checked
和unchecked
背景。
当运算>>
符的左操作数是有符号整数类型时,运算符执行算术右移,其中操作数的最高有效位(符号位)的值传播到高位空位位置。当运算>>
符的左操作数是无符号整数类型时,运算符执行逻辑右移,其中高位空位位置总是设置为零。要执行从操作数类型推断的相反操作,可以使用显式强制转换。例如,如果x
是类型的变量int
,则操作unchecked((int)((uint)x >> y))
执行逻辑右移x
。
关系型和类型测试运算符
==
,!=
,<
,>
,<=
,>=
,is
和as
运算符被称为关系和类型测试的运算符。
1 relational_expression 2 : shift_expression 3 | relational_expression '<' shift_expression 4 | relational_expression '>' shift_expression 5 | relational_expression '<=' shift_expression 6 | relational_expression '>=' shift_expression 7 | relational_expression 'is' type 8 | relational_expression 'as' type 9 ; 10 11 equality_expression 12 : relational_expression 13 | equality_expression '==' relational_expression 14 | equality_expression '!=' relational_expression 15 ;
is
描述在操作者的操作者是与as
操作者所描述的作为操作者。
的==
,!=
,<
,>
,<=
和>=
运算符都比较运算。
如果比较运算符的操作数具有编译时类型dynamic
,则表达式是动态绑定的(动态绑定)。在这种情况下,表达式的编译时类型是dynamic
,并且下面描述的解析将在运行时使用具有编译时类型的那些操作数的运行时类型进行dynamic
。
对于x
op 形式的操作y
,其中op是比较运算符,应用重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
以下各节介绍了预定义的比较运算符。所有预定义的比较运算符都返回类型的结果,bool
如下表所述。
操作 | 结果 |
---|---|
x == y |
true 如果x 等于y ,false 否则 |
x != y |
true 如果x 不相等y ,false 否则 |
x < y |
true 如果x 小于y ,false 否则 |
x > y |
true 如果x 大于y ,false 否则 |
x <= y |
true 如果x 小于或等于y ,false 否则 |
x >= y |
true 如果x 大于或等于y ,false 否则 |
整数比较运算符
预定义的整数比较运算符是:
1 bool operator ==(int x, int y); 2 bool operator ==(uint x, uint y); 3 bool operator ==(long x, long y); 4 bool operator ==(ulong x, ulong y); 5 6 bool operator !=(int x, int y); 7 bool operator !=(uint x, uint y); 8 bool operator !=(long x, long y); 9 bool operator !=(ulong x, ulong y); 10 11 bool operator <(int x, int y); 12 bool operator <(uint x, uint y); 13 bool operator <(long x, long y); 14 bool operator <(ulong x, ulong y); 15 16 bool operator >(int x, int y); 17 bool operator >(uint x, uint y); 18 bool operator >(long x, long y); 19 bool operator >(ulong x, ulong y); 20 21 bool operator <=(int x, int y); 22 bool operator <=(uint x, uint y); 23 bool operator <=(long x, long y); 24 bool operator <=(ulong x, ulong y); 25 26 bool operator >=(int x, int y); 27 bool operator >=(uint x, uint y); 28 bool operator >=(long x, long y); 29 bool operator >=(ulong x, ulong y);
这些运算符的比较两个整数的操作数的数字值,并返回一个bool
值,该值指示特定关系是否true
或false
。
浮点比较运算符
预定义的浮点比较运算符是:
1 bool operator ==(float x, float y); 2 bool operator ==(double x, double y); 3 4 bool operator !=(float x, float y); 5 bool operator !=(double x, double y); 6 7 bool operator <(float x, float y); 8 bool operator <(double x, double y); 9 10 bool operator >(float x, float y); 11 bool operator >(double x, double y); 12 13 bool operator <=(float x, float y); 14 bool operator <=(double x, double y); 15 16 bool operator >=(float x, float y); 17 bool operator >=(double x, double y);
运算符根据IEEE 754标准的规则比较操作数:
- 如果任一操作数是NaN,则结果适用
false
于除!=
结果之外的所有运算符true
。对于任何两个操作数,x != y
总是产生与之相同的结果!(x == y)
。然而,当一个或两个操作数是NaN,则<
,>
,<=
,和>=
运算符不产生相同的结果为相反运算符的逻辑非。例如,如果任一的x
和y
是NaN,则x < y
就是false
,但!(x >= y)
是true
。 -
当两个操作数都不是NaN时,运算符将两个浮点操作数的值与排序进行比较
-inf < -max < ... < -min < -0.0 == +0.0 < +min < ... < +max < +inf
-
其中
min
和max
是可以用给定浮点格式表示的最小和最大正有限值。这种排序的显着效果是:- 负和正零被认为是相等的。
- 负无穷大被认为小于所有其他值,但等于另一个负无穷大。
- 正无穷大被认为大于所有其他值,但等于另一个正无穷大。
十进制比较运算符
预定义的十进制比较运算符是:
1 bool operator ==(decimal x, decimal y); 2 bool operator !=(decimal x, decimal y); 3 bool operator <(decimal x, decimal y); 4 bool operator >(decimal x, decimal y); 5 bool operator <=(decimal x, decimal y); 6 bool operator >=(decimal x, decimal y);
这些运算符中的每一个都比较两个十进制操作数的bool
数值,并返回一个值,该值指示特定关系是否为true
或false
。每个十进制比较等效于使用类型的对应关系或等式运算符System.Decimal
。
布尔等式运算符
预定义的布尔等式运算符是:
1 bool operator ==(bool x, bool y); 2 bool operator !=(bool x, bool y);
结果==
是true
,如果这两个x
和y
是true
或者两者x
并y
有false
。否则,结果是false
。
结果!=
是false
,如果这两个x
和y
是true
或者两者x
并y
有false
。否则,结果是true
。当操作数是类型时bool
,!=
运算符产生与运算符相同的结果^
。
枚举比较运算符
每个枚举类型都隐式提供以下预定义比较运算符:
1 bool operator ==(E x, E y); 2 bool operator !=(E x, E y); 3 bool operator <(E x, E y); 4 bool operator >(E x, E y); 5 bool operator <=(E x, E y); 6 bool operator >=(E x, E y);
引用类型相等运算符
预定义的引用类型相等运算符是:
1 bool operator ==(object x, object y); 2 bool operator !=(object x, object y);
运算符返回比较两个引用的结果是否相等或不相等。
由于预定义的引用类型相等运算符接受类型的操作数object
,因此它们适用于所有未声明适用的类型operator ==
和operator !=
成员。相反,任何适用的用户定义的相等运算符都有效地隐藏了预定义的引用类型相等运算符。
预定义的引用类型相等运算符需要以下之一:
- 两个操作数都是已知为reference_type或文字的类型的值
null
。此外,从操作数的类型到另一个操作数的类型存在显式引用转换(显式引用转换)。 - 一个操作数是type的值,
T
其中T
是type_parameter而另一个操作数是literalnull
。此外T
,没有值类型约束。
除非其中一个条件成立,否则会发生绑定时错误。这些规则的显着含义是:
- 使用预定义的引用类型相等运算符来比较两个已知在绑定时不同的引用是一个绑定时错误。例如,如果操作数的结合时类型是两种类类型
A
和B
,如果既不A
也不B
从其它派生,那么这将是不可能的两个操作数来引用相同的对象。因此,该操作被认为是绑定时错误。 - 预定义的引用类型相等运算符不允许比较值类型操作数。因此,除非结构类型声明其自己的相等运算符,否则无法比较该结构类型的值。
- 预定义的引用类型相等运算符永远不会导致其操作数发生装箱操作。执行这样的装箱操作是没有意义的,因为对新分配的装箱实例的引用必然不同于所有其他引用。
- 如果
T
比较类型参数类型的操作数null
,并且运行时类型T
是值类型,则比较结果为false
。
以下示例检查无约束类型参数类型的参数是否为null
。
1 class C<T> 2 { 3 void F(T x) { 4 if (x == null) throw new ArgumentNullException(); 5 ... 6 } 7 }
该x == null
构建体被允许即使T
可以表示的值的类型,并且将结果简单地定义为false
当T
为值类型。
对于表单的操作,x == y
或者x != y
,如果适用operator ==
或operator !=
存在,操作符重载决策(二元运算符重载决策)规则将选择该运算符而不是预定义的引用类型相等运算符。但是,始终可以通过显式地将一个或两个操作数强制转换为类型来选择预定义的引用类型相等运算符object
。这个例子
1 using System; 2 3 class Test 4 { 5 static void Main() { 6 string s = "Test"; 7 string t = string.Copy(s); 8 Console.WriteLine(s == t); 9 Console.WriteLine((object)s == t); 10 Console.WriteLine(s == (object)t); 11 Console.WriteLine((object)s == (object)t); 12 } 13 }
产生输出
1 True 2 False 3 False 4 False
的s
和t
的变量是指两个不同的string
包含相同字符的实例。第一个比较输出True
是因为当两个操作数都是类型时,选择预定义的字符串相等运算符(字符串相等运算符)string
。剩余的比较全部输出,False
因为当一个或两个操作数是类型时,选择预定义的引用类型相等运算符object
。
请注意,上述技术对值类型没有意义。这个例子
1 class Test 2 { 3 static void Main() { 4 int i = 123; 5 int j = 123; 6 System.Console.WriteLine((object)i == (object)j); 7 } 8 }
输出,False
因为强制转换创建对两个单独的装箱int
值实例的引用。
字符串相等运算符
预定义的字符串相等运算符是:
1 bool operator ==(string x, string y); 2 bool operator !=(string x, string y);
string
如果满足以下条件之一,则认为两个值相等:
- 两个值都是
null
。 - 这两个值都是对字符串实例的非空引用,这些实例在每个字符位置具有相同的长度和相同的字符。
字符串相等运算符比较字符串值而不是字符串引用。当两个单独的字符串实例包含完全相同的字符序列时,字符串的值相等,但引用不同。如引用类型相等运算符中所述,引用类型相等运算符可用于比较字符串引用而不是字符串值。
委托比较运算符
每个委托类型都隐式提供以下预定义比较运算符:
1 bool operator ==(System.Delegate x, System.Delegate y); 2 bool operator !=(System.Delegate x, System.Delegate y);
两个代表实例被认为是如下:
- 如果任一委托实例是
null
,则当且仅当两者都相同时,它们是相等的null
。 - 如果代表具有不同的运行时类型,则它们永远不会相等。
- 如果两个委托实例都有一个调用列表(委托声明),那么这些实例是相等的,当且仅当它们的调用列表长度相同,并且一个调用列表中的每个条目与相应的条目相等(如下定义),按顺序,在另一个的调用列表中。
以下规则控制调用列表条目的相等性:
- 如果两个调用列表条目都引用相同的静态方法,则条目相等。
- 如果两个调用列表条目都引用同一目标对象上的相同非静态方法(由引用相等运算符定义),则条目相等。
- 通过评估具有相同(可能为空)的捕获的外部变量实例集的语义相同的anonymous_method_expression s或lambda_expression s 生成的调用列表条目是允许的(但不是必需的)是相等的。
等于运算符和null
的==
和!=
运算符允许一个操作数是一个空类型的值,另一个是null
文字,即使没有预定义的或用户定义的运算符(在未提升或提升形式)存在的操作。
对于其中一种形式的操作
1 x == null 2 null == x 3 x != null 4 null != x
is运算符
的is
操作者用于动态检查是否一个对象的运行时类型是与给定类型兼容。操作的结果E is T
,其中E
是表达式并且T
是类型,是一个布尔值,指示是否E
可以T
通过引用转换,装箱转换或取消装箱转换成功转换为类型。在将类型参数替换为所有类型参数之后,将按如下方式评估操作:
- 如果
E
是匿名函数,则发生编译时错误 - 如果
E
是方法组或null
文字,如果类型E
是引用类型或可空类型且值为E
null,则结果为false。 - 否则,让我们
D
表示动态类型E
如下:- 如果类型
E
是引用类型,D
则是实例引用的运行时类型E
。 - 如果类型
E
是可空类型,D
则是该可空类型的基础类型。 - 如果类型
E
是非可空值类型,D
则类型为E
。
- 如果类型
- 操作的结果取决于
D
与T
如下:- 如果
T
是引用类型,其结果是,如果真D
和T
是相同的类型,如果D
是引用类型和从隐式引用转换D
到T
存在,或者如果D
是一个值类型和从一个装箱转换D
到T
存在。 - 如果
T
是可空类型,则结果为true,如果D
是基础类型T
。 - 如果
T
是非可空值类型,则结果为true,如果D
且T
是相同类型。 - 否则,结果为false。
- 如果
请注意,is
运算符不会考虑用户定义的转换。
as运算符
的as
运算符用于一个值显式转换为给定的参考类型或空类型。与强制转换表达式(Cast表达式)不同,as
运算符永远不会抛出异常。相反,如果无法指示转换,则结果值为null
。
在表单的操作中E as T
,E
必须是表达式,并且T
必须是引用类型,已知为引用类型的类型参数或可空类型。此外,必须至少满足下列条件之一,否则会发生编译时错误:
- 身份(身份转换),隐式可空(隐式可空转换),隐式引用(隐式引用转换),装箱(装箱转换),显式可空(显式可空转换),显式引用(显式引用转换)或取消装箱(取消装箱转换))转换从存在
E
到T
。 - 类型
E
或是T
开放类型。 E
是null
文字。
如果编译时类型E
不是dynamic
,则操作E as T
产生与之相同的结果
E is T ? (T)(E) : (T)null
除了E
仅评估一次。可以期望编译器优化E as T
以执行至多一个动态类型检查,而不是上面的扩展所暗示的两个动态类型检查。
如果编译时类型E
是dynamic
,则与强制转换运算as
符不同,运算符不是动态绑定的(动态绑定)。因此,在这种情况下的扩展是:
E is T ? (T)(object)(E) : (T)null
请注意,某些转换(例如用户定义的转换)对于as
运算符是不可能的,而应使用强制转换表达式执行。
在这个例子中
1 class X 2 { 3 4 public string F(object o) { 5 return o as string; // OK, string is a reference type 6 } 7 8 public T G<T>(object o) where T: Attribute { 9 return o as T; // Ok, T has a class constraint 10 } 11 12 public U H<U>(object o) { 13 return o as U; // Error, U is unconstrained 14 } 15 }
类型参数T
的G
已知为引用类型,因为它具有类约束。类型参数U
的H
不然而; 因此不允许使用as
运算符H
。
逻辑运算符
&
,^
和|
运算符称为逻辑运算符。
1 and_expression 2 : equality_expression 3 | and_expression '&' equality_expression 4 ; 5 6 exclusive_or_expression 7 : and_expression 8 | exclusive_or_expression '^' and_expression 9 ; 10 11 inclusive_or_expression 12 : exclusive_or_expression 13 | inclusive_or_expression '|' exclusive_or_expression 14 ;
如果逻辑运算符的操作数具有编译时类型dynamic
,则表达式是动态绑定的(动态绑定)。在这种情况下,表达式的编译时类型是dynamic
,并且下面描述的解析将在运行时使用具有编译时类型的那些操作数的运行时类型进行dynamic
。
对于表单的操作x op y
,其中op
一个是逻辑运算符,应用重载决策(二元运算符重载决策)来选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
以下各节介绍了预定义的逻辑运算符。
整数逻辑运算符
预定义的整数逻辑运算符是:
1 int operator &(int x, int y); 2 uint operator &(uint x, uint y); 3 long operator &(long x, long y); 4 ulong operator &(ulong x, ulong y); 5 6 int operator |(int x, int y); 7 uint operator |(uint x, uint y); 8 long operator |(long x, long y); 9 ulong operator |(ulong x, ulong y); 10 11 int operator ^(int x, int y); 12 uint operator ^(uint x, uint y); 13 long operator ^(long x, long y); 14 ulong operator ^(ulong x, ulong y);
该&
运算符计算按位逻辑AND
的两个操作数的|
运算符计算按位逻辑OR
的两个操作数,而^
运算符计算按位逻辑独占OR
两个操作数。这些操作不会溢出。
枚举逻辑运算符
每个枚举类型都E
隐式提供以下预定义逻辑运算符:
1 E operator &(E x, E y); 2 E operator |(E x, E y); 3 E operator ^(E x, E y);
评估的结果x op y
,其中x
和y
是E
具有基础类型的枚举类型的表达式U
,并且op
是逻辑运算符之一,与评估完全相同(E)((U)x op (U)y)
。换句话说,枚举类型逻辑运算符只是对两个操作数的基础类型执行逻辑运算。
布尔逻辑运算符
预定义的布尔逻辑运算符是:
1 bool operator &(bool x, bool y); 2 bool operator |(bool x, bool y); 3 bool operator ^(bool x, bool y);
结果x & y
是true
,如果这两个x
和y
的true
。否则,结果是false
。
结果x | y
是true
,如果任一x
或者y
是true
。否则,结果是false
。
结果x ^ y
是true
,如果x
是true
和y
是false
,或者x
是false
和y
是true
。否则,结果是false
。当操作数是类型时bool
,^
运算符计算与运算符相同的结果!=
。
可为空的布尔逻辑运算符
可空布尔类型bool?
可表示三个值,true
,false
,和null
,并在概念上类似于用于SQL布尔表达式的三值类型。为了确保操作符&
和|
操作符生成的结果bool?
与SQL的三值逻辑一致,提供了以下预定义运算符:
1 bool? operator &(bool? x, bool? y); 2 bool? operator |(bool? x, bool? y);
下表列出了由这些操作符的值的所有组合产生的结果true
,false
和null
。
| x
| y
| x & y
| x | y
| |:-------:|:-------:|:-------:|:-------:| | true
| true
| true
| true
| | true
| false
| false
| true
| | true
| null
| null
| true
| | false
| true
| false
| true
| | false
| false
| false
| false
| | false
| null
| false
| null
| | null
| true
| null
| true
| | null
| false
| false
| null
| | null
| null
| null
| null
|
条件逻辑运算符
在&&
与||
运算符被称为条件逻辑运算符。它们也被称为“短路”逻辑运算符。
1 conditional_and_expression 2 : inclusive_or_expression 3 | conditional_and_expression '&&' inclusive_or_expression 4 ; 5 6 conditional_or_expression 7 : conditional_and_expression 8 | conditional_or_expression '||' conditional_and_expression 9 ;
在&&
和||
运算符都是有条件的版本&
和|
运算符:
- 该操作
x && y
对应于操作x & y
,除非y
仅在x
不进行评估时进行评估false
。 - 该操作
x || y
对应于操作x | y
,除非y
仅在x
不进行评估时进行评估true
。
如果条件逻辑运算符的操作数具有编译时类型dynamic
,则表达式是动态绑定的(动态绑定)。在这种情况下,表达式的编译时类型是dynamic
,并且下面描述的解析将在运行时使用具有编译时类型的那些操作数的运行时类型进行dynamic
。
形式的操作x && y
或x || y
通过施加过载的分辨率(处理二进制符重载解析)的操作是否被写入x & y
或x | y
。然后,
- 如果重载决策无法找到单个最佳运算符,或者重载决策选择了预定义的整数逻辑运算符之一,则会发生绑定时错误。
- 否则,如果所选运算符是预定义的布尔逻辑运算符(布尔逻辑运算符)或可空布尔逻辑运算符(可空布尔逻辑运算符)之一,则按布尔条件逻辑运算符中的描述处理该运算。
- 否则,所选运算符是用户定义的运算符,并按照用户定义的条件逻辑运算符中的描述处理该运算。
不可能直接重载条件逻辑运算符。但是,因为条件逻辑运算符是根据常规逻辑运算符计算的,所以常规逻辑运算符的重载在某些限制下也会被视为条件逻辑运算符的重载。这在用户定义的条件逻辑运算符中进一步描述。
布尔条件逻辑运算符
当操作数为&&
或者||
是类型bool
的操作数时,或者操作数是没有定义适用的类型,operator &
或者operator |
确实定义了隐式转换时bool
,操作按如下方式处理:
- 该操作
x && y
被评估为x ? y : false
。换句话说,x
首先评估并转换为类型bool
。然后,如果x
是true
,y
则评估并转换为类型bool
,这将成为操作的结果。否则,操作的结果是false
。 - 该操作
x || y
被评估为x ? true : y
。换句话说,x
首先评估并转换为类型bool
。然后,如果x
是true
,则操作的结果是true
。否则,y
将评估并转换为类型bool
,这将成为操作的结果。
用户定义的条件逻辑运算符
当的操作数&&
或||
是其声明类型的适用的用户定义operator &
或operator |
,两个以下必须是真实的,其中T
是在其中所选择的算被声明的类型:
- 必须是所选运算符的返回类型和每个参数的类型
T
。换句话说,运算符必须计算两个类型操作数的逻辑AND
或逻辑,并且必须返回类型的结果。OR
T
T
T
必须包含operator true
和的声明operator false
。
如果不满足这些要求中的任何一个,则会发生绑定时错误。否则,&&
或||
操作通过组合评估的用户定义的operator true
或operator false
与所选择的用户定义的运算:
- 该操作
x && y
被评估为T.false(x) ? x : T.&(x, y)
,其中T.false(x)
是对所operator false
声明的in 的调用T
,并且T.&(x, y)
是对所选操作的调用operator &
。换句话说,x
首先评估并operator false
在结果上调用以确定是否x
肯定是假的。然后,如果x
肯定是假的,则操作的结果是先前计算的值x
。否则,y
对其进行求值,并对operator &
先前计算x
的值和为计算的值进行调用y
以生成操作的结果。 - 该操作
x || y
被评估为T.true(x) ? x : T.|(x, y)
,其中T.true(x)
是对所operator true
声明的in 的调用T
,并且T.|(x,y)
是对所选操作的调用operator|
。换句话说,x
首先评估并operator true
在结果上调用以确定是否x
肯定为真。然后,如果x
肯定是真的,则操作的结果是先前计算的值x
。否则,y
对其进行求值,并对operator |
先前计算x
的值和为计算的值进行调用y
以生成操作的结果。
在这些操作中的任何一个中,给定的表达式x
仅被评估一次,并且给定的表达式y
不被评估或仅被评估一次。
对于实施的类型的示例operator true
和operator false
,见数据库的布尔类型。
空合并运算符
该??
运算符称为空合并运算符。
1 null_coalescing_expression 2 : conditional_or_expression 3 | conditional_or_expression '??' null_coalescing_expression 4 ;
表单的空合并表达式a ?? b
需要a
是可空类型或引用类型。如果a
是非null,则结果a ?? b
为a
; 否则,结果是b
。b
仅当a
null为null时,操作才会计算。
空合并运算符是右关联运算符,这意味着操作从右到左分组。例如,表单的表达式a ?? b ?? c
被评估为a ?? (b ?? c)
。一般而言,表单的表达式E1 ?? E2 ?? ... ?? En
返回非空的第一个操作数,如果所有操作数都为null,则返回null。
表达式的类型a ?? b
取决于操作数上可用的隐式转换。按优先顺序排列,所述类型的a ?? b
IS A0
,A
或B
,其中A
是的式a
(前提是a
具有类型),B
是的类型b
(其中,b
有一个类型)和A0
为基础类型的A
,如果A
是空类型,或者A
其他。具体来说,a ?? b
处理如下:
- 如果
A
存在且不是可空类型或引用类型,则发生编译时错误。 - 如果
b
是动态表达式,则结果类型为dynamic
。在运行时,a
首先进行评估。如果a
不为null,a
则转换为动态,这将成为结果。否则,b
进行评估,这就成了结果。 - 否则,如果
A
存在并且是可空类型并且存在来自b
to 的隐式转换A0
,则结果类型为A0
。在运行时,a
首先进行评估。如果a
不为null,a
则打开以进行类型化A0
,这将成为结果。否则,b
将评估并转换为类型A0
,这将成为结果。 - 否则,如果
A
存在并且从b
到存在隐式转换A
,则结果类型为A
。在运行时,a
首先进行评估。如果a
不为null,则a
成为结果。否则,b
将评估并转换为类型A
,这将成为结果。 - 否则,如果
b
有一个类型B
和的隐式转换从存在a
于B
,结果类型是B
。在运行时,a
首先进行评估。如果a
不为null,a
则解包为typeA0
(如果A
存在并且可以为空)并转换为typeB
,这将成为结果。否则,b
评估并成为结果。 - 否则,
a
并且b
不兼容,并发生编译时错误。
条件运算符
?:
运算符称为条件运算符。它有时也被称为三元运算符。
1 conditional_expression 2 : null_coalescing_expression 3 | null_coalescing_expression '?' expression ':' expression 4 ;
表单的条件表达式b ? x : y
首先评估条件b
。然后,如果b
是true
,x
则评估并成为操作的结果。否则,y
评估并成为操作的结果。条件表达式永远不会同时评估x
和y
。
条件运算符是右关联的,这意味着操作从右到左分组。例如,表单的表达式a ? b : c ? d : e
被评估为a ? b : (c ? d : e)
。
运算?:
符的第一个操作数必须是可以隐式转换为bool
的表达式,或者是实现的类型的表达式operator true
。如果这两个要求都不满足,则会发生编译时错误。
在第二和第三操作数,x
并且y
,所述的?:
操作者控制所述条件表达式的类型。
- 如果
x
有类型X
,那么y
有类型Y
- 如果一个隐式转换(隐式转换)从存在
X
于Y
,但不能从Y
到X
,然后Y
是条件表达式的类型。 - 如果一个隐式转换(隐式转换)从存在
Y
于X
,但不能从X
到Y
,然后X
是条件表达式的类型。 - 否则,不能确定表达式类型,并发生编译时错误。
- 如果一个隐式转换(隐式转换)从存在
- 如果只有一个
x
与y
具有类型,并且两个x
和y
,的是隐式转换为这种类型,那么这是条件表达式的类型。 - 否则,不能确定表达式类型,并发生编译时错误。
表单的条件表达式的运行时处理b ? x : y
包括以下步骤:
- 首先,
b
评估,并确定bool
值b
:- 如果从类型的隐式转换
b
到bool
存在,则这个隐式转换的执行,以产生bool
值。 - 否则,调用
operator true
由类型定义b
的bool
值来生成值。
- 如果从类型的隐式转换
- 如果
bool
上面步骤产生的值是true
,则x
评估并转换为条件表达式的类型,这将成为条件表达式的结果。 - 否则,
y
将评估并转换为条件表达式的类型,这将成为条件表达式的结果。
匿名函数表达式
一个匿名函数是一个表示“在线”方法定义的表达式。匿名函数本身没有值或类型,但可以转换为兼容的委托或表达式树类型。匿名函数转换的评估取决于转换的目标类型:如果它是委托类型,则转换将计算为引用匿名函数定义的方法的委托值。如果它是表达式树类型,则转换将计算为表达式树,该表达式树将方法的结构表示为对象结构。
由于历史原因,有两种匿名函数的语法风格,即lambda_expression s和anonymous_method_expressions。对于几乎所有目的,lambda_expression都比anonymous_method_expression更简洁和富有表现力,它们仍然保留在语言中以便向后兼容。
1 lambda_expression 2 : anonymous_function_signature '=>' anonymous_function_body 3 ; 4 5 anonymous_method_expression 6 : 'delegate' explicit_anonymous_function_signature? block 7 ; 8 9 anonymous_function_signature 10 : explicit_anonymous_function_signature 11 | implicit_anonymous_function_signature 12 ; 13 14 explicit_anonymous_function_signature 15 : '(' explicit_anonymous_function_parameter_list? ')' 16 ; 17 18 explicit_anonymous_function_parameter_list 19 : explicit_anonymous_function_parameter (',' explicit_anonymous_function_parameter)* 20 ; 21 22 explicit_anonymous_function_parameter 23 : anonymous_function_parameter_modifier? type identifier 24 ; 25 26 anonymous_function_parameter_modifier 27 : 'ref' 28 | 'out' 29 ; 30 31 implicit_anonymous_function_signature 32 : '(' implicit_anonymous_function_parameter_list? ')' 33 | implicit_anonymous_function_parameter 34 ; 35 36 implicit_anonymous_function_parameter_list 37 : implicit_anonymous_function_parameter (',' implicit_anonymous_function_parameter)* 38 ; 39 40 implicit_anonymous_function_parameter 41 : identifier 42 ; 43 44 anonymous_function_body 45 : expression 46 | block 47 ;
该=>
经营者具有相同的优先级分配(=
)和向右关联。
具有async
修饰符的匿名函数是异步函数,并遵循迭代器中描述的规则。
可以显式或隐式地键入lambda_expression形式的匿名函数的参数。在显式类型化参数列表中,明确说明了每个参数的类型。在隐式类型参数列表中,参数的类型是从匿名函数发生的上下文中推断出来的 - 具体来说,当匿名函数转换为兼容的委托类型或表达式树类型时,该类型提供参数类型(匿名)功能转换)。
在具有单个隐式类型参数的匿名函数中,可以从参数列表中省略括号。换句话说,表单的匿名函数
( param ) => expr
可以缩写为
param => expr
在形式的匿名函数的参数列表anonymous_method_expression是可选的。如果给定,则必须明确键入参数。如果不是,匿名函数可以转换为具有任何不包含out
参数的参数列表的委托。
阿块匿名函数的体是可到达的(结束点和可到达),除非不可达语句内发生匿名函数。
匿名函数的一些示例如下:
1 x => x + 1 // Implicitly typed, expression body 2 x => { return x + 1; } // Implicitly typed, statement body 3 (int x) => x + 1 // Explicitly typed, expression body 4 (int x) => { return x + 1; } // Explicitly typed, statement body 5 (x, y) => x * y // Multiple parameters 6 () => Console.WriteLine() // No parameters 7 async (t1,t2) => await t1 + await t2 // Async 8 delegate (int x) { return x + 1; } // Anonymous method expression 9 delegate { return 1 + 1; } // Parameter list omitted
lambda_expression s和anonymous_method_expression s 的行为是相同的,除了以下几点:
- anonymous_method_expression允许完全省略参数列表,从而产生可转换性以委托任何值参数列表的类型。
- lambda_expression s允许省略和推断参数类型,而anonymous_method_expression s需要明确声明参数类型。
- lambda_expression的主体可以是表达式或语句块,而anonymous_method_expression的主体必须是语句块。
- 只有lambda_expression才能转换为兼容的表达式树类型(表达式树类型)。
匿名函数签名
匿名函数的可选anonymous_function_signature定义匿名函数的形式参数的名称和可选类型。匿名函数的参数范围是anonymous_function_body。(范围)与参数列表(如果给定)一起,匿名方法体构成声明空间(声明)。因此,匿名函数的参数名称与本地变量,本地常量或其范围包括anonymous_method_expression或lambda_expression的参数的名称匹配是编译时错误。
如果匿名函数具有explicit_anonymous_function_signature,则兼容委托类型和表达式树类型的集合仅限于具有相同顺序的相同参数类型和修饰符的那些。与方法组转换(方法组转换)相反,不支持匿名函数参数类型的反方差。如果匿名函数没有anonymous_function_signature,则兼容委托类型和表达式树类型的集合仅限于那些没有out
参数的类型。
请注意,anonymous_function_signature不能包含属性或参数数组。然而,anonymous_function_signature可以与其参数列表包含参数数组的委托类型兼容。
另请注意,即使兼容,转换为表达式树类型仍可能在编译时失败(表达式树类型)。
匿名函数体
匿名函数的主体(表达式或块)遵循以下规则:
- 如果匿名函数包含签名,则签名中指定的参数在正文中可用。如果匿名函数没有签名,则可以将其转换为具有参数的委托类型或表达式类型(匿名函数转换),但无法在正文中访问这些参数。
- 除了最近的封闭匿名函数的签名(如果有)中指定的参数
ref
或out
参数外,正文访问ref
或out
参数是编译时错误。 - 当类型
this
是结构类型时,正是身体访问的编译时错误this
。这是真实的访问是否是显式的(如在this.x
)或隐式的(如在x
其中x
是该结构的一个实例成员)。此规则仅禁止此类访问,并且不会影响成员查找是否导致结构的成员。 - 正文可以访问匿名函数的外部变量(外部变量)。访问外部变量将引用在评估lambda_expression或anonymous_method_expression时激活的变量的实例(匿名函数表达式的评估)。
- 正文的编译时错误包含一个
goto
语句,break
语句或continue
语句,其目标位于正文之外或包含在匿名函数体内。 return
正文中的语句从最近的封闭匿名函数的调用返回控制,而不是从封闭的函数成员调用。语句中指定的表达式return
必须可隐式转换为最近的封闭lambda_expression或anonymous_method_expression转换为的委托类型或表达式树类型的返回类型(匿名函数转换)。
除了通过评估和调用lambda_expression或anonymous_method_expression之外,是否有任何方法可以执行匿名函数的块,这是明确未指定的。特别地,编译器可以选择通过合成一个或多个命名方法或类型来实现匿名函数。任何此类合成元素的名称必须是为编译器使用而保留的形式。
重载解析和匿名函数
参数列表中的匿名函数参与类型推断和重载解析。有关确切规则,请参阅类型推断和重载分辨率。
以下示例说明了匿名函数对重载决策的影响。
1 class ItemList<T>: List<T> 2 { 3 public int Sum(Func<T,int> selector) { 4 int sum = 0; 5 foreach (T item in this) sum += selector(item); 6 return sum; 7 } 8 9 public double Sum(Func<T,double> selector) { 10 double sum = 0; 11 foreach (T item in this) sum += selector(item); 12 return sum; 13 } 14 }
该ItemList<T>
课程有两种Sum
方法。每个都接受一个selector
参数,该参数从列表项中提取值以求和。提取的值可以是a int
或a double
,结果和也可以是a int
或a double
。
该Sum
方法可以例如用于从订单中的细节线列表计算总和。
1 class Detail 2 { 3 public int UnitCount; 4 public double UnitPrice; 5 ... 6 } 7 8 void ComputeSums() { 9 ItemList<Detail> orderDetails = GetOrderDetails(...); 10 int totalUnits = orderDetails.Sum(d => d.UnitCount); 11 double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount); 12 ... 13 }
在第一次调用时orderDetails.Sum
,两种Sum
方法都适用,因为匿名函数d => d. UnitCount
兼容Func<Detail,int>
和Func<Detail,double>
。但是,重载决策选择第Sum
一种方法,因为转换为转换Func<Detail,int>
为更好Func<Detail,double>
。
在第二次调用中orderDetails.Sum
,只有第二种Sum
方法适用,因为匿名函数d => d.UnitPrice * d.UnitCount
产生的值为type double
。因此,重载决策选择了Sum
该调用的第二种方法。
匿名函数和动态绑定
匿名函数不能是动态绑定操作的接收者,参数或操作数。
外部变量
范围包括lambda_expression或anonymous_method_expression的任何局部变量,值参数或参数数组称为匿名函数的外部变量。在类的实例函数成员中,该this
值被视为值参数,并且是函数成员中包含的任何匿名函数的外部变量。
捕获外部变量
当外部变量由匿名函数引用时,外部变量被称为已由匿名函数捕获。通常,局部变量的生命周期限于执行与之关联的块或语句(局部变量)。但是,捕获的外部变量的生命周期至少会延长,直到从匿名函数创建的委托或表达式树符合垃圾回收的条件。
在这个例子中
1 using System; 2 3 delegate int D(); 4 5 class Test 6 { 7 static D F() { 8 int x = 0; 9 D result = () => ++x; 10 return result; 11 } 12 13 static void Main() { 14 D d = F(); 15 Console.WriteLine(d()); 16 Console.WriteLine(d()); 17 Console.WriteLine(d()); 18 } 19 }
x
匿名函数捕获局部变量,并且生命周期x
至少延长,直到返回的委托F
变得有资格进行垃圾收集(直到程序结束才会发生)。由于匿名函数的每次调用都在同一个实例上运行x
,因此示例的输出为:
1 1 2 2 3 3
当匿名函数捕获局部变量或值参数时,局部变量或参数不再被视为固定变量(固定和可移动变量),而是被视为可移动变量。因此unsafe
,获取捕获的外部变量的地址的任何代码必须首先使用该fixed
语句来修复该变量。
请注意,与未捕获的变量不同,捕获的局部变量可以同时暴露给多个执行线程。
局部变量的实例化
当执行进入变量的范围时,认为局部变量被实例化。例如,当调用以下方法时x
,对于循环的每次迭代,局部变量被实例化并初始化三次 - 一次。
1 static void F() { 2 for (int i = 0; i < 3; i++) { 3 int x = i * 2 + 1; 4 ... 5 } 6 }
但是,移动x
循环外部的声明会导致单个实例化x
:
1 static void F() { 2 int x; 3 for (int i = 0; i < 3; i++) { 4 x = i * 2 + 1; 5 ... 6 } 7 }
当未捕获时,无法准确地观察局部变量实例化的频率 - 因为实例化的生命期是不相交的,所以每个实例化都可以简单地使用相同的存储位置。但是,当匿名函数捕获局部变量时,实例化的效果变得明显。
这个例子
1 using System; 2 3 delegate void D(); 4 5 class Test 6 { 7 static D[] F() { 8 D[] result = new D[3]; 9 for (int i = 0; i < 3; i++) { 10 int x = i * 2 + 1; 11 result[i] = () => { Console.WriteLine(x); }; 12 } 13 return result; 14 } 15 16 static void Main() { 17 foreach (D d in F()) d(); 18 } 19 }
产生输出:
1 1 2 3 3 5
但是,当声明x
移出循环时:
1 static D[] F() { 2 D[] result = new D[3]; 3 int x; 4 for (int i = 0; i < 3; i++) { 5 x = i * 2 + 1; 6 result[i] = () => { Console.WriteLine(x); }; 7 } 8 return result; 9 }
输出是:
1 5 2 5 3 5
如果for循环声明了一个迭代变量,那么该变量本身被认为是在循环之外声明的。因此,如果更改示例以捕获迭代变量本身:
1 static D[] F() { 2 D[] result = new D[3]; 3 for (int i = 0; i < 3; i++) { 4 result[i] = () => { Console.WriteLine(i); }; 5 } 6 return result; 7 }
只捕获迭代变量的一个实例,从而产生输出:
1 3 2 3 3 3
匿名函数委托可以共享一些捕获的变量,但具有其他的单独实例。例如,如果F
更改为
1 static D[] F() { 2 D[] result = new D[3]; 3 int x = 0; 4 for (int i = 0; i < 3; i++) { 5 int y = 0; 6 result[i] = () => { Console.WriteLine("{0} {1}", ++x, ++y); }; 7 } 8 return result; 9 }
这三个代理捕获相同x
但不同的实例y
,输出为:
1 1 1 2 2 1 3 3 1
单独的匿名函数可以捕获外部变量的相同实例。在示例中:
1 using System; 2 3 delegate void Setter(int value); 4 5 delegate int Getter(); 6 7 class Test 8 { 9 static void Main() { 10 int x = 0; 11 Setter s = (int value) => { x = value; }; 12 Getter g = () => { return x; }; 13 s(5); 14 Console.WriteLine(g()); 15 s(10); 16 Console.WriteLine(g()); 17 } 18 }
两个匿名函数捕获局部变量的相同实例,x
因此它们可以通过该变量“通信”。该示例的输出是:
1 5 2 10
评估匿名函数表达式
F
必须始终将匿名函数直接或通过执行委托创建表达式转换为委托类型D
或表达式树类型。此转换确定匿名函数的结果,如匿名函数转换中所述。E
new D(F)
查询表达式
查询表达式为查询提供了语言集成语法,类似于SQL和XQuery等关系和层次查询语言。
1 query_expression 2 : from_clause query_body 3 ; 4 5 from_clause 6 : 'from' type? identifier 'in' expression 7 ; 8 9 query_body 10 : query_body_clauses? select_or_group_clause query_continuation? 11 ; 12 13 query_body_clauses 14 : query_body_clause 15 | query_body_clauses query_body_clause 16 ; 17 18 query_body_clause 19 : from_clause 20 | let_clause 21 | where_clause 22 | join_clause 23 | join_into_clause 24 | orderby_clause 25 ; 26 27 let_clause 28 : 'let' identifier '=' expression 29 ; 30 31 where_clause 32 : 'where' boolean_expression 33 ; 34 35 join_clause 36 : 'join' type? identifier 'in' expression 'on' expression 'equals' expression 37 ; 38 39 join_into_clause 40 : 'join' type? identifier 'in' expression 'on' expression 'equals' expression 'into' identifier 41 ; 42 43 orderby_clause 44 : 'orderby' orderings 45 ; 46 47 orderings 48 : ordering (',' ordering)* 49 ; 50 51 ordering 52 : expression ordering_direction? 53 ; 54 55 ordering_direction 56 : 'ascending' 57 | 'descending' 58 ; 59 60 select_or_group_clause 61 : select_clause 62 | group_clause 63 ; 64 65 select_clause 66 : 'select' expression 67 ; 68 69 group_clause 70 : 'group' expression 'by' expression 71 ; 72 73 query_continuation 74 : 'into' identifier query_body 75 ;
查询表达式以from
子句开头,以select
或group
子句结束。初始from
子句可以跟随零个或多个from
,let
,where
,join
或orderby
条款。每个from
子句都是一个引入范围变量的生成器,该范围变量的范围超过序列的元素。每个let
子句引入一个范围变量,表示通过先前范围变量计算的值。每个where
子句都是一个从结果中排除项目的过滤器。每个join
子句将源序列的指定键与另一个序列的键进行比较,产生匹配对。每个或orderby
条款根据指定的标准重新排序项目。最后select
group
子句根据范围变量指定结果的形状。最后,into
通过将一个查询的结果视为后续查询中的生成器,可以使用子句来“拼接”查询。
查询表达式中的歧义
查询表达式包含许多“上下文关键字”,即在给定上下文中具有特殊含义的标识符。具体地,这些是from
,where
,join
,on
,equals
,into
,let
,orderby
,ascending
,descending
,select
,group
和by
。为了避免由于将这些标识符混合使用作为关键字或简单名称而导致的查询表达式含糊不清,这些标识符在查询表达式中的任何位置发生时都被视为关键字。
为此,查询表达式是以“ from identifier
” 开头,后跟除“ ;
”,“ =
”或“ ,
” 之外的任何标记的任何表达式。
为了在查询表达式中将这些单词用作标识符,它们可以以“ @
”(标识符)为前缀。
查询表达式翻译
C#语言未指定查询表达式的执行语义。相反,查询表达式被转换为符合查询表达式模式(查询表达式模式)的方法的调用。具体而言,查询表达式被转换成的命名方法的调用Where
,Select
,SelectMany
,Join
,GroupJoin
,OrderBy
,OrderByDescending
,ThenBy
,ThenByDescending
,GroupBy
,和Cast
预期。这些方法具有特定签名和结果类型,如在查询表达式模式。这些方法可以是被查询对象的实例方法,也可以是对象外部的扩展方法,它们实现查询的实际执行。
从查询表达式到方法调用的转换是在执行任何类型绑定或重载解析之前发生的语法映射。保证翻译在语法上是正确的,但不能保证生成语义正确的C#代码。在转换查询表达式之后,生成的方法调用将作为常规方法调用进行处理,这可能反过来发现错误,例如,如果方法不存在,参数类型错误,或者方法是通用的,并且类型推断失败。
通过重复应用以下转换来处理查询表达式,直到无法进一步缩减为止。翻译按应用顺序列出:每个部分假定前面部分中的翻译已经详尽地执行,并且一旦用尽,以后将不会在处理相同查询表达式时重新访问该部分。
查询表达式中不允许分配范围变量。但是,允许C#实现并不总是强制执行此限制,因为此处提供的语法转换方案有时可能无法实现。
某些翻译使用透明标识符注入范围变量*
。透明标识符的特殊性质在进一步讨论透明标识符。
select和group by子句与continuation
带延续的查询表达式
from ... into x ...
被翻译成
from x in ( from ... ) ...
以下部分中的翻译假定查询没有into
延续。
这个例子
1 from c in customers 2 group c by c.Country into g 3 select new { Country = g.Key, CustCount = g.Count() }
被翻译成
1 from g in 2 from c in customers 3 group c by c.Country 4 select new { Country = g.Key, CustCount = g.Count() }
其最终翻译是
1 customers. 2 GroupBy(c => c.Country). 3 Select(g => new { Country = g.Key, CustCount = g.Count() })
显式范围变量类型
from
明确指定了范围可变型子句
from T x in e
被翻译成
from x in ( e ) . Cast < T > ( )
join
明确指定了范围可变型子句
join T x in e on k1 equals k2
被翻译成
join x in ( e ) . Cast < T > ( ) on k1 equals k2
以下部分中的转换假定查询没有明确的范围变量类型。
这个例子
1 from Customer c in customers 2 where c.City == "London" 3 select c
被翻译成
1 from c in customers.Cast<Customer>() 2 where c.City == "London" 3 select c
其最终翻译是
1 customers. 2 Cast<Customer>(). 3 Where(c => c.City == "London")
显式范围变量类型对于查询实现非通用IEnumerable
接口的集合非常有用,但不适用于通用IEnumerable<T>
接口。在上面的例子中,如果customers
属于类型,则会出现这种情况ArrayList
。
查询表达式
表单的查询表达式
from x in e select x
被翻译成
( e ) . Select ( x => x )
这个例子
1 from c in customers 2 select c
被翻译成
customers.Select(c => c)
简并查询表达式是一种简单地选择源元素的表达式。翻译的后期阶段通过用其源替换它们来移除由其他翻译步骤引入的简并查询。但是,重要的是要确保查询表达式的结果永远不是源对象本身,因为这会向查询的客户端显示源的类型和标识。因此,此步骤通过显式调用源来保护直接在源代码中编写的退化查询Select
。然后由实现者Select
和其他查询运算符来确保这些方法永远不会返回源对象本身。
from,let,where,join和orderby子句
带有第二个from
子句后跟select
子句的查询表达式
1 from x1 in e1 2 from x2 in e2 3 select v
被翻译成
( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => v )
带有第二个from
子句的查询表达式,后跟除select
子句之外的其他内容:
1 from x1 in e1 2 from x2 in e2 3 ...
被翻译成
1 from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new { x1 , x2 } ) 2 ...
带有let
子句的查询表达式
1 from x in e 2 let y = f 3 ...
被翻译成
1 from * in ( e ) . Select ( x => new { x , y = f } ) 2 ...
带有where
子句的查询表达式
1 from x in e 2 where f 3 ...
被翻译成
1 from x in ( e ) . Where ( x => f ) 2 ...
查询表达式与join
子句没有into
后跟一个select
条款
1 from x1 in e1 2 join x2 in e2 on k1 equals k2 3 select v
被翻译成
( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => v )
带有join
子句的查询表达式,into
后面跟不是select
子句的子句
1 from x1 in e1 2 join x2 in e2 on k1 equals k2 3 ...
被翻译成
1 from * in ( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => new { x1 , x2 }) 2 ...
与查询表达式join
与子句into
后跟一个select
条款
1 from x1 in e1 2 join x2 in e2 on k1 equals k2 into g 3 select v
被翻译成
( e1 ) . GroupJoin( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => v )
带有join
子句的查询表达式,into
后跟除select
子句之外的其他内容
1 from x1 in e1 2 join x2 in e2 on k1 equals k2 into g 3 ...
被翻译成
1 from * in ( e1 ) . GroupJoin( e2 , x1 => k1 , x2 => k2 , ( x1 , g ) => new { x1 , g }) 2 ...
带有orderby
子句的查询表达式
1 from x in e 2 orderby k1 , k2 , ..., kn 3 ...
被翻译成
1 from x in ( e ) . 2 OrderBy ( x => k1 ) . 3 ThenBy ( x => k2 ) . 4 ... . 5 ThenBy ( x => kn ) 6 ...
如果一个排序子句指定了一个descending
方向指示符,则调用OrderByDescending
或ThenByDescending
生成一个方向指示符。
下面的转换假定不存在let
,where
,join
或orderby
条款,并且不超过所述一个初始多个from
在每个查询表达式子句。
这个例子
1 from c in customers 2 from o in c.Orders 3 select new { c.Name, o.OrderID, o.Total }
被翻译成
1 customers. 2 SelectMany(c => c.Orders, 3 (c,o) => new { c.Name, o.OrderID, o.Total } 4 )
这个例子
1 from c in customers 2 from o in c.Orders 3 orderby o.Total descending 4 select new { c.Name, o.OrderID, o.Total }
被翻译成
1 from * in customers. 2 SelectMany(c => c.Orders, (c,o) => new { c, o }) 3 orderby o.Total descending 4 select new { c.Name, o.OrderID, o.Total }
其最终翻译是
1 customers. 2 SelectMany(c => c.Orders, (c,o) => new { c, o }). 3 OrderByDescending(x => x.o.Total). 4 Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })
其中x
是编译器生成的标识符,否则该标识符将不可见且不可访问。
这个例子
1 from o in orders 2 let t = o.Details.Sum(d => d.UnitPrice * d.Quantity) 3 where t >= 1000 4 select new { o.OrderID, Total = t }
被翻译成
1 from * in orders. 2 Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }) 3 where t >= 1000 4 select new { o.OrderID, Total = t }
其最终翻译是
1 orders. 2 Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) }). 3 Where(x => x.t >= 1000). 4 Select(x => new { x.o.OrderID, Total = x.t })
其中x
是编译器生成的标识符,否则该标识符将不可见且不可访问。
这个例子
1 from c in customers 2 join o in orders on c.CustomerID equals o.CustomerID 3 select new { c.Name, o.OrderDate, o.Total }
被翻译成
1 customers.Join(orders, c => c.CustomerID, o => o.CustomerID, 2 (c, o) => new { c.Name, o.OrderDate, o.Total })
这个例子
1 from c in customers 2 join o in orders on c.CustomerID equals o.CustomerID into co 3 let n = co.Count() 4 where n >= 10 5 select new { c.Name, OrderCount = n }
被翻译成
1 from * in customers. 2 GroupJoin(orders, c => c.CustomerID, o => o.CustomerID, 3 (c, co) => new { c, co }) 4 let n = co.Count() 5 where n >= 10 6 select new { c.Name, OrderCount = n }
其最终翻译是
1 customers. 2 GroupJoin(orders, c => c.CustomerID, o => o.CustomerID, 3 (c, co) => new { c, co }). 4 Select(x => new { x, n = x.co.Count() }). 5 Where(y => y.n >= 10). 6 Select(y => new { y.x.c.Name, OrderCount = y.n)
其中x
和y
是编译器生成的标识符,否则这些标识符将不可见且不可访问。
这个例子
1 from o in orders 2 orderby o.Customer.Name, o.Total descending 3 select o
最后的翻译
1 orders. 2 OrderBy(o => o.Customer.Name). 3 ThenByDescending(o => o.Total)
select语句
表单的查询表达式
from x in e select v
被翻译成
( e ) . Select ( x => v )
除非v是标识符x,否则翻译很简单
( e )
例如
1 from c in customers.Where(c => c.City == "London") 2 select c
简单地翻译成
customers.Where(c => c.City == "London")
Groupby语句
表单的查询表达式
from x in e group v by k
被翻译成
( e ) . GroupBy ( x => k , x => v )
除非v是标识符x,否则翻译为
( e ) . GroupBy ( x => k )
这个例子
1 from c in customers 2 group c.Name by c.Country
被翻译成
1 customers. 2 GroupBy(c => c.Country, c => c.Name)
透明标识符
某些翻译注入范围变量与透明标识符记*
。透明标识符不是适当的语言功能; 它们仅作为查询表达式转换过程中的中间步骤存在。
当查询转换注入透明标识符时,进一步的转换步骤将透明标识符传播到匿名函数和匿名对象初始值设定项中。在这些上下文中,透明标识符具有以下行为:
- 当透明标识符作为匿名函数中的参数出现时,关联匿名类型的成员将自动在匿名函数体中的范围内。
- 当具有透明标识符的成员在范围内时,该成员的成员也在范围内。
- 当透明标识符作为匿名对象初始值设定项中的成员声明符出现时,它会引入具有透明标识符的成员。
- 在上述转换步骤中,始终将透明标识符与匿名类型一起引入,目的是捕获多个范围变量作为单个对象的成员。允许C#的实现使用与匿名类型不同的机制将多个范围变量组合在一起。以下翻译示例假定使用匿名类型,并显示如何转换透明标识符。
这个例子
1 from c in customers 2 from o in c.Orders 3 orderby o.Total descending 4 select new { c.Name, o.Total }
被翻译成
1 from * in customers. 2 SelectMany(c => c.Orders, (c,o) => new { c, o }) 3 orderby o.Total descending 4 select new { c.Name, o.Total }
进一步翻译成
1 customers. 2 SelectMany(c => c.Orders, (c,o) => new { c, o }). 3 OrderByDescending(* => o.Total). 4 Select(* => new { c.Name, o.Total })
当擦除透明标识符时,它等同于
1 customers. 2 SelectMany(c => c.Orders, (c,o) => new { c, o }). 3 OrderByDescending(x => x.o.Total). 4 Select(x => new { x.c.Name, x.o.Total })
其中x
是编译器生成的标识符,否则该标识符将不可见且不可访问。
这个例子
1 from c in customers 2 join o in orders on c.CustomerID equals o.CustomerID 3 join d in details on o.OrderID equals d.OrderID 4 join p in products on d.ProductID equals p.ProductID 5 select new { c.Name, o.OrderDate, p.ProductName }
被翻译成
1 from * in customers. 2 Join(orders, c => c.CustomerID, o => o.CustomerID, 3 (c, o) => new { c, o }) 4 join d in details on o.OrderID equals d.OrderID 5 join p in products on d.ProductID equals p.ProductID 6 select new { c.Name, o.OrderDate, p.ProductName }
这进一步减少到
1 customers. 2 Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }). 3 Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }). 4 Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { *, p }). 5 Select(* => new { c.Name, o.OrderDate, p.ProductName })
其最终翻译是
1 customers. 2 Join(orders, c => c.CustomerID, o => o.CustomerID, 3 (c, o) => new { c, o }). 4 Join(details, x => x.o.OrderID, d => d.OrderID, 5 (x, d) => new { x, d }). 6 Join(products, y => y.d.ProductID, p => p.ProductID, 7 (y, p) => new { y, p }). 8 Select(z => new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName })
where x
,y
和z
是编译器生成的标识符,否则这些标识符是不可见的和不可访问的。
查询表达式模式
所述查询表达模式建立的该类型可以实现,以支持查询表达式的方法的模式。由于查询表达式通过语法映射转换为方法调用,因此类型在实现查询表达式模式方面具有相当大的灵活性。例如,模式的方法可以实现为实例方法或扩展方法,因为两者具有相同的调用语法,并且方法可以请求委托或表达式树,因为匿名函数可以转换为两者。
C<T>
支持查询表达式模式的泛型类型的推荐形状如下所示。使用泛型类型是为了说明参数和结果类型之间的正确关系,但也可以为非泛型类型实现模式。
1 delegate R Func<T1,R>(T1 arg1); 2 3 delegate R Func<T1,T2,R>(T1 arg1, T2 arg2); 4 5 class C 6 { 7 public C<T> Cast<T>(); 8 } 9 10 class C<T> : C 11 { 12 public C<T> Where(Func<T,bool> predicate); 13 14 public C<U> Select<U>(Func<T,U> selector); 15 16 public C<V> SelectMany<U,V>(Func<T,C<U>> selector, 17 Func<T,U,V> resultSelector); 18 19 public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, 20 Func<U,K> innerKeySelector, Func<T,U,V> resultSelector); 21 22 public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector, 23 Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector); 24 25 public O<T> OrderBy<K>(Func<T,K> keySelector); 26 27 public O<T> OrderByDescending<K>(Func<T,K> keySelector); 28 29 public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector); 30 31 public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector, 32 Func<T,E> elementSelector); 33 } 34 35 class O<T> : C<T> 36 { 37 public O<T> ThenBy<K>(Func<T,K> keySelector); 38 39 public O<T> ThenByDescending<K>(Func<T,K> keySelector); 40 } 41 42 class G<K,T> : C<T> 43 { 44 public K Key { get; } 45 }
上述方法使用泛型委托类型Func<T1,R>
和Func<T1,T2,R>
,但他们同样可以使用过的其他委托或表达式目录树类型的参数和结果类型相同的关系。
请注意建议的关系C<T>
,O<T>
它确保ThenBy
和ThenByDescending
方法仅在OrderBy
或的结果上可用OrderByDescending
。还要注意推荐的结果形状GroupBy
- 序列序列,其中每个内部序列都有一个附加Key
属性。
该System.Linq
命名空间提供了用于实现该任何类型的查询运算符模式的实现System.Collections.Generic.IEnumerable<T>
接口。
分配操作符
赋值运算符为变量,属性,事件或索引器元素分配新值。
1 assignment 2 : unary_expression assignment_operator expression 3 ; 4 5 assignment_operator 6 : '=' 7 | '+=' 8 | '-=' 9 | '*=' 10 | '/=' 11 | '%=' 12 | '&=' 13 | '|=' 14 | '^=' 15 | '<<=' 16 | right_shift_assignment 17 ;
赋值的左操作数必须是分类为变量的表达式,属性访问,索引器访问或事件访问。
该=
操作被称为简单赋值运算符。它将右操作数的值分配给左操作数给出的变量,属性或索引器元素。简单赋值运算符的左操作数可能不是事件访问(除了在类字段事件中描述)。简单赋值操作符在简单赋值中描述。
除=
运算符之外的赋值运算符称为复合赋值运算符。这些运算符对两个操作数执行指示的操作,然后将结果值分配给左操作数给出的变量,属性或索引器元素。复合赋值运算符在复合赋值中描述。
所述+=
和-=
与一个事件的访问表达式作为左操作数运算符被称为事件赋值运算符。没有其他赋值运算符作为左操作数的事件访问有效。事件分配中描述了事件赋值运算符。
赋值运算符是右关联的,这意味着操作从右到左分组。例如,表单的表达式a = b = c
被评估为a = (b = c)
。
简单的任务
该=
操作被称为简单赋值运算符。
如果简单赋值的左操作数的形式为E.P
或E[Ei]
其中E
具有编译时类型dynamic
,则该分配是动态绑定(动态绑定)。在这种情况下,赋值表达式的编译时类型是dynamic
,并且下面描述的分辨率将在运行时根据运行时类型进行E
。
在简单赋值中,右操作数必须是可隐式转换为左操作数类型的表达式。该操作将右操作数的值分配给左操作数给出的变量,属性或索引器元素。
简单赋值表达式的结果是赋给左操作数的值。结果与左操作数的类型相同,并且始终归类为值。
如果左操作数是属性或索引器访问权限,则属性或索引器必须具有set
访问者。如果不是这种情况,则会发生绑定时错误。
表单的简单赋值的运行时处理x = y
包括以下步骤:
- 如果
x
被归类为变量:x
被评估以产生变量。y
被评估,并且如果需要,x
通过隐式转换(隐式转换)转换为类型。- 如果给定的变量
x
是reference_type的数组元素,则执行运行时检查以确保为其计算的值y
与数组实例x
是元素兼容。如果y
是null
,则检查成功,或者是否从包含的数组实例的实际元素类型引用的实例的实际类型中存在隐式引用转换(隐式引用转换)。否则,抛出一个。y
x
System.ArrayTypeMismatchException
- 评估和转换产生的值
y
存储在评估给定的位置x
。
- 如果
x
被归类为属性或索引器访问:- 将评估实例表达式(如果
x
不是static
)和x
与之关联的参数列表(如果是索引器访问)x
,并将结果用于后续的set
访问者调用。 y
被评估,并且如果需要,x
通过隐式转换(隐式转换)转换为类型。- 使用为其参数计算的值调用
set
访问器。x
y
value
- 将评估实例表达式(如果
阵列协方差规则(阵列协方差)准许的阵列类型的值A[]
是一个数组类型的实例的引用B[]
,提供了一种隐式引用转换从存在B
到A
。由于这些规则,对reference_type的数组元素的赋值需要运行时检查以确保分配的值与数组实例兼容。在这个例子中
1 string[] sa = new string[10]; 2 object[] oa = sa; 3 4 oa[0] = null; // Ok 5 oa[1] = "Hello"; // Ok 6 oa[2] = new ArrayList(); // ArrayTypeMismatchException
最后一个赋值会导致System.ArrayTypeMismatchException
抛出一个因为一个实例ArrayList
无法存储在一个元素中string[]
。
当在struct_type中声明的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式必须归类为变量。如果实例表达式被分类为值,则发生绑定时错误。由于成员访问权限,同样的规则也适用于字段。
鉴于声明:
1 struct Point 2 { 3 int x, y; 4 5 public Point(int x, int y) { 6 this.x = x; 7 this.y = y; 8 } 9 10 public int X { 11 get { return x; } 12 set { x = value; } 13 } 14 15 public int Y { 16 get { return y; } 17 set { y = value; } 18 } 19 } 20 21 struct Rectangle 22 { 23 Point a, b; 24 25 public Rectangle(Point a, Point b) { 26 this.a = a; 27 this.b = b; 28 } 29 30 public Point A { 31 get { return a; } 32 set { a = value; } 33 } 34 35 public Point B { 36 get { return b; } 37 set { b = value; } 38 } 39 }
在示例中
1 Point p = new Point(); 2 p.X = 100; 3 p.Y = 100; 4 Rectangle r = new Rectangle(); 5 r.A = new Point(10, 10); 6 r.B = p;
到分配p.X
,p.Y
,r.A
,和r.B
被允许的,因为p
和r
变数。但是,在示例中
1 Rectangle r = new Rectangle(); 2 r.A.X = 10; 3 r.A.Y = 10; 4 r.B.X = 100; 5 r.B.Y = 100;
该分配都是无效的,因为r.A
和r.B
不变量。
复合赋值
如果复合赋值指配的左操作数的形式为E.P
或E[Ei]
其中E
具有编译时类型dynamic
,则该分配是动态绑定(动态绑定)。在这种情况下,赋值表达式的编译时类型是dynamic
,并且下面描述的分辨率将在运行时根据运行时类型进行E
。
x op= y
通过应用二进制运算符重载决策(二进制运算符重载决策)来处理表单的操作,就像写入操作一样x op y
。然后,
- 如果所选运算符的返回类型可隐式转换为类型
x
,则操作将被计算为x = x op y
,除了x
仅计算一次。 - 否则,如果所选运算符是预定义运算符,如果所选运算符的返回类型可显式转换为类型
x
,并且ify
可隐式转换为类型x
或运算符是移位运算符,则操作将计算为x = (T)(x op y)
,其中T
是类型x
,除了x
仅评估一次。 - 否则,复合赋值无效,并发生绑定时错误。
术语“仅评估一次”意味着在评估中x op y
,任何组成表达式的结果x
被临时保存,然后在执行分配时重复使用x
。例如,在分配A()[B()] += C()
,其中A
是返回的方法int[]
,和B
和C
在返回的方法int
中,所述方法被调用一次,在顺序A
,B
,C
。
当复合赋值的左操作数是属性访问或索引器访问时,属性或索引器必须同时具有get
访问者和set
访问者。如果不是这种情况,则会发生绑定时错误。
上述第二条规则允许在某些情况下x op= y
进行评估x = (T)(x op y)
。规则存在,使得预定义的运算符可作为化合物运算符时的左操作数的类型是sbyte
,byte
,short
,ushort
,或char
。即使两个参数都属于这些类型之一,预定义运算符也会生成类型的结果int
,如二进制数字促销中所述。因此,如果没有强制转换,则无法将结果分配给左操作数。
预定义的运营规则的直观效果仅仅是x op= y
被允许的,如果两者的x op y
和x = y
被允许的。在这个例子中
1 byte b = 0; 2 char ch = '\0'; 3 int i = 0; 4 5 b += 1; // Ok 6 b += 1000; // Error, b = 1000 not permitted 7 b += i; // Error, b = i not permitted 8 b += (byte)i; // Ok 9 10 ch += 1; // Error, ch = 1 not permitted 11 ch += (char)1; // Ok
每个错误的直观原因是相应的简单分配也是一个错误。
这也意味着复合赋值操作支持提升操作。在这个例子中
1 int? i = 0; 2 i += 1; // Ok
事件分配
如果+=
或-=
运算符的左操作数被分类为事件访问,则表达式的计算方法如下:
- 将评估事件访问的实例表达式(如果有)。
- 计算
+=
or-=
运算符的右操作数,如果需要,通过隐式转换(隐式转换)将其转换为左操作数的类型。 - 调用事件的事件访问器,参数列表由右操作数组成,在评估之后,如果需要,还包括转换。如果是运算符
+=
,add
则调用访问者; 如果是运算符-=
,remove
则调用访问器。
事件赋值表达式不会产生值。因此,事件赋值表达式仅在statement_expression(Expression语句)的上下文中有效。
表达式
一个表达式可以是一个non_assignment_expression或分配。
1 expression 2 : non_assignment_expression 3 | assignment 4 ; 5 6 non_assignment_expression 7 : conditional_expression 8 | lambda_expression 9 | query_expression 10 ;
常量表达式
constant_expression是可以在编译时被完全计算的表达式。
1 constant_expression 2 : expression 3 ;
常量表达式必须是null
文字或与以下类型之一的值:sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
,bool
,object
,string
,或任何枚举类型。在常量表达式中只允许以下构造:
- 文字(包括
null
文字)。 const
对类和结构类型成员的引用。- 引用枚举类型的成员。
- 对
const
参数或局部变量的引用 - 带括号的子表达式,它们本身是常量表达式。
- 如果目标类型是上面列出的类型之一,则转换表达式。
checked
和unchecked
表达- 默认值表达式
- 表达的名称
- 预定义的
+
,-
,!
,和~
一元运算符。 - 预定义的
+
,-
,*
,/
,%
,<<
,>>
,&
,|
,^
,&&
,||
,==
,!=
,<
,>
,<=
,和>=
的二进制运算符,提供的每个操作数是上面列出的类型。 - 在
?:
有条件的经营者。
常量表达式允许以下转换:
- 身份转换
- 数字转换
- 枚举转换
- 常量表达式转换
- 隐式和显式引用转换,前提是转换源是一个求值为null值的常量表达式。
常量表达式中不允许进行其他转换,包括装箱,取消装箱和非空值的隐式引用转换。例如:
1 class C { 2 const object i = 5; // error: boxing conversion not permitted 3 const object str = "hello"; // error: implicit reference conversion 4 }
i的初始化是一个错误,因为需要装箱转换。str的初始化是一个错误,因为需要从非null值进行隐式引用转换。
只要表达式满足上面列出的要求,就会在编译时计算表达式。即使表达式是包含非常量构造的较大表达式的子表达式,也是如此。
常量表达式的编译时评估使用与非常量表达式的运行时评估相同的规则,但运行时评估会抛出异常的情况除外,编译时评估会导致编译时错误发生。
除非在unchecked
上下文中显式放置常量表达式,否则在表达式的编译时评估期间,在整型算术运算和转换中发生的溢出总是会导致编译时错误(常量表达式)。
常量表达式出现在下面列出的上下文中。在这些上下文中,如果在编译时无法完全计算表达式,则会发生编译时错误。
- 常量声明(常量)。
- 枚举成员声明(枚举成员)。
- 形式参数列表的默认参数(方法参数)
case
switch
语句的标签(switch语句)。goto case
语句(goto语句)。- 包含初始值设定项的数组创建表达式(数组创建表达式)中的维度长度。
- 属性(属性)。
隐式常量表达式转换(隐式常量表达式转换)允许类型的常量表达式int
被转换为sbyte
,byte
,short
,ushort
,uint
,或ulong
,所提供的常量表达式的值在目标类型的范围内。
布尔表达式
逻辑表达式是产生类型的结果的表达式bool
; 直接或通过operator true
在以下指定的某些情况下的应用。
1 boolean_expression 2 : expression 3 ;
if_statement(if语句),while_statement(while语句),do_statement(do语句)或for_statement(for语句)的控制条件表达式是boolean_expression。?:
运算符(条件运算符)的控制条件表达式遵循与boolean_expression相同的规则,但出于运算符优先级的原因,将其归类为conditional_or_expression。
甲逻辑表达式 E
需要能够生产类型的值bool
,如下所示:
- 如果
E
是隐式转换为bool
然后在该应用隐式转换运行。 - 否则,一元运算符重载决策(一元运算符重载决策)用于查找运算符
true
on 的唯一最佳实现E
,并且该实现在运行时应用。 - 如果未找到此类运算符,则会发生绑定时错误。
Database boolean type中的DBBool
struct类型提供了一个实现和的类型的示例。operator true
operator false