12 表达式

12.1 常规

表达式是一系列运算符和操作数。 此子句定义操作数和运算符的计算顺序以及表达式的含义。

12.2 表达式分类

12.2.1 总则

表达式的结果被归类为下列结果之一:

  • 一个 值。 每个值都有一个关联的类型。
  • 变量。 除非另有说明,否则变量是显式类型,并有一个相关类型,即变量的声明类型。 隐式类型变量没有相关类型。
  • 空文字。 具有此分类的表达式可以隐式转换为引用类型或可空值类型。
  • 匿名函数。 具有此分类的表达式可以隐式转换为兼容的委托类型或表达式树类型。
  • 元组。 每个元组都有固定数量的元素,每个元素都有一个表达式和一个可选的元组元素名称。
  • 属性访问。 每个属性访问都有一个相关类型,即属性类型。 此外,属性访问可能具有关联的实例表达式。 在调用实例属性访问的访问器时,实例表达式的求值结果将成为 this 所代表的实例 (§12.8.14)。
  • 索引器访问。 每个索引器访问都具有关联的类型,即索引器的元素类型。 此外,索引器访问具有关联的实例表达式和关联的参数列表。 调用索引器访问器时,计算实例表达式的结果将成为由 this§12.8.14)表示的实例,而参数列表的计算结果则将成为调用的参数列表。
  • 无。 当表达式调用返回类型为 void的方法时,就会发生这种情况。 被分类为无内容的表达式仅在 statement_expression§13.7)或作为 lambda_expression§12.19)的正文时有效。

对于作为较大表达式的子表达式的表达式,在注意到的限制条件下,结果也可以归类为下列之一:

  • 命名空间。 具有此分类的表达式只能出现在 member_access 的左侧(§12.8.7)。 在任何其他上下文中,分类为命名空间的表达式会导致编译时错误。
  • 类型。 具有这种分类的表达式只能作为 member_access 的左侧表达式出现 (§12.8.7)。 在任何其他上下文中,分类为类型的表达式会导致编译时错误。
  • 方法组,是通过成员查找得出的一组重载方法 (§12.5)。 方法组可能具有关联的实例表达式和关联的类型参数列表。 调用实例方法时,计算实例表达式的结果将成为由 this§12.8.14) 表示的实例。 在 invocation_expression (§12.8.10) 或 delegate_creation_expression (§12.8.17.6) 中允许使用方法组,并可隐式转换为兼容的委托类型 (§10.8)。 在任何其他上下文中,分类为方法组的表达式会导致编译时错误。
  • 事件访问。 每个事件访问都有一个关联类型,即事件的类型。 此外,事件访问可能有一个相关的实例表达式。 事件访问可以作为 +=-= 运算符的左运算符出现 (§12.21.5)。 在任何其他上下文中,分类为事件访问的表达式会导致编译时错误。 在调用实例事件访问的访问器时,实例表达式的求值结果将成为 this 所代表的实例 (§12.8.14)。
  • throw 表达式,可在多种情况下用于引发表达式中的异常。 throw 表达式可通过隐式转换为任何类型。

通过调用 get 访问器或 set 访问器,属性访问或索引器访问总是被重新分类为值。 特定访问器由属性或索引器访问的上下文确定:如果访问是分配的目标,则会调用 set 访问器来分配新值(§12.21.2)。 否则,调用 get 访问器以获取当前值(§12.2.2)。

实例访问器 是实例的属性访问、实例上的事件访问或索引器访问。

12.2.2 表达式的值

涉及表达式的大多数构造最终要求表达式表示 。 在这种情况下,如果实际表达式表示命名空间、类型、方法组或无任何内容,则会发生编译时错误。 但是,如果表达式表示属性访问、索引器访问或变量,则隐式替换属性、索引器或变量的值:

  • 变量的值只是当前存储在变量标识的存储位置中的值。 变量在获取其值之前,应被视为明确赋值(§9.4),否则会发生编译时错误。
  • 通过调用属性的 get 访问器来获取属性访问表达式的值。 如果该属性没有 get 访问器,则会发生编译时错误。 否则,将执行函数成员调用(§12.6.6),调用的结果将成为属性访问表达式的值。
  • 通过调用索引器的 get 访问器来获取索引器访问表达式的值。 如果索引器没有 get 访问器,则会发生编译时错误。 否则,将对与索引器访问表达式关联的参数列表执行函数成员调用(§12.6.6),调用的结果将成为索引器访问表达式的值。
  • 元组表达式的值是通过将隐式元组转换(§10.2.13)应用于元组表达式的类型来获取的。 获取没有类型的元组表达式的值是一个错误。

12.3 静态绑定和动态绑定

12.3.1 常规

绑定 是根据表达式的类型或值(参数、操作数、接收器)确定操作所指对象的过程。 例如,方法调用的绑定取决于接收方和参数的类型。 运算符的绑定取决于其操作数的类型。

在 C# 中,操作的绑定通常在编译时根据其子表达式的编译时类型确定。 同样,如果表达式包含错误,则会在编译时检测并报告该错误。 此方法称为 静态绑定

但是,如果表达式是 动态表达式(即具有类型 dynamic),则表示其参与的任何绑定都应基于其运行时类型,而不是编译时所参与的类型。 因此,此类操作的绑定将延迟到在程序运行期间执行操作的时间。 这称为 动态绑定

如果操作被动态绑定,那么编译时几乎不会进行检查。 相反,如果运行时绑定失败,错误会在运行时报告为异常。

C# 中的以下操作受绑定约束:

  • 成员访问:e.M
  • 方法调用:e.M(e₁,...,eᵥ)
  • 委托调用:e(e₁,...,eᵥ)
  • 元素访问:e[e₁,...,eᵥ]
  • 对象创建:新 C(e₁,...,eᵥ)
  • 重载的一元运算符:+-!(仅限逻辑非)、~++--truefalse
  • 重载二进制运算符:+-*/%&&&|||??^<<>>==!=><>=<=
  • 赋值运算符:== ref+=-=*=/=%=&=|=^=<<=>>=
  • 隐式和显式转换

如果没有涉及动态表达式,C# 默认为静态绑定,这意味着在选择过程中使用子表达式的编译时类型。 但是,当上面列出的操作中的某个子表达式是动态表达式时,该操作将改为动态绑定。

如果方法调用是动态绑定的,并且任何参数(包括接收方)都是输入参数,则这是编译时错误。

12.3.2 绑定时

静态绑定在编译时发生,而动态绑定在运行时进行。 在以下子项中,绑定时 术语是指编译时或运行时,具体取决于绑定的发生时间。

示例:下面说明了静态绑定和动态绑定的概念以及绑定时间的概念:

object o = 5;
dynamic d = 5;
Console.WriteLine(5); // static binding to Console.WriteLine(int)
Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

前两个调用是静态绑定的:Console.WriteLine 的重载是根据其参数的编译时类型选择的。 因此,绑定时是编译时

第三个调用是动态绑定的:Console.WriteLine 的重载是根据其参数的运行时类型来选择的。 之所以发生这种情况,是因为该参数是动态表达式 , 其编译时类型是动态的。 因此,第三次调用的绑定时为运行时

结束示例

12.3.3 动态绑定

此小节是信息性的。

动态绑定允许 C# 程序与动态对象进行交互,即不遵循 C# 类型系统的正常规则的对象。 动态对象可以是具有不同类型系统的其他编程语言中的对象,也可能是以编程方式设置的对象,以便为不同的操作实现自己的绑定语义。

动态对象实现其自身语义的机制取决于具体实现。 给定接口(再次定义实现)由动态对象实现,以向 C# 运行时发出信号,这些对象具有特殊的语义。 因此,每当动态对象上的操作以动态绑定方式进行时,接管的是其自身的绑定语义,而不是此规范中规定的 C# 的绑定语义。

虽然动态绑定的目的是允许与动态对象进行互操作,但 C# 允许对所有对象进行动态绑定,无论它们是否是动态的。 这允许更流畅地集成动态对象,因为对其操作的结果可能不是动态对象,但在编译时仍属于程序员未知的类型。 即使没有涉及的对象是动态对象,动态绑定也有助于消除易出错的基于反射的代码。

12.3.4 子表达式的类型

当操作静态绑定时,子表达式的类型(例如接收方和参数、索引或操作数)始终被视为该表达式的编译时类型。

动态绑定操作时,子表达式的类型根据子表达式的编译时类型的不同方式确定:

  • 编译时动态类型的子表达式在运行时被认为具有表达式求值的实际值的类型
  • 编译时类型为类型参数的子表达式被视为具有类型参数在运行时绑定到的类型
  • 否则,子表达式被视为具有其编译时类型。

12.4 运算符

12.4.1 概述

表达式是从操作数和运算符构造的。 表达式的运算符指明了向操作数应用的运算。

示例:运算符示例包括 +-*/new。 操作数的示例包括文本、字段、局部变量和表达式。 结束示例

运算符有三种:

  • 一元运算符。 一元运算符只有一个操作数,使用前缀表示法(如 –x)或后缀表示法(如 x++)。
  • 二进制运算符。 二元运算符有两个操作数,全部使用中缀表示法(如 x + y)。
  • 三元运算符。 只有一个三元运算符 ?:存在,它采用三个操作数并使用中缀表示法(c ? x : y)。

表达式中运算符的计算顺序由运算符的 优先关联性§12.4.2)决定。

表达式中的操作数从左到右计算。

示例:在 F(i) + G(i++) * H(i)中,使用旧值 i调用方法 F,然后使用旧值 i调用方法 G,最后,使用 i 的新值调用方法 H。 这与运算符优先级毫无关联。 结束示例

特定的运算符可重载。 运算符重载(§12.4.3) 允许为一个或多个操作数属于用户定义的类或结构类型的操作指定用户定义的运算符实现。

12.4.2 运算符优先级和关联性

当表达式包含多个运算符时,运算符 优先级 控制各个运算符的计算顺序。

注意:例如,表达式 x + y * z 的计算结果为 x + (y * z),因为 * 运算符的优先级高于二进制 + 运算符。 尾注

运算符的优先级由其关联的文法产生式的定义来确定。

注意:例如,additive_expression 是由 multiplicative_expression序列组成,它们由 +- 运算符分隔,这样一来,+- 运算符的优先级低于 */% 运算符。 尾注

注意:下表按优先级从高到低的顺序汇总了所有运算符:

子子句 类别 运算符
§12.8 主要节点 x.yx?.yf(x)a[x]a?[x]x++x--x!newtypeofdefaultcheckeduncheckeddelegatestackalloc
§12.9 一元 +-!x~++x--x(T)xawait x
§12.10 乘法性的 */%
§12.10 累加性 +-
§12.11 Shift <<>>
§12.12 关系和类型测试 <><=>=isas
§12.12 平等 ==!=
§12.13 逻辑与 &
§12.13 逻辑“异或” ^
§12.13 逻辑或 \|
§12.14 条件“与” &&
§12.14 条件“或” \|\|
§12.15§12.16 Null 合并和 throw 表达式 ??throw x
§12.18 有條件的 ?:
§12.21§12.19 赋值和 lambda 表达式 == ref*=/=%=+=-=<<=>>=&=^=\|==>

尾注

当操作数出现在两个具有相同优先级的运算符之间时,运算符的结合性 控制操作执行的顺序:

  • 除赋值运算符和 null 合并运算符外,所有二元运算符都 左关联,这意味着操作从左到右执行。

    示例x + y + z 将计算为 (x + y) + z结束示例

  • 赋值运算符、null 合并运算符和条件运算符 (?:) 为右结合运算符,即从右向左执行运算。

    示例x = y = z 被评估为 x = (y = z)结束示例

可以使用括号控制优先级和关联性。

示例x + y * zy 乘以 z,然后将结果添加到 x,但 (x + y) * z 先添加 xy,然后将结果乘以 z结束示例

12.4.3 运算符重载

所有一元运算符和二进制运算符都具有预定义的实现。 此外,还可以通过在类和结构中包含运算符声明(§15.10)来引入用户定义的实现。 用户定义的运算符实现始终优先于预定义运算符实现:只有在不存在适用的用户定义的运算符实现时,才会考虑预定义运算符实现,如 §12.4.4§12.4.5中所述。

可重载的一元运算符包括:

+ - !(仅逻辑否定)~ ++ -- true false

注意:尽管表达式中未显式使用 truefalse(因此未包含在 §12.4.2中的优先表中),但它们被视为运算符,因为它们是在多个表达式上下文中调用的s:布尔表达式(§12.24)和涉及条件(§12.18)和条件逻辑运算符的表达式(§12.14)。 尾注

注意:null 包容运算符(后缀 !§12.8.9)并非可重载运算符。 尾注

可重载的二进制运算符是:

+  -  *  /  %  &  |  ^  <<  >>  ==  !=  >  <  <=  >=

只有上述运算符可以被重载。 具体而言,无法重载成员访问、方法调用或 =&&||???:=>checkeduncheckednewtypeofdefaultasis 运算符。

重载二进制运算符时,相应的复合赋值运算符(如果有)也会隐式重载。

示例:运算符 * 的重载也是运算符 *= 的重载。 §12.21对此进行进一步介绍。 结束示例

赋值运算符本身 (=) 不能重载。 赋值始终将值简单存储到变量(§12.21.2)。

通过提供用户定义的转换 (§10.5) 可以重载诸如 (T)x 等强制转换运算。

注意:用户定义的转换不会影响 isas 运算符的行为。 尾注

元素访问(如 a[x])不被视为可重载运算符。 相反,索引器支持用户定义的索引(§15.9)。

在表达式中,运算符是使用运算符表示法引用的,在声明中,运算符是使用功能表示法引用的。 下表显示了一元运算符和二元运算符的运算符与功能表示法之间的关系。 在第一个条目中,«op» 表示任何可重载的一元前缀运算符。 在第二个条目中,«op» 表示一元后缀 ++-- 运算符。 在第三个条目中,«op» 表示任何可重载的二进制运算符。

注意:有关重载 ++-- 运算符的示例,请参阅 §15.10.2尾注

运算符表示法 功能表示法
«op» x operator «op»(x)
x «op» operator «op»(x)
x «op» y operator «op»(x, y)

用户定义的运算符声明始终要求至少一个参数属于包含运算符声明的类或结构类型。

注意:因此,用户定义的运算符不可能具有与预定义运算符相同的签名。 尾注

用户定义的运算符声明不能修改运算符的语法、优先级或关联性。

示例/ 运算符始终是二进制运算符,始终具有 §12.4.2中指定的优先级别,并且始终是左关联。 结束示例

注意:虽然用户定义的运算符可以执行它所希望的任何计算,但我们强烈建议不要产生非直观预期结果的实现。 例如,运算符 == 的实现应比较两个操作数是否相等,并返回适当的 bool 结果。 尾注

§12.9§12.21 中描述每个运算符的说明,规定了运算符的预定义实现以及适用于每个运算符的任何其他规则。 说明中使用了术语一元运算符重载决策二元运算符重载决策数值提升,其提升运算符定义见以下子子句。

12.4.4 一元运算符重载决策

格式为 «op» xx «op»的操作,其中«op»是可重载的一元运算符,且 x 是类型为 X的表达式,处理方式如下:

  • X 为操作 operator «op»(x) 提供的候选用户定义运算符集是使用 §12.4.6规则确定的。
  • 如果候选用户定义运算符集不为空,则这将成为操作的候选运算符集。 否则,预定义的二元 operator «op» 实现(包括其提升形式)将成为该运算的一组候选运算符。 给定运算符的预定义实现在运算符的说明中指定。 枚举或委托类型提供的预定义运算符只有在操作数的绑定时类型(或基础类型 — 如果是可为 null 的类型)是枚举或委托类型时,才会包含在此运算符集中。
  • §12.6.4 的重载解析规则将应用于候选运算符集,以选择与参数列表 (x)相关的最佳运算符,此运算符将成为重载解析过程的结果。 如果重载解析无法选择单个最佳运算符,则会发生绑定时错误。

12.4.5 二元运算符重载决策

形如 x «op» y的操作,其中 «op» 是可重载的二元运算符,xX类型的表达式,yY类型的表达式,处理如下:

  • 确定 XY 为运算 operator «op»(x, y) 提供的候选用户定义运算符集。 该集由 X 提供的候选运算符和由 Y提供的候选运算符的联合组成,每个运算符均使用 §12.4.6的规则确定。 对于合并集,候选者的合并方式如下:
    • 如果 XY 是可转换的标识,或者如果 XY 派生自通用基类型,则共享候选运算符仅在组合集中发生一次。
    • 如果 XY之间存在相同转换,并且由 Y 提供的运算符 «op»Y 的返回类型与 X 提供的 «op»X 相同,且 «op»Y 的操作数类型能够相同转换为 «op»X 的相应操作数类型,那么只有 «op»X 会包含在集合中。
  • 如果候选用户定义运算符集不为空,则这将成为操作的候选运算符集。 否则,预定义的二元 operator «op» 实现(包括其提升形式)将成为该运算的一组候选运算符。 给定运算符的预定义实现在运算符的说明中指定。 对于预定义枚举和委托运算符,唯一考虑的运算符是枚举或委托类型提供的运算符,该枚举或委托类型是其中一个操作数的绑定时类型。
  • §12.6.4 的重载解析规则将应用于候选运算符集,以选择与参数列表 (x, y)相关的最佳运算符,此运算符将成为重载解析过程的结果。 如果重载解析无法选择单个最佳运算符,则会发生绑定时错误。

12.4.6 候选用户定义运算符

给定类型 T 和操作 operator «op»(A),其中 «op» 是可重载运算符,A 是参数列表,则由 T 为运算符 «op»(A) 提供的候选用户定义运算符集确定如下:

  • 确定类型 T₀。 如果 T 为可为 null 的值类型,则 T₀ 是其基础类型;否则,T₀ 等于 T
  • 对于 T₀ 中的所有 operator «op» 声明及其所有提升形式,如果在针对参数列表 A时至少有一个运算符适用(§12.6.4.2),那么候选运算符集由 T₀中所有这样的适用运算符组成。
  • 否则,如果 T₀object,则候选运算符集为空。
  • 否则,T₀ 提供的候选运算符集是由 T₀的直接基类提供的候选运算符集,或者 T₀ 的有效基类(如果 T₀ 是类型参数)。

12.4.7 数值提升

12.4.7.1 常规

此子款是信息性的。

§12.4.7 及其子子句是以下各项的合并效果的摘要:

数值提升包括自动执行预定义一元和二元数值运算符操作数的某些隐式转换。 数值提升并不是一种独特的机制,而是对预定义运算符应用重载决策的一种效果。 虽然用户定义的运算符也可以产生类似的效果,但数值提升并不会影响用户定义运算符的计算。

以数值提升为例,请参见二元 * 运算符的预定义实现:

int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);

当重载解析规则(§12.6.4)应用于这组运算符时,效果是选择操作数类型中存在隐式转换的第一个运算符。

示例:对于操作 b * s,其中 bbytesshort,重载决策选择 operator *(int, int) 作为最佳运算符。 因此,效果是 bs 转换为 int,结果的类型 int。 同样,对于操作 i * d,其中 iintddoubleoverload 决议选择 operator *(double, double) 作为最佳操作员。 结束示例

信息性文本的结尾。

12.4.7.2 一元数值提升

此子条款是信息性的。

对于预定义的 +-~ 一元运算符的操作数,会进行一元数值提升。 一元数值提升只需将 sbytebyteshortushortchar 类型的操作数转换为 int 类型。 此外,对于一元运算符,一元数值推广将操作数类型 uint 转换为类型 long

信息性文本的结尾。

12.4.7.3 二元数值提升

此子条款是信息性的。

对于预定义的 +-*/%&|^==!=><>=<= 二元运算符的操作数,会进行二元数值提升。 二元数值提升会将两个操作数隐式转换为一种通用类型,对于非关系运算符,该类型也成为运算的结果类型。 二元数值提升包括按以下顺序应用以下规则:

  • 如果任一操作数的类型为 decimal,则另一个操作数将转换为类型 decimal;如果另一个操作数的类型为 floatdouble,则会发生绑定时错误。
  • 否则,如果任一操作数的类型为 double,则另一个操作数将转换为类型 double
  • 否则,如果任一操作数的类型为 float,则另一个操作数将转换为类型 float
  • 否则,如果任一操作数的类型为 ulong,则另一个操作数将转换为类型 ulong;如果另一个操作数为 type sbyteshortintlong,则会发生绑定时错误。
  • 否则,如果任一操作数的类型为 long,则另一个操作数将转换为类型 long
  • 否则,如果任一操作数的类型为 uint,另一个操作数的类型为 sbyteshortint,则两个操作数将转换为类型 long
  • 否则,如果任一操作数的类型为 uint,则另一个操作数将转换为类型 uint
  • 否则,两个操作数将转换为 int 类型。

注意:第一个规则禁止将 decimal 类型与 doublefloat 类型混合的任何操作。 规则遵循以下事实:decimal 类型和 doublefloat 类型之间没有隐式转换。 尾注

注意:另请注意,当另一个操作数是带符号的整型类型时,操作数不可能是 ulong 类型。 原因是没有一种整型类型能像带符号整型类型那样表示 ulong 的全部范围。 尾注

在上述两种情况下,可以使用类型转换表达式将一个操作数显式地转换为与另一个操作数兼容的类型。

示例:在以下代码中

decimal AddPercent(decimal x, double percent) =>
    x * (1.0 + percent / 100.0);

发生绑定时错误,因为 decimal 不能乘以 double。 解决该错误的方法是将第二个操作数明确转换为 decimal,如下所示:

decimal AddPercent(decimal x, double percent) =>
    x * (decimal)(1.0 + percent / 100.0);

结束示例

信息性文本的结尾。

12.4.8 提升的运算符

提升运算符 允许对不可为 null 的值类型进行运算的预定义和用户定义的运算符也可用于这些类型的可为 null 形式。 提升运算符由符合特定要求的预定义运算符和用户定义运算符构建而成,如下所述:

  • 对于一元运算符 +++---!(逻辑求反)和 ~,如果操作数和结果类型都是不可为 null 的值类型,则存在运算符的提升形式。 通过在操作数和结果类型中添加一个 ? 修饰符,即可构造出提升形式。 如果操作数是 null,提升运算符会生成 null 值。 否则,提升运算符会解开操作数,应用基础运算符,并封装结果。
  • 对于二进制运算符 +-*/%&|^<<>>,如果操作数和结果类型都是不可为 null 的值类型,则存在运算符的提升形式。 通过在每个操作数和结果类型中添加一个 ? 修饰符,即可构造出提升形式。 如果一个或两个操作数都是 null,则提升运算符会生成 null 值(§12.13.5 中介绍的 bool? 类型的 &| 运算符除外)。 否则,提升运算符会解开操作数,应用基础运算符,并封装结果。
  • 对于相等运算符 ==!= 而言,如果操作数类型都是不可为 null 的值类型,并且结果类型是 bool,则存在运算符的提升形式。 通过在每个操作数类型中添加一个 ? 修饰符,即可构造出提升形式。 提升运算符将两个 null 值视为相等,null 值与任何非null 值不相等。 如果两个操作数都是非 null,则提升后的运算符会解包操作数,并使用基础运算符来生成 bool 结果。
  • 对于关系运算符 <><=>=,如果操作数类型都是不可为 null 的值类型,并且结果类型为 bool,那么此运算符就存在一种提升的形式。 通过在每个操作数类型中添加一个 ? 修饰符,即可构造出提升形式。 如果一个或两个操作数都是 null,则提升运算符会生成值 false。 否则,提升运算符会解开操作数,应用基础运算符来生成 bool 结果。

12.5 成员查找

12.5.1 常规

成员查找是指确定类型上下文中名称的含义的过程。 在计算表达式中的 simple_name (§12.8.4) 或 member_access (§12.8.7) 时,可以进行成员查找。 如果 simple_namemember_access 作为 invocation_expressionprimary_expression 出现 (§12.8.10.2),则该成员会显示为已调用

如果成员是方法或事件,或者它是委托类型的常量、字段或属性(§20)或类型 dynamic§8.2.4),则表示该成员 可调用。

成员查找不仅考虑成员的名称,还考虑成员具有的类型参数数以及成员是否可访问。 出于成员查找的目的,泛型方法和嵌套泛型类型具有各自的声明中指示的类型参数数,所有其他成员都具有零类型参数。

在类型 T 中,使用 K 类型参数对名称 N 进行成员查找的处理过程如下:

  • 首先,确定一组名为 N 的可访问成员:
    • 如果 T 是一个类型参数,那么该集就是为 T 指定为主要约束或辅助约束 (§15.2.5) 的每个类型中名为 N 的可访问成员集的联合,以及 object 中名为 N 的可访问成员集。
    • 否则,该集合由 T中名为 N 的所有可访问成员(§7.5)组成,包括继承的成员,以及 object中名为 N 的可访问成员。 如果 T 是构造类型,则通过替换类型参数来获取成员集,如 §15.3.3中所述。 包含 override 修饰符的成员将从集中排除。
  • 接下来,如果 K 为零,则删除其声明包括类型参数的所有嵌套类型。 如果 K 不为零,则删除具有不同类型参数数量的所有成员。 当 K 为零时,不会删除具有类型参数的方法,因为类型推理过程(§12.6.3)可能能够推断类型参数。
  • 接下来,如果成员被调用,则所有不可调用的成员都会从集中移除。
  • 接下来,被其他成员隐藏的成员将从集合中移除。 对于集中的每个成员 S.M(其中 S 是声明成员 M 的类型),都要应用以下规则:
    • 如果 M 是常量、字段、属性、事件或枚举成员,则会从集中删除在基类型 S 声明的所有成员。
    • 如果 M 是类型声明,那么从集中删除在基类型 S 中声明的所有非类型元素,并且从集中删除所有在基类型 S 中声明、其类型参数数量与 M 相同的类型声明。
    • 如果 M 是一种方法,则会从集中删除在基类型 S 中声明的所有非方法成员。
  • 接下来,类成员隐藏的接口成员将从集合中被移除。 仅当 T 是类型参数,并且 T 具有除 object 以外的有效基类和非空有效接口集(§15.2.5)时,此步骤才有效。 对于集合中的每个成员 S.M,其中 S 是声明了成员 M 的类型,如果 S 是除 object以外的类声明,则应用以下规则:
    • 如果 M 是常量、字段、属性、事件、枚举成员或类型声明,则会从集中删除在接口声明中声明的所有成员。
    • 如果 M 是一种方法,则从集中删除在接口声明中声明的所有非方法成员,并且从集中删除与接口声明中声明 M 相同的签名的所有方法。
  • 最后,在移除隐藏成员后,就可以确定查找的结果:
    • 如果集由不是方法的单个成员组成,则此成员是查找的结果。
    • 否则,如果集仅包含方法,则此组方法是查找的结果。
    • 否则,查找将不明确,并会发生绑定时错误。

对于类型参数和接口以外的类型中的成员查找,以及严格的单一继承接口(继承链中的每个接口都有零个或一个直接的基接口)中的成员查找,查找规则的作用仅仅是派生成员隐藏具有相同名称或签名的基成员。 这种单一继承查找绝不会含糊不清。 多重继承接口中的成员查找可能产生的歧义在 §18.4.6 中进行了介绍。

注意:此阶段仅说明一种歧义。 如果成员查找结果为方法组,那么对方法组的进一步使用可能会由于歧义而失败,例如,如 §12.6.4.1§12.6.6.2中所述。 尾注

12.5.2 基类型

出于成员查找的目的,类型 T 被视为具有以下基类型:

  • 如果 Tobjectdynamic,则 T 没有基类型。
  • 如果 Tenum_type,则 T 的基类型是类类型 System.EnumSystem.ValueTypeobject
  • 如果 Tstruct_type,则 T 的基类型是类类型 System.ValueTypeobject

    注意nullable_value_type 是一个 struct_type (§8.3.1)。 尾注

  • 如果 Tclass_type,则 T 的基类型是 T的基类,包括类类型 object
  • 如果 Tinterface_type,则 T 的基类型是 T 的基本接口和类类型 object
  • 如果 Tarray_type,则 T 的基类型是类类型 System.Arrayobject
  • 如果 Tdelegate_type,则 T 的基类型是类类型 System.Delegateobject

12.6 函数成员

12.6.1 常规

函数成员是包含可执行语句的成员。 函数成员始终是类型的成员,不能是命名空间的成员。 C# 定义以下类别的函数成员:

  • 方法
  • 性能
  • 事件
  • 索引员
  • 用户定义的运算符
  • 实例构造函数
  • 静态构造函数
  • 终结器

除终结器和静态构造函数(无法显式调用)外,函数成员中包含的语句通过函数成员调用执行。 编写函数成员调用的实际语法取决于特定的函数成员类别。

函数成员调用的参数列表 (§12.6.2) 为函数成员的参数提供实际值或变量引用。

泛型方法的调用可能采用类型推理来确定要传递给该方法的类型参数集。 此过程在 §12.6.3中介绍。

方法、索引器、运算符和实例构造函数的调用采用重载解析来确定要调用的候选函数成员集。 此过程在 §12.6.4中介绍。

在绑定时识别特定函数成员(可能通过重载解析)后,函数成员的实际运行时调用过程在 §12.6.6中进行了描述。

注释:下表汇总了在涉及可显式调用的六类函数成员的构造中发生的处理。 在表中,exyvalue 指示分类为变量或值的表达式,T 指示分类为类型的表达式,F 是方法的简单名称,P 是属性的简单名称。

构造 描述
方法调用 F(x, y) 重载决策用于选择包含类或结构中的最佳方法 F。 使用参数列表 (x, y)调用该方法。 如果方法不是 static,则实例表达式为 this
T.F(x, y) 重载解析应用于在类或结构 T中选择最佳方法 F。 如果方法不符合 static,则会发生绑定时错误。 使用参数列表 (x, y)调用该方法。
e.F(x, y) 重载解析用于在类、结构或接口中选择由 e类型给出的最佳方法 F。 如果方法 static,则会发生绑定时错误。 该方法使用实例表达式 e 和参数列表 (x, y)调用。
P 会调用包含类或结构中属性 P 的 get 访问器。 如果 P 是只读的,则会发生编译时错误。 如果 P 不等于 static,则实例表达式为 this
P = value 使用参数表 (value) 调用包含类或结构中属性 P 的 set 访问器。 如果 P 为只读,则会发生编译时错误。 如果 P 不是 static,则实例表达式为 this
T.P 会调用包含类或结构 T 中属性 P 的 get 访问器。 如果 P 不等于 staticP 是只写的,则会发生编译时错误。
T.P = value 使用参数表 (value) 调用类或结构 T 中属性 P 的 set 访问器。 如果 P 不是 static 或者 P 是只读的,则会发生编译时错误。
e.P 使用实例表达式 e 调用 E 类型给出的类、结构或接口中属性 P 的 get 访问器。 如果 PstaticP 为只写入,则会发生绑定时错误。
e.P = value 使用实例表达式 e 和参数列表 (value) 调用 E 类型指定的类、结构或接口中属性 P 的 set 访问器。 如果 Pstatic 或者 P 是只读的,则会发生绑定时错误。
事件访问 E += value 调用包含类或结构中事件 E 的 add 访问器。 如果 E 不等于 static,则实例表达式是 this
E -= value 调用包含类或结构中事件 E 的 remove 访问器。 如果 E 不是 static,则实例表达式是 this
T.E += value 调用包含类或结构 T 中事件 E 的 add 访问器。 如果E不是static,则会发生绑定时间错误。
T.E -= value 调用包含类或结构 T 中事件 E 的 remove 访问器。 如果 E 未是 static,则会发生绑定时间错误。
e.E += value 使用实例表达式 e 调用 E 类型给出的类、结构或接口中事件 E 的 add 访问器。 如果 Estatic,就会发生绑定时错误。
e.E -= value 使用实例表达式 e 调用 E 类型给出的类、结构或接口中事件 E 的 remove 访问器。 如果 E 等于 static,则会发生绑定时错误。
索引器访问 e[x, y] 重载决策用于在 e 类型指定的类、结构或接口中选择最佳索引器。 使用实例表达式 e 和参数列表 (x, y)调用索引器的 get 访问器。 如果索引器为只写,则会发生绑定时错误。
e[x, y] = value 重载决策用于在 e 类型指定的类、结构或接口中选择最佳索引器。 使用实例表达式 e 和参数列表 (x, y, value) 调用索引器的 set 访问器。 如果索引器为只读,则会发生绑定时错误。
运算符调用 -x 重载解析应用于选择由 x类型给出的类或结构中最好的一元运算符。 使用参数列表 (x)调用所选运算符。
x + y 重载解析用于在类或结构中选择由 xy类型给出的最佳二进制运算符。 使用参数列表 (x, y)调用所选运算符。
实例构造函数调用 new T(x, y) 重载解析应用于在类或结构 T中选择最佳实例构造函数。 使用参数列表 (x, y)调用实例构造函数。

尾注

12.6.2 参数列表

12.6.2.1 常规

每个函数成员和委托调用都包含一个参数列表,该列表为函数成员的参数提供实际值或变量引用。 指定函数成员调用的参数列表的语法取决于函数成员类别:

  • 对于实例构造函数、方法、索引器和委托,参数以 argument_list 的形式指定,如下所述。 对于索引器,在调用 set 访问器时,参数列表还包括作为赋值运算符右操作数指定的表达式。

    注意:此附加参数不用于重载决策,仅在调用 set 访问器时使用。 尾注

  • 对于属性,调用 get 访问器时,参数列表为空;调用 set 访问器时,参数列表由作为赋值运算符右操作数的表达式组成。
  • 对于事件,参数列表由作为 +=-= 运算符右操作数指定的表达式组成。
  • 对于用户定义的运算符,参数列表由一元运算符的单一操作数或二元运算符的两个操作数组成。

属性(§15.7)和事件(§15.8)的参数始终作为值参数传递(§15.6.2.2)。 用户定义的运算符(§15.10)的参数始终作为值参数(§15.6.2.2)或输入参数(§9.2.8)传递。 索引器(§15.9)的参数始终作为值参数(§15.6.2.2)、输入参数(§9.2.8)或参数数组(§15.6.2.4) 传递。 这些类别的函数成员不支持输出和引用参数。

实例构造函数、方法、索引器或委托调用的参数指定为 argument_list

argument_list
    : argument (',' argument)*
    ;

argument
    : argument_name? argument_value
    ;

argument_name
    : identifier ':'
    ;

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' variable_reference
    ;

argument_list 由一个或多个参数组成,以逗号分隔。 每个参数都包含一个可选的 argument_name,后跟一个 argument_value。 具有 argument_nameargument 称为命名参数,而没有 argument_nameargument位置参数

argument_value 可以是以下形式之一:

  • 一个表达式,表示参数是作为值参数传递,还是转换为输入参数后再作为值参数传递,具体取决于 §12.6.4.2§12.6.2.3 中的描述。
  • 关键字 in 后跟一个 variable_reference (§9.5),表示参数是作为输入参数传递的 (§15.6.2.3.2)。 变量必须明确分配(§9.4),然后才能将其作为输入参数传递。
  • 关键字 ref 后跟一个 variable_reference (§9.5),表示参数是作为引用参数传递的 (§15.6.2.3.2)。 变量必须明确分配(§9.4),然后才能将其作为引用参数传递。
  • 关键字 out 后跟一个 variable_reference (§9.5),表示参数是作为输出参数传递的 (§15.6.2.3.2)。 在将变量作为输出参数传递的函数成员调用后,变量被视为已被确定赋值 (§9.4)。

形式决定了参数的参数传递模式:分别是输入引用输出。 但是,如上所述,具有值传递模式的参数可能会转换为具有输入传递模式的参数。

将可变字段(§15.5.4)作为输入、输出或引用参数传递会导致警告,因为该字段可能不会被调用的方法视为可变字段。

12.6.2.2 相应的参数

对于参数列表中的每个参数,被调用的函数成员或委托中都必须有一个对应的参数。

以下中使用的参数列表按如下方式确定:

  • 对于类中定义的虚拟方法和索引器,参数列表是从接收器的静态类型开始,在其基类中搜索到的函数成员的第一个声明或覆盖中提取的。
  • 对于分部方法,使用定义分部方法声明的参数列表。
  • 对于所有其他函数成员和委托而言,只有一个参数列表,即使用的参数列表。

参数或参数的位置定义为参数列表或参数列表中前面的参数或参数的数目。

函数成员参数的相应参数如下:

  • 实例构造函数、方法、索引器和委托的 argument_list 中的参数:
    • 除非参数是参数数组,且函数成员是以展开形式调用的,否则参数在参数列表中同一位置出现的位置参数与该参数相对应。
    • 以扩展形式调用的具有参数数组的函数成员的位置参数,如果出现在参数列表中参数数组的位置上或之后,则对应于参数数组中的一个元素。
    • 命名参数对应于参数列表中同名的参数。
    • 对于索引器,在调用 set 访问器时,作为赋值运算符右操作数指定的表达式与 set 访问器声明中的隐式 value 参数相对应。
  • 对于属性,调用 get 访问器时没有参数。 调用 set 访问器时,指定为赋值运算符右操作数的表达式对应于 set 访问器声明的隐式值参数。
  • 对于用户定义的一元运算符(包括转换),单个操作数对应于运算符声明的单个参数。
  • 对于用户定义的二进制运算符,左操作数对应于第一个参数,右操作数对应于运算符声明的第二个参数。
  • 当未命名参数位于已命名参数或与参数数组相对应的已命名参数之后时,该参数不对应任何参数。

    注意:这可以防止 M(c: false, valueB);调用 void M(bool a = true, bool b = true, bool c = true);。 第一个参数在位置外使用(参数在第一个位置使用,但名为 c 的参数位于第三位置),因此应命名以下参数。 换句话说,只有在名称和位置导致找到相同的相应参数时,才允许使用非尾随命名参数。 尾注

12.6.2.3 参数列表的运行时评估

在函数成员调用(§12.6.6)运行时处理期间,参数列表的表达式或变量引用按从左到右的顺序计算,如下所示:

  • 对于值参数,如果参数的传递模式为值

    • 计算参数表达式,并执行对相应参数类型的隐式转换(§10.2)。 生成的值将成为函数成员调用中的 value 参数的初始值。

    • 否则,参数的传递模式为输入。 如果参数是变量引用,并且参数的类型与参数的类型之间存在标识转换(§10.2.2),则生成的存储位置将成为函数成员调用中的参数表示的存储位置。 否则,会使用与相应参数相同的类型来创建存储位置。 计算参数表达式,并执行对相应参数类型的隐式转换(§10.2)。 生成的值存储在该存储位置中。 该存储位置由函数成员调用中的输入参数表示。

      示例:给定以下声明和方法调用:

      static void M1(in int p1) { ... }
      int i = 10;
      M1(i);         // i is passed as an input argument
      M1(i + 5);     // transformed to a temporary input argument
      

      M1(i) 方法调用中,i 本身作为输入参数传递,因为它被分类为变量,并且其类型与输入参数 int 相同。 在 M1(i + 5) 方法调用中,将创建一个未命名的 int 变量,使用参数的值进行初始化,然后作为输入参数传递。 请参阅 §12.6.4.2§12.6.4.4

      结束示例

  • 对于输入、输出或引用参数,变量引用将被计算,其生成的存储位置将成为函数调用中参数所指代的存储位置。 对于输入或引用参数,应在方法调用点明确分配变量。 如果变量引用作为输出参数提供,或是 reference_type的数组元素,则执行运行时检查以确保数组的元素类型与参数的类型相同。 如果此检查失败,则会抛出 System.ArrayTypeMismatchException 错误。

注意:由于数组协变(§17.6),此运行时检查是必需的。 尾注

示例:在以下代码中

class Test
{
    static void F(ref object x) {...}

    static void Main()
    {
        object[] a = new object[10];
        object[] b = new string[10];
        F(ref a[0]); // Ok
        F(ref b[1]); // ArrayTypeMismatchException
    }
}

第二次调用 F 会导致抛出 System.ArrayTypeMismatchException,因为 b 的实际元素类型是 string,而不是 object

结束示例

方法、索引器和实例构造函数可能将其最右的参数声明为参数数组(§15.6.2.4)。 根据适用的形式(§12.6.4.2),以正常形式或扩展形式调用此类函数成员:

  • 以正常形式调用具有参数数组的函数成员时,为参数数组提供的参数应为可隐式转换为参数数组类型的单个表达式(§10.2)。 在这种情况下,参数数组的行为与值参数类似。
  • 当以参数数组的扩展形式调用具有参数数组的函数成员时,调用应为参数数组指定零个或多个位置参数,其中每个参数都是可隐式转换为参数数组的元素类型的表达式(§10.2)。 在这种情况下,调用会创建参数数组类型的实例,其长度对应于参数数,使用给定参数值初始化数组实例的元素,并使用新创建的数组实例作为实际参数。

参数列表的表达式始终按文本顺序求值。

示例:因此,此示例

class Test
{
    static void F(int x, int y = -1, int z = -2) =>
        Console.WriteLine($"x = {x}, y = {y}, z = {z}");

    static void Main()
    {
        int i = 0;
        F(i++, i++, i++);
        F(z: i++, x: i++);
    }
}

生成输出

x = 0, y = 1, z = 2
x = 4, y = -1, z = 3

结束示例

当一个具有参数数组的函数成员以至少一个扩展参数的扩展形式被调用时,该调用的处理方式就好像在扩展参数周围插入了一个带有数组初始化器 (§12.8.17.5) 的数组创建表达式。 当参数数组没有参数时,会传递一个空数组;至于传递的引用是新分配的空数组还是现有的空数组,则没有指定。

示例:给定声明

void F(int x, int y, params object[] args);

方法的扩展形式的以下调用

F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);

完全对应于

F(10, 20, new object[] { 30, 40 });
F(10, 20, new object[] { 1, "hello", 3.0 });

结束示例

当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。 (这可以涉及创建存储位置,如上所述。

注意:因为这些值始终是常量,因此其计算不会影响其余参数的计算。 尾注

12.6.3 类型推理

12.6.3.1 常规

在没有指定类型参数的情况下调用泛型方法时,类型推理 进程尝试推断调用的类型参数。 类型推理的存在允许更方便的语法用于调用泛型方法,并允许程序员避免指定冗余类型信息。

示例

class Chooser
{
    static Random rand = new Random();

    public static T Choose<T>(T first, T second) =>
        rand.Next(2) == 0 ? first : second;
}

class A
{
    static void M()
    {
        int i = Chooser.Choose(5, 213); // Calls Choose<int>
        string s = Chooser.Choose("apple", "banana"); // Calls Choose<string>
    }
}

通过类型推断,从方法的参数中确定类型参数 intstring

结束示例

类型推理是在方法调用(§12.8.10.2)的绑定时间处理阶段发生,并且是在调用的重载解析步骤之前发生。 当在方法调用中指定特定方法组,并且没有将类型参数指定为方法调用的一部分时,类型推理将应用于方法组中的每个泛型方法。 如果类型推理成功,则推断的类型参数用于确定后续重载解析的参数类型。 如果重载解析选择泛型方法作为要调用的方法,则推断的类型参数将用作调用的类型参数。 如果特定方法的类型推理失败,则该方法不参与重载解析。 类型推理失败本身不会导致绑定时错误。 然而,当重载决策无法找到任何适用的方法时,往往会导致绑定时错误。

如果每个提供的参数没有准确对应于方法中的一个参数(§12.6.2.2),或者存在没有相应参数的非可选参数,那么推理将立即失败。 否则,假定泛型方法具有以下签名:

Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)

对于形式为 M(E₁ ...Eₓ) 的方法调用,类型推断的任务是为每个类型参数 X₁...Xᵥ 找到唯一的类型参数 S₁...Sᵥ,从而使调用 M<S₁...Sᵥ>(E₁...Eₓ) 变得有效。

下面将类型推理的过程描述为算法。 如果符合性编译器在所有情况下都达到相同的结果,则可以使用替代方法实现符合性编译器。

在推理过程中,每个类型参数 Xᵢ 要么固定为特定类型 Sᵢ,要么不固定,并带有一组相关的边界。每个边界都是某种类型的 T。 最初,每个类型变量 Xᵢ 未固定,并且具有空的边界集。

类型推理分阶段进行。 每个阶段将尝试根据上一阶段的发现推断更多类型变量的类型参数。 第一阶段对界限进行一些初始推理,而第二阶段则将类型变量固定为特定类型并推断出更多的界限。 第二阶段可能需要重复多次。

注意:类型推理也用于其他上下文,包括方法组转换(§12.6.3.14),并查找一组表达式的最佳常见类型(§12.6.3.15)。 尾注

12.6.3.2 第一阶段

对于每个方法参数 Eᵢ

  • 如果 Eᵢ 是匿名函数,则会进行EᵢTᵢ显式参数类型推理 (§12.6.3.8)。
  • 否则,如果 Eᵢ 的类型是 U,且相应的参数是值参数 (§15.6.2.2),则会进行UTᵢ下限推理 (§12.6.3.10)。
  • 否则,如果 Eᵢ 的类型为 U,且相应参数为引用参数 (§15.6.2.3.3) 或输出参数 (§15.6.2.3.4),则会进行UTᵢ确切推理 (§12.6.3.9)。
  • 否则,如果 Eᵢ 的类型是 U,且相应的参数是输入参数 (§15.6.2.3.2),而 Eᵢ 是输入参数,那么就会进行UTᵢ确切推理 (§12.6.3.9)。
  • 否则,如果 Eᵢ 的类型是 U,且相应的参数是输入参数 (§15.6.2.2),则会进行UTᵢ下限推理 (§12.6.3.10)。
  • 否则,不会对此参数进行推理。

12.6.3.3 第二阶段

第二阶段按如下所示进行:

  • 所有非固定类型的变量 Xᵢ,只要不依赖于 (§12.6.3.6) 任何被固定的 Xₑ (§12.6.3.12)。
  • 如果不存在这样的类型变量,那么所有非固定类型的变量 Xᵢ 都会被固定,对于这些类型变量,以下所有内容都将被保留:
    • 至少有一个依赖于Xᵢ的类型变量 Xₑ
    • Xᵢ 具有一个非空的界限集
  • 如果不存在此类类型变量,并且仍有 未修复 类型变量,则类型推理将失败。
  • 否则,如果不存在进一步的 未确定的 类型变量,则类型推断成功。
  • 否则,对于具有相应参数类型 Tᵢ 的所有参数 Eᵢ 而言,如果输出类型 (§12.6.3.5) 包含非固定类型的变量 Xₑ,但输入类型 (§12.6.3.4) 未包含的情况下,将进行EᵢTᵢ输出类型推理 (§12.6.3.7)。 然后重复第二个阶段。

12.6.3.4 输入类型

如果 E 是方法组或隐式类型的匿名函数,而 T 是委托类型或表达式树类型,那么 T 的所有参数类型都是类型为T输入类型E

12.6.3.5 输出类型

如果 E 是方法组或匿名函数,而 T 是委托类型或表达式树类型,那么 T 的返回类型就是类型为T输出类型E

12.6.3.6 依赖

如果对于某个类型为 TᵥXₑ 的参数 Eᵥ 出现在类型为 TᵥEᵥ输入类型中,并且 Xᵢ 出现在类型为 TᵥEᵥ 输出类型中,则非固定类型变量Xᵢ直接依赖于一个非固定类型变量 Xₑ

如果 Xₑ直接依赖于Xᵢ,或者 Xᵢ直接依赖于XᵥXᵥ依赖于Xₑ,则 Xₑ取决于Xᵢ。 因此,“依赖于”是“直接依赖于”的可传递的闭包,但不是自反的闭包。

12.6.3.7 输出类型推理

表达式 E 类型 T 的输出类型推论按以下方式进行:

  • 如果 E 是具有推断返回类型 U 的匿名函数(§12.6.3.13),并且 T 是具有返回类型 Tₓ的委托类型或表达式树类型,那么将根据 UTₓ进行 下限推理§12.6.3.10)。
  • 否则,如果 E 是一个方法组,而 T 是一个委托类型或表达式树类型,其参数类型为 T₁...Tᵥ 而返回类型为 Tₓ,并且使用类型 T₁...TᵥE 进行重载决策会得到一个返回类型为 U 的单一方法,那么就会进行UTₓ下限推理
  • 否则,如果 E 是一个类型为 U 的表达式,则会进行UT下限推理
  • 否则,不进行推理。

12.6.3.8 显式参数类型推理

显式参数类型推断表达式 E类型 T的推断:

  • 如果 E 是参数类型为 U₁...Uᵥ 的显式类型匿名函数,而 T 是参数类型为 V₁...Vᵥ 的委托类型或表达式树类型,那么对每个 Uᵢ 都要进行Uᵢ对应的 Vᵢ确切推理 (§12.6.3.9)。

12.6.3.9 精确推理

类型 U类型 V确切推理如下:

  • 如果 V中未固定的Xᵢ 之一,则将 U 添加到 Xᵢ的确切边界集。
  • 否则,集 V₁...VₑU₁...Uₑ 将通过检查是否存在以下情况来确定:
    • V 是数组类型 V₁[...]U 是相同排名的数组类型 U₁[...]
    • V 是类型 V₁?U 是类型 U₁
    • V 是构造类型 C<V₁...Vₑ>U 是构造类型 C<U₁...Uₑ>
      如果出现上述任何一种情况,那么就会从每个 Uᵢ 到相应的 Vᵢ 进行确切推理
  • 否则,不进行推理。

12.6.3.10 下限推理

从类型U类型 V下限推理如下:

  • 如果 是未固定之一,则将 添加到 的下限集。
  • 否则,如果 V 是类型 V₁?U 是类型 U₁?,则从 U₁V₁进行下限推理。
  • 否则,通过检查下列任何情况是否适用来确定集合 U₁...UₑV₁...Vₑ
    • V 是数组类型 V₁[...]U 是相同排名的数组类型 U₁[...]
    • VIEnumerable<V₁>ICollection<V₁>IReadOnlyList<V₁>>IReadOnlyCollection<V₁>IList<V₁> 之一,U 是单维数组类型 U₁[]
    • V 是一个构造的 classstructinterfacedelegate 类型 C<V₁...Vₑ>,并且存在一个唯一的类型 C<U₁...Uₑ>,使得 U(或者,如果 U 是一个类型 parameter,则其有效基类或其有效接口集的任何成员)与之相同,inherits 自(直接或间接)或实现(直接或间接)C<U₁...Uₑ>
    • (“唯一性”限制意味着,在接口 C<T>{} class U: C<X>, C<Y>{}的情况下,在从 U 推断到 C<T> 时不会进行推理,因为 U₁ 可能是 XY
      如果这些情况中的任何一种适用,那么就会从每个 Uᵢ 到相应的 Vᵢ 进行如下推理:
    • 如果不知道 Uᵢ 是引用类型,则会进行确切推理
    • 否则,如果 U 是数组类型,则会进行下限推理
    • 否则,如果 VC<V₁...Vₑ>,那么推断依赖于 Ci-th 类型参数。
      • 如果是协变,则会进行下限推理
      • 如果它是逆变类型的,则会进行上限推理
      • 如果它是固定的,则会进行确切推理
  • 否则,不进行推理。

12.6.3.11 上限推理

从类型U类型 V上限推理如下:

  • 如果 V中未固定的Xᵢ 之一,则将 U 添加到 Xᵢ的上限集。
  • 否则,集 V₁...VₑU₁...Uₑ 将通过检查是否存在以下情况来确定:
    • U 是数组类型 U₁[...]V 是相同排名的数组类型 V₁[...]
    • UIEnumerable<Uₑ>ICollection<Uₑ>IReadOnlyList<Uₑ>IReadOnlyCollection<Uₑ>IList<Uₑ> 之一,V 是单维数组类型 Vₑ[]
    • U 是类型 U1?V 是类型 V1?
    • U 是构建的类、结构、接口或委托类型 C<U₁...Uₑ>Videnticalclass, struct, interfacedelegate 类型,inherits 自(直接或间接)或(直接或间接)实现唯一类型 C<V₁...Vₑ>
    • (“唯一性”限制意味着给定接口 C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{},则在从 C<U₁> 推断到 V<Q>时,不会进行推理。推理不是从 U₁X<Q>Y<Q>
      如果这些情况中的任何一种适用,那么就会从每个 Uᵢ 到相应的 Vᵢ 进行如下推理:
    • 如果不知道 Uᵢ 是引用类型,则会进行确切推理
    • 否则,如果 V 是数组类型,则会进行上限推理
    • 否则,如果 UC<U₁...Uₑ>,那么推理依赖于 Ci-th 类型参数。
      • 如果它是协变类型的,则会进行上限推理
      • 如果它是逆变类型的,则会进行下限推理
      • 如果它是固定的,则会进行确切推理
  • 否则,不进行推理。

12.6.3.12 修复

具有一组绑定的非固定类型变量 Xᵢ固定如下:

  • 候选类型Uₑ集最初是 Xᵢ 绑定集中所有类型的集合。
  • 依次检查 Xᵢ 的每个绑定:对于 Xᵢ 的每个确切绑定 U,所有与 U 不相同的类型 Uₑ 都会从候选集中删除。 对于 Xᵢ 的每个下限 U,候选集中会删除所有类型 Uₑ,它们不会U 进行隐式转换。 对于 Xᵢ 的每个上限 U,候选集中将删除所有 Uₑ 类型,它们不会隐式转换为 U
  • 如果在剩余的候选类型 Uₑ 中,存在一个从所有其他候选类型可以隐式转换的唯一类型 V,则 Xᵢ 被固定为 V
  • 否则,类型推理失败。

12.6.3.13 推断返回类型

匿名函数的推断返回类型 F 用于类型推理和重载解析。 在所有参数类型已知的情况下,推断的返回类型只能用于匿名函数。这些参数类型要么是显式给定的,要么是通过匿名函数转换提供的,或者是在封闭泛型方法调用中的类型推理过程中推断出来的。

推断的有效返回类型 确定如下:

  • 如果 F 正文是具有类型的 表达式,则推断出的有效返回类型 F 是该表达式的类型。
  • 如果 F 主体是 ,并且块的 return 语句中的表达式集具有最佳常见类型 T§12.6.3.15),则推断出的有效返回类型为 FT
  • 否则,无法推断 F的有效返回类型。

推断的返回类型 确定如下:

  • 如果 F 是异步的,而 F 的主体是一个被归类为无的表达式 (§12.2),或者是一个没有 return 语句表达式的块,则推断的返回类型是 «TaskType» (§15.15.1)。
  • 如果 F 是异步的,并且其推断出的有效返回类型是 T,那么该推断的返回类型就是 «TaskType»<T>»§15.15.1)。
  • 如果 F 非异步且具有推断出的有效返回类型 T,则所推断的返回类型是 T
  • 否则,无法推断 F的返回类型。

示例:作为涉及匿名函数的类型推理的示例,请考虑在 System.Linq.Enumerable 类中声明的 Select 扩展方法:

namespace System.Linq
{
    public static class Enumerable
    {
        public static IEnumerable<TResult> Select<TSource,TResult>(
            this IEnumerable<TSource> source,
            Func<TSource,TResult> selector)
        {
            foreach (TSource element in source)
            {
                yield return selector(element);
            }
        }
   }
}

假设 System.Linq 命名空间是使用 using namespace 指令导入的,并且给定类 Customer 具有 string类型的 Name 属性,Select 方法可用于选择客户列表的名称:

List<Customer> customers = GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);

在处理 Select 的扩展方法调用 (§12.8.10.3) 时,会将调用重写为静态方法调用:

IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);

由于未显式指定类型参数,因此类型推理用于推断类型参数。 首先,客户的论点与源参数相关,推断TSourceCustomer。 然后,使用上述匿名函数类型推理过程,c 给定类型 Customer,表达式 c.Name 与选择器参数的返回类型相关,推断 TResultstring。 因此,调用等效于

Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)

并且结果的类型为 IEnumerable<string>

以下示例演示匿名函数类型推理如何允许类型信息在泛型方法调用中的参数之间“流”。 给定以下方法和调用:

class A
{
    static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2)
    {
        return f2(f1(value));
    }

    static void M()
    {
        double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours);
    }
}

调用的类型推理将按如下所示进行:首先,参数“1:15:30”与值参数相关,推断 X 为字符串。 然后,第一个匿名函数的参数 s给出推断的类型 string,表达式 TimeSpan.Parse(s)f1的返回类型相关,推断 YSystem.TimeSpan。 最后,第二个匿名函数(t)的参数得到推断的类型 System.TimeSpan,表达式 t.TotalHoursf2的返回类型相关,推断 Zdouble。 因此,调用的结果的类型为 double

结束示例

12.6.3.14 方法组转换的类型推理

与泛型方法的调用类似,当包含泛型方法的方法组 M 转换为给定委托类型 D§10.8)时,还应应用类型推理。 给定一种方法

Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)

方法组 M 被分配给委托类型 D,类型推理的任务是查找类型参数 S₁...Sᵥ,以便使表达式:

M<S₁...Sᵥ>

变得与 D 兼容 (§20.2)。

与泛型方法调用的类型推理算法不同,在这种情况下,只有参数 类型,没有参数 表达式。 具体而言,没有匿名函数,因此不需要多个推理阶段。

相反,所有 Xᵢ 都会被视为非固定,并 D 的每个参数类型 Uₑ M的相应参数类型 Tₑ 进行下限推理。 如果对于任何 Xᵢ 未找到边界,则类型推理将失败。 否则,所有 Xᵢ 都会固定到相应的 Sᵢ 中,这是类型推理的结果。

12.6.3.15 查找一组表达式的最佳常见类型

在某些情况下,需要为一组表达式推断通用类型。 具体而言,可以通过这种方式找到隐式类型数组的元素类型和具有 主体的匿名函数的返回类型。

表达式集合E₁...Eᵥ的最适合的通用类型如下:

  • 引入了一个新的 未固定 类型变量 X
  • 对于每个表达式 Ei,都要进行输出类型推理 (§12.6.3.7),并从中推理出 X
  • X固定的 (§12.6.3.12),如果可能,得到的类型是最佳通用类型。
  • 否则推理失败。

注意:直观地说,这种推理相当于调用方法 void M<X>(X x₁ ... X xᵥ)Eᵢ 作为参数和推断 X尾注

12.6.4 重载决策

12.6.4.1 常规

重载决策是一种绑定时机制,用于在给定参数列表和一组候选函数成员的情况下,选择要调用的最佳函数成员。 在 C# 中,重载解析在以下不同的上下文中选择要调用的函数成员:

  • 调用以 invocation_expression (§12.8.10) 命名的方法。
  • 调用以 object_creation_expression (§12.8.17.2) 命名的实例构造函数。
  • 通过 element_access (§12.8.12) 调用索引器访问器。
  • 调用表达式中引用的预定义或用户定义的运算符(§12.4.4§12.4.5)。

每个上下文都以自己的唯一方式定义候选函数成员集和参数列表。 例如,方法调用的候选项集不包括标记为重写的方法(§12.5),如果派生类中的任何方法适用(§12.8.10.2),则基类中的方法不是候选项。

确定候选函数成员和参数列表后,在所有情况下,最佳函数成员的选择都是相同的:

  • 首先,候选函数成员集将减少为适用于给定参数列表的函数成员(§12.6.4.2)。 如果此减少集为空,则会发生编译时错误。
  • 然后,从适用的候选函数成员集中选定最佳的函数成员。 如果集仅包含一个函数成员,则该函数成员是最佳函数成员。 否则,最佳函数成员是就给定参数列表而言优于所有其他函数成员的函数成员,前提是使用 §12.6.4.3 中的规则将每个函数成员与所有其他函数成员进行比较。 如果没有完全比所有其他函数成员更好的函数成员,则函数成员调用不明确,并且会发生绑定时错误。

以下子子句定义了术语适用的函数成员更好的函数成员的确切含义。

12.6.4.2 适用的函数成员

当以下条件全部为 true 时,就参数列表 A 而言,一个函数成员被称为适用的函数成员

  • A 中的每个参数都对应于函数成员声明中的参数,如 §12.6.2.2中所述,最多一个参数对应于每个参数,任何参数都不对应的任何参数都是可选参数。
  • 对于 A中的每个参数,参数的参数传递模式与相应参数的参数传递模式相同,并且
    • 对于值参数或参数数组,从参数表达式到相应参数的类型存在隐式转换(§10.2),
    • 对于引用或输出参数,参数表达式的类型(如果有)与相应参数的类型之间存在标识转换,或者
    • 对于输入参数,当相应参数具有 in 修饰符时,参数表达式(如有)的类型与相应参数的类型之间存在标识转换,或者
    • 对于输入参数,如果相应参数省略了 in 修饰符,则存在从参数表达式到相应参数类型的隐式转换 (§10.2)。

对于包含参数数组的函数成员,如果函数成员适用上述规则,则表示其 正常形式适用。 如果包含参数数组的函数成员在其普通形式中不适用,则函数成员可能改为适用于其 扩展形式

  • 扩展形式通过在函数成员声明中用参数数组的元素类型的零个或多个值参数替换参数数组来构造,从而使参数列表 A 中的参数数目与参数总数相匹配。 如果 A 的参数数少于函数成员声明中的固定参数数,则无法构造函数成员的扩展形式,因此不适用。
  • 否则,如果 A中每个参数满足以下条件之一,则适用展开形式:
    • 参数的参数传递模式与相应参数的参数传递模式相同,并且
      • 对于扩展创建的固定值参数或值参数,从参数表达式到相应参数的类型存在隐式转换(§10.2),
      • 对于旁引用参数,参数表达式的类型与相应参数的类型相同。
    • 参数的参数传递模式为值,相应参数的参数传递模式为输入,并且存在从参数表达式到相应参数类型的隐式转换(§10.2

当从参数类型到输入参数的参数类型的隐式转换是动态隐式转换(§10.2.10),则结果是未定义的。

示例:给定以下声明和方法调用:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
public static void M2(in int p1) { ... }
public static void Test()
{
    int i = 10; uint ui = 34U;

    M1(in i);   // M1(in int) is applicable
    M1(in ui);  // no exact type match, so M1(in int) is not applicable
    M1(i);      // M1(int) and M1(in int) are applicable
    M1(i + 5);  // M1(int) and M1(in int) are applicable
    M1(100u);   // no implicit conversion exists, so M1(int) is not applicable

    M2(in i);   // M2(in int) is applicable
    M2(i);      // M2(in int) is applicable
    M2(i + 5);  // M2(in int) is applicable
}

结束示例

  • 静态方法只有在通过类型的 simple_namemember_access 得到方法组时才适用。
  • 只有当方法组产生于 simple_name、通过变量或值的 member_accessbase_access 时,实例方法才适用。
    • 果方法组是由 simple_name 得出的,则实例方法只有在 this 允许访问 §12.8.14 时才适用。
  • 当方法组源自于 member_access 时(如 §12.8.7.2 所述,可以通过实例或类型),实例方法和静态方法都适用。
  • 一个泛型方法,其类型参数(显式指定或推断)并不完全满足其约束不适用。
  • 在方法组转换的上下文中,应存在标识转换(§10.2.2)或隐式引用转换(§10.2.8)从方法返回类型到委托的返回类型。 否则,候选方法不适用。

12.6.4.3 更好的函数成员

为了确定最优的函数成员,专门构建了一个精简后的参数列表 A,该列表仅包含原始参数列表中按顺序出现的自变量表达式,并排除所有 outref 参数。

按以下方式构造每个候选函数成员的参数列表:

  • 如果函数成员仅能以扩展形式使用,则使用扩展形式。
  • 没有相应参数的可选参数将从参数列表中删除
  • 从参数列表中删除引用和输出参数
  • 参数重新排序,使其与参数列表中的相应参数位于同一位置。

给定一个参数列表 A,其中包含一组参数表达式 {E₁, E₂, ..., Eᵥ},以及两个适用的函数成员 MᵥMₓ(参数类型分别为 {P₁, P₂, ..., Pᵥ}{Q₁, Q₂, ..., Qᵥ}),如果符合以下条件,则 Mᵥ 会被定义为比 Mₓ 更好的函数成员

  • 对于每个参数,从 EᵥQᵥ 的隐式转换并不优于从 EᵥPᵥ的隐式转换,并且
  • 对于至少一个参数,从 EᵥPᵥ 的转换优于从 Eᵥ 转换为 Qᵥ

如果参数类型序列 {P₁, P₂, ..., Pᵥ}{Q₁, Q₂, ..., Qᵥ} 是等效的(即每个 Pᵢ 与相应的 Qᵢ 有相同的转换),则依次采用以下决胜规则,以确定更好的函数成员。

  • 如果 Mᵢ 是非泛型方法,Mₑ 是泛型方法,则 Mᵢ 优于 Mₑ
  • 否则,如果 Mᵢ 适用于其正常形式,并且 Mₑ 具有参数数组,并且仅适用于其扩展形式,则 Mᵢ 优于 Mₑ
  • 否则,如果两种方法都具有参数数组,并且仅适用于其扩展形式,并且 Mᵢ 的参数数组的元素少于 Mₑ的参数数组,则 Mᵢ 优于 Mₑ
  • 否则,如果 Mᵥ 的参数类型比 Mₓ更具体,则 Mᵥ 优于 Mₓ。 让 {R1, R2, ..., Rn}{S1, S2, ..., Sn} 表示 MᵥMₓ的未经证实和未表达式的参数类型。 如果对于每个参数,Rx 不小于 Sx,并且对于至少一个参数,RxSx更具体,则 Mᵥ的参数类型比 Mₓ更具体:
    • 类型参数不特定于非类型参数。
    • 递归地,一个构造类型比另一个构造类型更具体(具有相同数量的类型参数)如果至少有一个类型参数更具体,并且没有任何类型参数比另一个构造类型中的相应类型参数更不具体。
    • 如果一个数组类型的元素类型比另一个数组类型(具有相同维数)的元素类型更具体,则该数组类型比另一个数组类型更具体。
  • 否则,如果一个成员是非提升运算符,而另一个成员是提升运算符,那么非提升运算符更好。
  • 如果两个函数成员都找不到更好的参数,并且 Mᵥ 的所有参数都具有相应的参数,而默认参数需要在 Mₓ中替换至少一个可选参数,则 Mᵥ 优于 Mₓ
  • 如果 Mᵥ 中至少有一个参数使用了比 Mₓ 中相应参数更好的参数传递选择 (§12.6.4.4),并且 Mₓ 中没有一个参数使用了比 Mᵥ 更好的参数传递选择,则 Mᵥ 优于 Mₓ
  • 否则,没有哪个函数成员更好。

12.6.4.4 更好的参数传递模式

允许两个重载方法中的相应参数仅在参数传递模式上有所不同,条件是其中一个参数具有值传递模式,如下所示:

public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }

给定的 int i = 10;,根据 §12.6.4.2,调用 M1(i)M1(i + 5) 会导致两个重载选项都适用。 在这种情况下,参数传递模式为值的方法是更好的参数传递模式选择

注意:输入、输出或引用传递模式的参数不存在此类选择,因为这些参数仅匹配完全相同的参数传递模式。 尾注

12.6.4.5 更好的从表达式转换

给定从表达式 E 转换为类型 T₁的隐式转换 C₁,以及从表达式 E 转换为类型 T₂的隐式转换 C₂,如果满足以下任一条件,C₁C₂ 更好的转换:

  • E 完全匹配 T₁,而 ET₂ 不完全匹配(§12.6.4.6
  • E 要么与 T₁T₂都完全匹配,要么与之都不匹配,而 T₁ 是比 T₂ 更好的转换目标(§12.6.4.7
  • E 是一个方法组(§12.2),T₁ 兼容于方法组中用于转换 C₁的单个最佳方法(§20.4),而 T₂ 不兼容于方法组中用于转换 C₂ 的单个最佳方法。

12.6.4.6 完全匹配的表达式

给定表达式 E 和类型 TE完全匹配T 如果满足以下任一条件:

  • E 具有类型 S,并且存在从 ST 的标识转换
  • E 是匿名方法,T 是委托类型 D 或表达式树类型 Expression<D>,并满足以下条件之一:
    • D 参数列表(§12.6.3.12)的上下文中,E 具有推断出的返回类型 X,并且存在从 XD 返回类型的身份转换。
    • E 是无返回值的 async lambda,D 的返回类型是非泛型的 «TaskType»
    • E 是非异步的,且 D 具有返回类型 Y,或者 E 是异步的,且 D 具有返回类型 «TaskType»<Y>§15.15.1),以下任一条件成立:
      • E 的正文是与 Y 完全匹配的表达式
      • E 的正文是一个块,其中每个返回语句都会返回一个与 Y 完全匹配的表达式

12.6.4.7 更好的转换目标

给定两种类型 T₁T₂,如果以下任一条件成立,T₁ 是一个比 T₂ 更好的 的转换目标

  • 存在从 T₁T₂ 的隐式转换,不存在从 T₂T₁ 的隐式转换
  • T₁«TaskType»<S₁>§15.15.1),T₂«TaskType»<S₂>S₁ 是比 S₂ 更好的转换目标
  • T₁«TaskType»<S₁>§15.15.1),T₂«TaskType»<S₂>T₁T₂ 更专业
  • T₁S₁S₁?,其中 S₁ 是有符号整数类型,T₂S₂S₂?,其中 S₂ 是无符号整型类型。 具体说来:
    • S₁sbyteS₂byteushortuintulong
    • S₁shortS₂ushortuintulong
    • S₁int,且 S₂uintulong
    • S₁longS₂ulong

12.6.4.8 泛型类中的重载

注意:虽然声明的签名应是唯一的(§8.6),但类型参数的替换可能导致相同的签名。 在这种情况下,如果存在原始签名(在类型参数替换之前),重载决策将选择最特殊的签名 (§12.6.4.3),否则将报错。 尾注

示例:以下示例显示根据此规则有效且无效的重载:

public interface I1<T> { ... }
public interface I2<T> { ... }

public abstract class G1<U>
{
    public abstract int F1(U u);           // Overload resolution for G<int>.F1
    public abstract int F1(int i);         // will pick non-generic

    public abstract void F2(I1<U> a);      // Valid overload
    public abstract void F2(I2<U> a);
}

abstract class G2<U,V>
{
    public abstract void F3(U u, V v);     // Valid, but overload resolution for
    public abstract void F3(V v, U u);     // G2<int,int>.F3 will fail

    public abstract void F4(U u, I1<V> v); // Valid, but overload resolution for
    public abstract void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail

    public abstract void F5(U u1, I1<V> v2);   // Valid overload
    public abstract void F5(V v1, U u2);

    public abstract void F6(ref U u);      // Valid overload
    public abstract void F6(out V v);
}

结束示例

12.6.5 动态成员调用的编译时检查

尽管动态绑定操作的重载决策是在运行时进行的,但有时在编译时也可以知道从中选择重载的函数成员列表:

  • 对于委托调用(§12.8.10.4),该列表是一个函数成员,其参数列表与调用 delegate_type 相同
  • 对于类型或静态类型不是动态的值的方法调用 (§12.8.10.2),方法组中的可访问方法集在编译时是已知的。
  • 对于对象创建表达式(§12.8.17.2),类型中的可访问构造函数集在编译时已知。
  • 对于索引器访问(§12.8.12.3),接收器中的可访问索引器集在编译时已知。

在这种情况下,会对已知函数成员集合中的每个成员进行有限的编译时检查,看是否可以确定在运行时永远不会调用。 对于每个函数成员 F,构造了修改后的参数和实参列表:

  • 首先,如果 F 是一个泛型方法并且给定了类型实参,那么这些类型实参将替换参数列表中的类型参数。 但是,如果未提供类型参数,则不会发生此类替换。
  • 然后,任何类型为开放的参数(即,包含类型参数;参见 §8.4.3)以及其对应的参数都被省略。

若要 F 通过检查,以下所有条件应满足:

  • F 的修改参数列表适用于 §12.6.4.2 的修改参数表。
  • 修改的参数列表中的所有构造类型都满足其约束(§8.4.5)。
  • 如果在上述步骤中替换了 F 的类型参数,则满足其约束。
  • 如果 F 是静态方法,则方法组不得由其接收者在编译时已知是变量或值的 member_access 产生。
  • 如果 F 是实例方法,则该方法组不应由其接收者在编译时已知是一个类型的 member_access 产生。

如果没有候选项通过此测试,则会出现编译时错误。

12.6.6 函数成员调用

12.6.6.1 常规

这个小节描述了在运行时调用特定函数成员的过程。 假设绑定时进程已经确定了要调用的特定成员,可能是通过对一组候选函数成员应用重载决策。

为了描述调用过程,函数成员分为两类:

  • 静态函数成员。 这些是静态方法、静态属性访问器和用户定义的运算符。 静态函数成员始终为非虚拟成员。
  • 实例函数成员。 这些是实例方法、实例构造函数、实例属性访问器和索引器访问器。 实例函数成员可以是虚拟的或非虚拟的,并且始终在特定实例上执行调用。 实例通过实例表达式计算,并可在函数成员中以 this (§12.8.14) 的形式来访问。 对于实例构造函数,将实例表达式设置为新分配的对象。

函数成员调用的运行时处理包括以下步骤,其中 M 是函数成员,如果 M 是实例成员,E 是实例表达式:

  • 如果 M 是静态函数成员:

    • 参数列表的计算方式为 §12.6.2中所述。
    • M 被调用。
  • 否则,如果 E 的类型是值类型 V,并且在 V 中声明或重写了 M

    • E 已计算。 如果此评估导致异常,则不会执行进一步的步骤。 对于实例构造函数而言,这一计算包括为新对象分配存储空间(通常来自执行堆栈)。 在这种情况下,E 被归类为变量。
    • 如果 E 未分类为变量,或者 V 不是只读结构类型(§16.2.2),E 为以下类型之一:

    然后会创建 E类型的临时局部变量,并将 E 的值分配给该变量。 然后,E 重新分类为对该临时局部变量的引用。 在 M中,临时变量可通过 this 访问,但不能以任何其他方式访问。 因此,只有在可以编写 E 时,调用方才能观察 Mthis所做的更改。

    • 参数列表的计算方式为 §12.6.2中所述。
    • M 被调用。 E 引用的变量将成为由 this引用的变量。
  • 否则:

    • E 已计算。 如果此评估导致异常,则不会执行进一步的步骤。
    • 参数列表的计算方式为 §12.6.2中所述。
    • 如果 E 的类型是 value_type,则要执行装箱转换 (§10.2.9),将 E 转换为 class_type,并在以下步骤中将 E 视为该 class_type 的类型。 如果 value_typeenum_type,那么class_typeSystem.Enum;,否则是 System.ValueType
    • 检查 E 的值是否有效。 如果 E 的值为 null,则会引发 System.NullReferenceException,并且不再执行其他步骤。
    • 确定要调用的函数成员实现:
      • 如果 E 的绑定时类型是接口,则要调用的函数成员是由 E引用的实例的运行时类型提供的 M 的实现。 此函数成员通过应用接口映射规则 (§18.6.5) 来确定 M 的实现,该实现由 E 引用的实例的运行时类型提供。
      • 否则,如果 M 是一个虚拟函数成员,则要调用的函数成员是由 E所引用实例的运行时类型所提供的 M 实现。 此函数成员通过应用规则来确定与 E引用的实例的运行时类型相关的 M 的最派生实现(§15.6.4)。
      • 否则,M 是一个非虚拟函数成员,要调用的函数成员是 M 本身。
    • 调用上一步确定的函数成员实现。 E 引用的对象将成为由此引用的对象。

调用实例构造函数(§12.8.17.2)的结果是创建的值。 调用任何其他函数成员的结果是其主体中返回的值(如果有)(§13.10.5)。

12.6.6.2 装箱实例上的调用

在以下情况时,可以通过 value_type 的装箱实例来调用 value_type 中实现的函数成员:

  • 当函数成员是从 class_type 类型继承的方法的重写,并通过 class_type 的实例表达式调用时。

    注意class_type 始终是 System.ObjectSystem.ValueTypeSystem.Enum之一。  尾注

  • 当函数成员是接口成员函数的实现,并通过 interface_type 的实例表达式调用时。
  • 当函数成员通过委托调用时。

在这种情况下,装箱实例会被视为包含 value_type 变量,该变量将成为函数成员调用中该变量所引用的变量。

注意:具体而言,这意味着在装箱实例上调用函数成员时,函数成员可以修改装箱实例中包含的值。 尾注

12.7 解构

解构是一个进程,其中表达式转换为单个表达式的元组。 当简单赋值的目标为元组表达式时,将使用析构来获取要分配给每个元组元素的值。

表达式 E 可以按以下方式解构为包含 n 元素的元组表达式:

  • 如果 E 是具有 n 元素的元组表达式,则解构的结果是表达式本身 E
  • 否则,如果 E 具有具有 n 元素的元组类型 (T1, ..., Tn),则 E 计算为临时变量 __v,析构的结果是表达式 (__v.Item1, ..., __v.Itemn)
  • 否则,如果表达式 E.Deconstruct(out var __v1, ..., out var __vn) 编译时解析为唯一实例或扩展方法,则计算该表达式,解构的结果是表达式 (__v1, ..., __vn)。 这种方法称为解构函数
  • 否则,无法解构 E

此处,__v__v1, ..., __vn 引用其他不可见且不可访问的临时变量。

注意:无法解构 dynamic 类型的表达式。 尾注

12.8 主要表达式

12.8.1 常规

主表达式包括最简单的表达式形式。

primary_expression
    : primary_no_array_creation_expression
    | array_creation_expression
    ;

primary_no_array_creation_expression
    : literal
    | interpolated_string_expression
    | simple_name
    | parenthesized_expression
    | tuple_expression
    | member_access
    | null_conditional_member_access
    | invocation_expression
    | element_access
    | null_conditional_element_access
    | this_access
    | base_access
    | post_increment_expression
    | post_decrement_expression
    | null_forgiving_expression
    | object_creation_expression
    | delegate_creation_expression
    | anonymous_object_creation_expression
    | typeof_expression
    | sizeof_expression
    | checked_expression
    | unchecked_expression
    | default_value_expression
    | nameof_expression    
    | anonymous_method_expression
    | pointer_member_access     // unsafe code support
    | pointer_element_access    // unsafe code support
    | stackalloc_expression
    ;

注意:这些语法规则不符合 ANTLR 的要求,因为它们是 ANTLR 无法处理的一组互相左递归规则(primary_expressionprimary_no_array_creation_expressionmember_accessinvocation_expressionelement_accesspost_increment_expressionpost_decrement_expressionnull_forgiving_expressionpointer_member_accesspointer_element_access)的一部分。 可以使用标准技术对语法进行转换,以消除互左递归。 这并没有完成,因为并非所有分析策略都需要它(例如 LALR 分析器不会),这样做会模糊化结构和说明。 尾注

pointer_member_access§23.6.3)和 pointer_element_access§23.6.4)仅在不安全的代码(§23)中可用。

主表达式分为 array_creation_expressionprimary_no_array_creation_expression。 以这种方式处理 array_creation_expression 而不是将其与其他简单表达式一起列出,可使语法不允许使用以下可能引起混淆的代码

object o = new int[3][1];

否则会被解释为

object o = (new int[3])[1];

12.8.2 字面量

字面量 (§6.4.5) 组成的 primary_expression 被归类为值。

12.8.3 内插字符串表达式

一个 interpolated_string_expression 包含 $$@$@,紧随其后的是 " 字符内的文本。 在带引号的文本中,有零个或多个 插值,由 {} 字符分隔,其中每个插值都包含 表达式 和可选的格式说明。

内插字符串表达式有两种形式:正则表达式 (interpolated_regular_string_expression) 和逐字字符串表达式 (interpolated_verbatim_string_expression);它们在词法上与字符串字面量的两种形式相似,但在语义上有所不同 (§6.4.5.6)。

interpolated_string_expression
    : interpolated_regular_string_expression
    | interpolated_verbatim_string_expression
    ;

// interpolated regular string expressions

interpolated_regular_string_expression
    : Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
      ('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
      Interpolated_Regular_String_End
    ;

regular_interpolation
    : expression (',' interpolation_minimum_width)?
      Regular_Interpolation_Format?
    ;

interpolation_minimum_width
    : constant_expression
    ;

Interpolated_Regular_String_Start
    : '$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Regular_String_Mid
    : Interpolated_Regular_String_Element+
    ;

Regular_Interpolation_Format
    : ':' Interpolated_Regular_String_Element+
    ;

Interpolated_Regular_String_End
    : '"'
    ;

fragment Interpolated_Regular_String_Element
    : Interpolated_Regular_String_Character
    | Simple_Escape_Sequence
    | Hexadecimal_Escape_Sequence
    | Unicode_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Regular_String_Character
    // Any character except " (U+0022), \\ (U+005C),
    // { (U+007B), } (U+007D), and New_Line_Character.
    : ~["\\{}\u000D\u000A\u0085\u2028\u2029]
    ;

// interpolated verbatim string expressions

interpolated_verbatim_string_expression
    : Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
      ('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
      Interpolated_Verbatim_String_End
    ;

verbatim_interpolation
    : expression (',' interpolation_minimum_width)?
      Verbatim_Interpolation_Format?
    ;

Interpolated_Verbatim_String_Start
    : '$@"'
    | '@$"'
    ;

// the following three lexical rules are context sensitive, see details below

Interpolated_Verbatim_String_Mid
    : Interpolated_Verbatim_String_Element+
    ;

Verbatim_Interpolation_Format
    : ':' Interpolated_Verbatim_String_Element+
    ;

Interpolated_Verbatim_String_End
    : '"'
    ;

fragment Interpolated_Verbatim_String_Element
    : Interpolated_Verbatim_String_Character
    | Quote_Escape_Sequence
    | Open_Brace_Escape_Sequence
    | Close_Brace_Escape_Sequence
    ;

fragment Interpolated_Verbatim_String_Character
    : ~["{}]    // Any character except " (U+0022), { (U+007B) and } (U+007D)
    ;

// lexical fragments used by both regular and verbatim interpolated strings

fragment Open_Brace_Escape_Sequence
    : '{{'
    ;

fragment Close_Brace_Escape_Sequence
    : '}}'
    ;

上述定义的词法规则中有六条对上下文敏感,具体如下:

规则 上下文要求
Interpolated_Regular_String_Mid 只能在 Interpolated_Regular_String_Start 之后、任何后续内插之间以及相应的 Interpolated_Regular_String_End 之前识别。
Regular_Interpolation_Format 只能在 regular_interpolation 中识别,并且起始冒号 (:) 不能嵌套在任何类型的括号(小括号/大括号/方括号)中。
Interpolated_Regular_String_End 只有在 Interpolated_Regular_String_Start 之后,并且只有当中间的任何标记是 Interpolated_Regular_String_Mid 或可以成为 regular_interpolation 一部分的标记,包括包含在这些内插中的任何 interpolated_regular_string_expression 的标记时,才会被识别。
Interpolated_Verbatim_String_MidVerbatim_Interpolation_FormatInterpolated_Verbatim_String_End 这三种规则的识别方法与上述相应规则的识别方法相同,但其中提到的每一条正则表达式语法规则都被相应的逐字语法规则所取代。

注意事项:上述规则对上下文敏感,因为它们的定义与语言中其他标记的定义有重叠。 尾注

注意:由于词法规则对上下文敏感,因此上述语法不支持 ANTLR。 与其他词法生成器一样,ANTLR 支持上下文敏感词法规则,例如使用其 词法模式,但这是实现细节,因此不属于此规范。 尾注

interpolated_string_expression 被归类为值。 如果一个表达式通过隐式内插字符串转换立即变为 System.IFormattableSystem.FormattableString§10.2.5),那么这个内插字符串表达式就具有该类型。 否则,它的类型为 string

注意内插字符串表达式的可能类型之间的区别可以从 System.String (§C.2) 和 System.FormattableString (§C.3) 的文档来确定。 尾注

内插(包括 regular_interpolationverbatim_interpolation 两种内插)的含义是将表达式的值格式化为 string,格式可以是 Regular_Interpolation_FormatVerbatim_Interpolation_Format 指定的格式,也可以是表达式类型的默认格式。 然后,格式化后的字符串会根据 interpolation_minimum_width 进行修改(如果有的话),以生成最终的 string 内插到 interpolated_string_expression 中。

注意:有关如何确定类型的默认格式的详细信息,请参阅 System.String§C.2)和 System.FormattableString§C.3)。 标准格式的说明与 Regular_Interpolation_FormatVerbatim_Interpolation_Format相同,可以在 System.IFormattable§C.4)的文档以及标准库(§C)的其他类型中找到。 尾注

interpolation_minimum_width 中,constant_expression 应隐式转换为 int。 让字段宽度成为 constant_expression 的绝对值,让对齐方式成为 constant_expression 值的符号(正或负):

  • 如果字段宽度的值小于或等于格式化字符串的长度,则不会修改格式化字符串。
  • 否则,格式化后的字符串将通过填充空格字符来使其长度达到字段宽度。
    • 如果对齐方式为正对齐,则格式化后的字符串将通过预填充进行右对齐,
    • 否则会通过追加填充来进行左对齐。

interpolated_string_expression 的整体含义(包括上述内插的格式化和填充)是通过将表达式转换为方法调用来定义的:如果表达式的类型是 System.IFormattableSystem.FormattableString,则该方法是 System.Runtime.CompilerServices.FormattableStringFactory.Create (§C.3),返回 System.FormattableString 类型的值;否则,类型应为 string,方法是 string.Format (§C.2),它会返回 string 类型的值。

在这两种情况下,调用的参数列表都由一个格式字符串字面量格式规范组成,后者用于每个内插,而每个表达式都有一个与格式规范相对应的参数。

格式字符串字面量的构造如下,其中 Ninterpolated_string_expression 中的内插次数。 格式字符串字面量依次包括:

  • Interpolated_Regular_String_StartInterpolated_Verbatim_String_Start 的字符
  • Interpolated_Regular_String_MidInterpolated_Verbatim_String_Mid 的字符,如有
  • 然后,如果 N ≥ 10N-1 的每个数字 I
    • 占位符规范:
      • 一个左括号 ({) 字符
      • I 的十进制表示形式
      • 然后,如果相应的 regular_interpolationverbatim_interpolation 有一个 interpolation_minimum_width,则在逗号 (,) 后面加上 constant_expression 值的十进制表示法
      • 相应 regular_interpolationverbatim_interpolationRegular_Interpolation_FormatVerbatim_Interpolation_Format 的字符(如有)
      • 一个右括号 (}) 字符
    • 紧随相应内插之后的 Interpolated_Regular_String_MidInterpolated_Verbatim_String_Mid 字符(如有)。
  • 最后,Interpolated_Regular_String_EndInterpolated_Verbatim_String_End 的字符。

随后的参数是内插中按顺序排列的表达式(如有)。

interpolated_string_expression 包含多个内插时,这些内插中的表达式将按照从左到右的文本顺序进行求值。

示例

此示例使用以下格式规范功能:

  • X 格式规范,将整数格式设置为大写十六进制,
  • string 值的默认格式是值本身,
  • 在指定的最小字段宽度内右对齐的正对齐值,
  • 在指定的最小字段宽度内左对齐的负对齐值,
  • interpolation_minimum_width 定义的常量,以及
  • {{}} 分别格式化为 {}

假定为:

string text = "red";
int number = 14;
const int width = -4;

然后:

内插字符串表达式 等效的含义,如 string
$"{text}" string.Format("{0}", text) "red"
$"{{text}}" string.Format("{{text}}) "{text}"
$"{ text , 4 }" string.Format("{0,4}", text) " red"
$"{ text , width }" string.Format("{0,-4}", text) "red "
$"{number:X}" string.Format("{0:X}", number) "E"
$"{text + '?'} {number % 3}" string.Format("{0} {1}", text + '?', number % 3) "red? 2"
$"{text + $"[{number}]"}" string.Format("{0}", text + string.Format("[{0}]", number)) "red[14]"
$"{(number==0?"Zero":"Non-zero")}" string.Format("{0}", (number==0?"Zero":"Non-zero")) "Non-zero"

结束示例

12.8.4 简单名称

simple_name 由一个标识符组成,后面可选择跟一个类型参数列表:

simple_name
    : identifier type_argument_list?
    ;

simple_name 的形式可以是 II<A₁, ..., Aₑ>,其中 I 是一个单个标识符,I<A₁, ..., Aₑ> 是可选 type_argument_list。 如果未指定任何 type_argument_list,请将 e 视为零。 simple_name 的计算和分类如下:

  • 如果 e 为零,并且 simple_name 出现在本地变量声明空间(§7.3)中,该局部变量、参数或常量直接包含名称 I的参数或常量,则 simple_name 引用该局部变量、参数或常量,并归类为变量或值。
  • 如果 e 为零,并且 simple_name 出现在泛型方法声明中,但不在其 method_declarationattributes 中,并且如果该声明包含名称为 I 的类型参数,则 simple_name 将指向该类型参数。
  • 否则,对于每个实例类型 T (§15.3.2),从紧接着的封闭类型声明的实例类型开始,然后从每个封闭类或结构声明的实例类型开始(如有):
    • 如果 e 为零,并且 T 声明包含名称为 I的类型参数,则 simple_name 引用该类型参数。
    • 否则,如果 TI 的成员查找 (§12.5) 与 e 类型参数产生了匹配项:
      • 如果 T 是立即封闭类或结构类型的实例类型,并且查找确定了一个或多个方法,那么结果就是一个方法组,其相关的实例表达式为 this。 如果指定了类型参数列表,则用于调用泛型方法(§12.8.10.2)。
      • 否则,如果 T 是立即封闭类或结构类型的实例类型, 如果查找标识实例成员,并且引用发生在实例构造函数、实例方法或实例访问器(§12.2.1)的 中,则结果与表单 this.I的成员访问(§12.8.7)相同。 这只能在 e 为零时发生。
      • 否则,结果与 T.IT.I<A₁, ..., Aₑ> 形式的成员访问 (§12.8.7) 相同。
  • 否则,对于每个命名空间 N,从 simple_name 所在的命名空间开始,继续每个封闭的命名空间(如果有),直到全局命名空间,以下步骤将被执行,直到找到一个实体:
    • 如果 e 为零,IN中的命名空间的名称,则:
      • 如果 simple_name 所在的位置被 N 的命名空间声明所括住,并且命名空间声明包含将 I 与命名空间或类型关联的 extern_alias_directiveusing_alias_directive,那么 simple_name 就会产生歧义,并出现编译时错误。
      • 否则,simple_name 将指向 N 中名为 I 的命名空间。
    • 否则,如果 N 包含一个具有名称 Ie 型参数的可访问类型,则:
      • 如果 e 为零,并且 simple_name 出现的位置被 N 命名空间声明包含,且该命名空间声明中包含一个 extern_alias_directiveusing_alias_directive 将名称 I 与某个命名空间或类型相关联,则该 simple_name 是不明确的,并会导致编译时错误。
      • 否则,namespace_or_type_name 指代由给定类型参数构造的类型。
    • 否则,如果发生 simple_name 的位置由 N的命名空间声明括起来:
      • 如果 e 为零,并且命名空间声明包含 extern_alias_directiveusing_alias_directive 将名称 I 与导入的命名空间或类型关联,则 simple_name 指向该命名空间或类型。
      • 否则,如果命名空间声明的 using_namespace_directive 所导入的命名空间正好包含一个名称为 Ie 类型参数的类型,那么 simple_name 将指向使用给定类型参数构造的类型。
      • 否则,如果命名空间声明的 using_namespace_directive 所导入的命名空间包含多个名称为 Ie 类型参数的类型,那么 simple_name 就会产生歧义,并出现编译时错误。

    注意:此步骤与处理 namespace_or_type_name§7.8)中的相应步骤完全并行。 尾注

  • 否则,如果 e 为零,且 I 为标识符 _,则 simple_namesimple discard,它是声明表达式的一种形式 (§12.17)。
  • 否则,simple_name 未定义且发生编译时错误。

12.8.5 括号表达式

一个 parenthesized_expression 包含一个表达式并用括号括起来。

parenthesized_expression
    : '(' expression ')'
    ;

parenthesized_expression 进行求值时,要对括号内的表达式进行求值。 如果括号内的 表达式 表示命名空间或类型,则会发生编译时错误。 否则,parenthesized_expression 的结果就是所包含的表达式的求值结果。

12.8.6 元组表达式

tuple_expression 表示一个元组,由两个或多个逗号分隔的、可选命名的表达式组成,并用括号括起来。 deconstruction_expression 是包含隐式类型声明表达式的元组的简写语法。

tuple_expression
    : '(' tuple_element (',' tuple_element)+ ')'
    | deconstruction_expression
    ;
    
tuple_element
    : (identifier ':')? expression
    ;
    
deconstruction_expression
    : 'var' deconstruction_tuple
    ;
    
deconstruction_tuple
    : '(' deconstruction_element (',' deconstruction_element)+ ')'
    ;

deconstruction_element
    : deconstruction_tuple
    | identifier
    ;

tuple_expression 被归类为元组。

deconstruction_expressionvar (e1, ..., en)tuple_expression(var e1, ..., var en) 的简称,两者具有相同的行为。 这将递归地应用于 deconstruction_expression 中的任何嵌套 deconstruction_tuple。 因此,嵌套在 deconstruction_expression 中的每个标识符都引入了声明表达式(§12.17)。 因此,deconstruction_expression 只能出现在简单赋值的左侧。

当且仅当元组表达式 Ei 的每个元素表达式都有类型 Ti 时,元组表达式才有类型。 类型应是与元组表达式具有相同迭代度的元组类型,其中每个元素由以下内容给出:

  • 如果相应位置的元组元素具有名称 Ni,则元组类型元素应 Ti Ni
  • 否则,如果 EiNiE.NiE?.Ni 的形式,那么元组类型元素应为 Ti Ni除非以下任一条件成立:
    • 元组表达式的另一个元素名称为 Ni,或
    • 没有名称的另一个元组元素具有格式为 NiE.NiE?.Ni的元组元素表达式,或者
    • NiItemX 的形式,其中 X 是一个非 0 开始的十进制数字序列,可以表示元组元素的位置,而 X 并不表示元素的位置。
  • 否则,元组类型元素应为 Ti

元组表达式的求值方法是按从左到右的顺序求值每个元素表达式。

可以通过将元组表达式转换为元组类型(§10.2.13)、重新分类为值(§12.2.2),或者使其成为解构赋值的目标(§12.21.2)来获取元组值。

示例

(int i, string) t1 = (i: 1, "One");
(long l, string) t2 = (l: 2, null);
var t3 = (i: 3, "Three");          // (int i, string)
var t4 = (i: 4, null);             // Error: no type

在此示例中,所有四个元组表达式都有效。 前两个,t1t2,不使用元组表达式的类型,而是应用隐式元组转换。 对于 t2,隐式元组转换依赖于从 2long 和从 nullstring的隐式转换。 第三个元组表达式的类型是 (int i, string),因此可以重新分类为该类型的值。 另一方面,t4的声明是错误的:因为元组表达式的第二个元素没有类型,所以整个元组表达式没有类型。

if ((x, y).Equals((1, 2))) { ... };

此示例显示元组有时可能会导致多层括号,尤其是在元组表达式是方法调用的唯一参数时。

结束示例

12.8.7 成员访问

12.8.7.1 常规

member_accessprimary_expressionpredefined_typequalified_alias_member 组成,后面跟一个“.”标记,再跟一个标识符标记,最后跟一个 type_argument_list

member_access
    : primary_expression '.' identifier type_argument_list?
    | predefined_type '.' identifier type_argument_list?
    | qualified_alias_member '.' identifier type_argument_list?
    ;

predefined_type
    : 'bool' | 'byte' | 'char' | 'decimal' | 'double' | 'float' | 'int'
    | 'long' | 'object' | 'sbyte' | 'short' | 'string' | 'uint' | 'ulong'
    | 'ushort'
    ;

qualified_alias_member 生产在 §14.8 中定义。

member_access 可以是 E.I 形式,也可以是 E.I<A₁, ..., Aₑ> 形式,其中 Eprimary_expressionpredefined_typequalified_alias_member,I 是单个标识符,<A₁, ..., Aₑ> 是可选的 type_argument_list。 如果未指定任何 type_argument_list,请将 e 视为零。

具有类型 dynamicprimary_expressionmember_access 会被动态绑定 (§12.3.3)。 在这种情况下,编译器将成员访问分类为类型 dynamic的属性访问。 然后,在运行时使用运行时类型而不是 primary_expression 的编译时类型,应用下面的规则来确定 member_access 的含义。 如果运行时分类导致方法组,那么成员访问应是 invocation_expressionprimary_expression

member_access 的计算和分类如下:

  • 如果 e 为零,E 为命名空间,并且 E 包含名称 I的嵌套命名空间,则结果是该命名空间。
  • 否则,如果 E 是命名空间,并且 E 包含具有名称 IK 类型参数的可访问类型,则结果是使用给定类型参数构造的类型。
  • 如果 E 被归类为类型,且 E 不是类型参数,并且在 E 中通过使用 K 类型参数对 I 进行成员查找(§12.5)后匹配成功,则对 E.I 进行评估并按如下分类:

    注意:当此类成员查找的结果为方法组且 K 为零时,方法组可以包含具有类型参数的方法。 这允许将此类方法视为类型参数推理。 尾注

    • 如果 I 标识类型,则结果是使用任何给定类型参数构造的类型。
    • 如果 I 标识一个或多个方法,则结果是没有关联的实例表达式的方法组。
    • 如果 I 标识静态属性,则结果是没有关联的实例表达式的属性访问。
    • 如果 I 标识静态字段:
      • 如果字段是只读的,并且引用发生在声明字段的类或结构的静态构造函数之外,则结果为一个值,即 E中静态字段 I 的值。
      • 否则,结果是一个变量,即 E中的静态字段 I
    • 如果 I 标识静态事件:
      • 如果引用发生在声明事件的类或结构中,并且事件是在没有 event_accessor_declarations§15.8.1)的情况下被声明的,那么 E.I 的处理方式完全如同 I 是一个静态字段。
      • 否则,结果将是一个没有相关实例表达式的事件访问。
    • 如果 I 标识常量,则结果为一个值,即该常量的值。
    • 如果 I 标识枚举成员,则结果为一个值,即该枚举成员的值。
    • 否则,E.I 是无效的成员引用,并且会发生编译时错误。
  • 如果 E 是一个属性访问、索引器访问、变量或值,其类型为 T,并且在 T 中使用 K 类型参数查找 I 的成员(第12.5节)产生匹配,则评估 E.I 并按如下方式分类:
    • 首先,如果 E 是属性或索引器访问,则获取属性或索引器访问的值(§12.2.2),E 将重新分类为值。
    • 如果 I 标识一个或多个方法,则结果是具有关联实例表达式 E的方法组。
    • 如果 I 标识一个实例属性,那么结果是一个属性访问,该访问具有与 E 相关的实例表达式,并且关联类型是该属性的类型。 如果 T 是类类型,则从 T 开始搜索基类时发现的第一个属性声明或重写中选择相关类型。
    • 如果 Tclass_typeI 标识该 class_type的实例字段:
      • 如果 E 的值为 null,则会引发 System.NullReferenceException
      • 否则,如果字段是只读的,并且引用发生在声明字段的类的实例构造函数之外,则结果为一个值,即由 E引用的对象中的字段 I 的值。
      • 否则,结果是一个变量,即 E引用的对象中的字段 I
    • 如果 Tstruct_type,并且 I 标识该 struct_type的实例字段:
      • 如果 E 是一个值,或者字段是只读的,并且引用发生在声明字段的结构的实例构造函数外部,则结果为一个值,即由 E给出的结构实例中字段 I 的值。
      • 否则,结果是一个变量,即 E给出的结构实例中的字段 I
    • 如果 I 标识实例事件:
      • 如果引用出现在声明事件的类或结构体中,并且事件在声明时没有使用 event_accessor_declarations (§15.8.1),并且引用没有出现在 a +=-= 操作符的左侧,则 E.I 的处理方式与 I 作为实例字段的处理方式完全相同。
      • 否则,结果是事件访问,相关的实例表达式为 E
  • 否则,将尝试把 E.I 作为扩展方法调用来处理 (§12.8.10.3)。 如果此操作失败,E.I 是无效的成员引用,并且会发生绑定时错误。

12.8.7.2 相同的简单名称和类型名称

在表单 E.I的成员访问中,如果 E 是单个标识符,并且 E 作为 simple_name§12.8.4)的含义是常量、字段、属性、局部变量或参数,其类型与 type_nameE 的含义相同(§7.8.1), 然后,允许 E 的两种可能含义。 E.I 的成员查找绝不会含糊不清,因为 I 在两种情况下都必然是 E 类型的成员。 换言之,该规则只是允许访问 E 的静态成员和嵌套类型,否则会出现编译时错误。

示例

struct Color
{
    public static readonly Color White = new Color(...);
    public static readonly Color Black = new Color(...);
    public Color Complement() => new Color(...);
}

class A
{
    public «Color» Color;              // Field Color of type Color

    void F()
    {
        Color = «Color».Black;         // Refers to Color.Black static member
        Color = Color.Complement();  // Invokes Complement() on Color field
    }

    static void G()
    {
        «Color» c = «Color».White;       // Refers to Color.White static member
    }
}

仅出于说明的目的,在 A 类中,引用 Color 类型的 Color 标识符的出现用 «...» 分隔,而引用 Color 字段的标识符则不用其进行分隔。

结束示例

12.8.8 Null 条件成员访问

null_conditional_member_accessmember_access (§12.8.7) 的条件版本,如果结果类型是 void,则为绑定时错误。 关于结果类型可能是 void 的空条件表达式,请参阅 (§12.8.11)。

null_conditional_member_accessprimary_expression 和两个标记“?”和“.”组成,后面是标识符和一个可选的 type_argument_list,后面是零或多个 dependent_access,其中任何一个前面都可以有一个 null_forgiving_operator

null_conditional_member_access
    : primary_expression '?' '.' identifier type_argument_list?
      (null_forgiving_operator? dependent_access)*
    ;
    
dependent_access
    : '.' identifier type_argument_list?    // member access
    | '[' argument_list ']'                 // element access
    | '(' argument_list? ')'                // invocation
    ;

null_conditional_projection_initializer
    : primary_expression '?' '.' identifier type_argument_list?
    ;

null_conditional_member_access 表达式 E 的形式为 P?.AE 的含义如下:

  • 如果 P 的类型是可空值类型:

    TP.Value.A的类型。

    • 如果 T 是一个类型参数,但并不知道它是引用类型还是不可为 null 的值类型,则会发生编译时错误。

    • 如果 T 是不可为 null 的值类型,则 E 的类型 T?E 的含义与以下含义相同:

      ((object)P == null) ? (T?)null : P.Value.A
      

      不同的是 P 只计算一次。

    • 否则,E 的类型是 TE 的含义与以下意思相同:

      ((object)P == null) ? (T)null : P.Value.A
      

      不同的是 P 只计算一次。

  • 否则:

    请设定 TP.A表达式的类型。

    • 如果 T 是一个未被确认为引用类型或非空值类型的类型参数,则会发生编译时错误。

    • 如果 T 是不可为 null 的值类型,则 E 的类型 T?E 的含义与以下含义相同:

      ((object)P == null) ? (T?)null : P.A
      

      不同的是 P 只计算一次。

    • 否则,E 的类型是 TE 的含义与以下内容的含义相同:

      ((object)P == null) ? (T)null : P.A
      

      不同的是 P 只计算一次。

注意:在一种形式的表达式中:

P?.A₀?.A₁

然后,如果 P 的计算结果为 null,那么 A₀A₁ 都不会被计算。 如果表达式是 null_conditional_member_accessnull_conditional_element_access§12.8.13 运算的序列,则情况也是如此。

尾注

null_conditional_projection_initializernull_conditional_member_access 的一种限制,具有相同的语义。 它仅在匿名对象创建表达式中作为投影初始值设定项发生(§12.8.17.7)。

12.8.9 Null 包容表达式

12.8.9.1 常规

Null 包容表达式的值、类型、分类 (§12.2) 和安全上下文 (§16.4.12) 是其 primary_expression 的值、类型、分类和安全上下文。

null_forgiving_expression
    : primary_expression null_forgiving_operator
    ;

null_forgiving_operator
    : '!'
    ;

注意:后缀 null 包容运算符和前缀逻辑否定运算符 (§12.9.4) 虽然用同一个词性标记 (!) 表示,但它们是不同的。 只有后者可以被重写 (§15.10),null 包容运算符的定义是固定的。 尾注

如果在同一个表达式中多次使用 null 包容运算符,尽管中间有括号,也会造成编译时错误。

示例:以下全部无效:

var p = q!!;            // error: applying null_forgiving_operator more than once
var s = ( ( m(t) ! ) )! // error: null_forgiving_operator applied twice to m(t)

结束示例

这个子集合的其余部分和以下同级子项是有条件的规范的。

执行静态 null 状态分析(§8.9.5)的编译器必须符合以下规范。

null 原谅运算符是一种编译时伪操作符,旨在提示编译器进行静态 null 状态分析。 它有两个用途:重写编译器对表达式可能为 null 的判断;重写编译器发出的与 Null 性有关的警告。

对于编译器的静态 null 状态分析没有产生任何警告的表达式,应用 null 包容运算符并不是错误。

12.8.9.2 重写“可能为 null”的判定

在某些情况下,编译器的静态 null 状态分析可能会确定一个表达式的 null 状态为 maybe null,而其他信息却表明该表达式不可能为 null,此时编译器可能会发出诊断警告。 将 null 宽恕运算符应用于这样的表达式时,会通知编译器的静态 null 状态分析,说明 null 状态位于 ,而不是位于 null 的;这不仅防止了诊断警告,还可能对任何正在进行的分析提供信息。

示例:请考虑以下事项:

#nullable enable
public static void M()
{
    Person? p = Find("John");                  // returns Person?
    if (IsValid(p))
    {
       Console.WriteLine($"Found {p!.Name}");  // p can't be null
    }
}

public static bool IsValid(Person? person) =>
    person != null && person.Name != null;

如果 IsValid 返回 true,则可以安全地引用 p 来访问其 Name 属性,并且可以使用 ! 来抑制“可能为 null 的值的取消引用”警告。

结束示例

示例: 使用空值宽容运算符时应谨慎,请考虑:

#nullable enable
int B(int? x)
{
    int y = (int)x!; // quash warning, throw at runtime if x is null
    return y;
}

此处,null 包容运算符应用于值类型,并取消了对 x的任何警告。 但是,如果 x 在运行时是 null,就会引发异常,因为 null 无法被强制转换为 int

结束示例

12.8.9.3 重写其他 null 分析警告

除了重写上述可能为 null 判断外,在其他情况下,可能还需要重写编译器的静态 null 状态分析判断,即表达式需要一个或多个警告。 对这样的表达式应用 null 包容运算符,会要求编译器不对该表达式发出任何警告。 在响应中,编译器可以选择不发出警告,还可以修改其进一步分析。

示例:请考虑以下事项:

#nullable enable
public static void Assign(out string? lv, string? rv) { lv = rv; }

public string M(string? t)
{
    string s;
    Assign(out s!, t ?? "«argument was null»");
    return s;
}

方法 Assign的参数类型 lv & rvstring?lv 为输出参数,并执行简单的赋值。

方法 M 将类型为 string的变量 s作为 Assign的输出参数传递,由于 s 不是可为 null 的变量,编译器发出了警告。 鉴于 Assign的第二个参数不能为 null,因此使用空值容忍运算符来消除警告。

结束示例

有条件的规范性文本结束。

12.8.10 调用表达式

12.8.10.1 常规

invocation_expression 用于调用方法。

invocation_expression
    : primary_expression '(' argument_list? ')'
    ;

当且仅当 primary_expression 具有 delegate_type 时,它才可以是 null_forgiving_expression

如果以下情况至少有一种成立,则动态绑定 invocation_expression (§11.3.3):

  • primary_expression 具有编译时类型 dynamic
  • 在可选的 argument_list 参数中,至少有一个参数的编译时类型为 dynamic

在这种情况下,编译器将 invocation_expression 分类为类型 dynamic的值。 在运行时,我们将使用运行时类型而不是编译时类型来确定 primary_expression 和具有编译时类型 dynamic 的参数的 invocation_expression 的含义。 如果 主表达式 没有编译时类型 dynamic,则方法调用将进行有限的编译时检查,如 §12.6.5中所述。

invocation_expressionprimary_expression 应是方法组或 delegate_type 的值。 如果 primary_expression 是方法组,则 invocation_expression 是方法调用(§12.8.10.2)。 如果 primary_expressiondelegate_type的值,则 invocation_expression 是委托调用(§12.8.10.4)。 如果 primary_expression 既不是方法组,也不是 delegate_type的值,则会发生绑定时错误。

可选的 argument_list§12.6.2)为方法的参数提供值或变量引用。

invocation_expression 进行计算的结果分类如下:

  • 如果 invocation_expression 调用了一个无返回值方法 (§15.6.1) 或无返回值的委托,结果不会返回任何值。 只有在 statement_expression (§13.7) 的上下文中或作为 lambda_expression (§12.19) 的主体,才允许使用被归类为无的表达式。 否则,将发生绑定时错误。
  • 否则,如果 invocation_expression 调用了按引用返回方法 (§15.6.1) 或按引用返回委托,结果将是一个变量,其关联类型与方法或委托的返回类型相同。 如果调用的是实例方法,而接收者属于类类型 T,则从 T 开始搜索基类时发现的第一个方法声明或重写中选择相关类型。
  • 否则,invocation_expression 将调用按值返回方法 (§15.6.1) 或按值返回委托,结果将是一个值,其关联类型与方法或委托的返回类型相同。 如果调用的是实例方法,而接收者属于类类型 T,则从 T 开始搜索基类时发现的第一个方法声明或重写中选择相关类型。

12.8.10.2 方法调用

对于方法调用,invocation_expressionprimary_expression 应是一个方法组。 方法组标识要调用的一种方法,或者从一组重载方法中选择一个特定的方法进行调用。 在后一种情况下,要调用的特定方法的确定基于 argument_list中参数类型提供的上下文。

对于形式为 M(A) 的方法调用,其中 M 是一个方法组(可能包括 type_argument_list),而 A 是一个可选的 argument_list,其绑定时处理包括以下步骤:

  • 方法调用的候选方法集被构造。 对于与方法组 M 相关联的每个方法 F
    • 如果 F 是非泛型,则 F 在以下情况下为候选项:
      • M 没有类型参数列表,并且
      • F 适用于 A§12.6.4.2)。
    • 如果 F 为泛型且 M 没有类型参数列表,则当以下情况下,F 是候选项:
      • 类型推理(§12.6.3)成功,推断调用的类型参数列表,并
      • 将推断的类型参数替换为相应的方法类型参数后,F 参数列表中的所有构造类型都满足其约束(§8.4.5),F 的参数列表适用于 A§12.6.4.2
    • 如果 F 是泛型的,并且 M 包含类型参数列表,则当以下情况下 F 是候选项:
      • F 与类型参数列表中提供的方法类型参数数量相同,并且
      • 将类型参数替换为相应的方法类型参数后,F 的参数列表中的所有构造类型都满足其约束(§8.4.5),并且 F 的参数列表适用于 A§12.6.4.2)。
  • 候选方法集缩小到只包含最派生类型的方法:对于集中的每一个方法 C.F(其中 C 是方法 F 所声明的类型),所有在 C 的基类型中声明的方法都会从集中移除。 此外,如果 C 不是 object类类型,则会从集中删除在接口类型中声明的所有方法。

    注意:后一条规则只有在方法组是对类型参数进行成员查找的结果时才有效,该类型参数具有 object 以外的有效基类和非空的有效接口集。 尾注

  • 如果生成的候选方法集为空,则会放弃以下步骤的进一步处理,而是尝试将调用作为扩展方法调用进行处理(§12.8.10.3)。 如果此操作失败,则不存在适用的方法,并且会发生绑定时错误。
  • 使用 §12.6.4的重载解析规则标识候选方法集的最佳方法。 如果无法识别单个最佳方法,则方法调用不明确,并且会发生绑定时错误。 在执行重载决策时,先将类型参数(提供的或推断的)替换为相应的方法类型参数,然后再考虑泛型方法的参数。

通过上述步骤在绑定时选择并验证方法后,将根据 §12.6.6中所述的函数成员调用规则处理实际的运行时调用。

注意:上述解析规则的直观效果如下所示:若要查找方法调用调用的特定方法,请从方法调用指示的类型开始,并继续继承链,直到找到至少一个适用、可访问、不可替代的方法声明。 然后对该类型中声明的适用、可访问的非重写方法集执行类型推理和重载解析,并调用因此选择的方法。 如果未找到方法,则尝试将调用作为扩展方法调用进行处理。 尾注

12.8.10.3 扩展方法调用

在下列形式之一的方法调用 (§12.6.6.2) 中

«expr» . «identifier» ( )  
«expr» . «identifier» ( «args» )  
«expr» . «identifier» < «typeargs» > ( )  
«expr» . «identifier» < «typeargs» > ( «args» )

如果调用的正常处理找不到适用的方法,则尝试将构造作为扩展方法调用进行处理。 如果 «expr» 或任何 «args» 具有编译时类型 dynamic,则扩展方法将不适用。

目标是找到最佳的 type_nameC,以便能够调用相应的静态方法:

C . «identifier» ( «expr» )  
C . «identifier» ( «expr» , «args» )  
C . «identifier» < «typeargs» > ( «expr» )  
C . «identifier» < «typeargs» > ( «expr» , «args» )

如果出现以下情况,则扩展方法 Cᵢ.Mₑ 符合条件

  • Cᵢ 是非泛型、非嵌套类
  • Mₑ 的名称是 标识符
  • Mₑ 是可访问的,当作为静态方法应用于参数时适用,如上所示
  • exprMₑ第一个参数的类型之间存在隐式标识、引用或装箱转换。

搜索 C 的步骤如下:

  • 从最近的封闭命名空间声明开始,依次在每个封闭命名空间声明中继续,最后在包含的编译单元中结束,逐步尝试查找一组候选扩展方法:
    • 如果给定的命名空间或编译单元直接包含 Cᵢ 符合条件的扩展方法 Mₑ的非泛型类型声明,则这些扩展方法的集合是候选集。
    • 如果通过使用给定命名空间或编译单元中的命名空间指令导入的命名空间直接包含具有符合条件的扩展方法 CᵢMₑ的非泛型类型声明,则这些扩展方法的集合是候选集。
  • 如果在任何封闭的命名空间声明或编译单元中未找到候选集,则会发生编译时错误。
  • 否则,将按照 §12.6.4 中的描述对候选集进行重载决策。 如果未找到任何最佳方法,则会发生编译时错误。
  • C 是一种类型,其中最适合的方法被声明为扩展方法。

使用 C 作为目标,方法调用随后作为静态方法调用进行处理(§12.6.6)。

注意:与实例方法调用不同,当 expr 计算结果为 null 引用时,不会引发异常。 然而,此 null 值会传递给扩展方法,就像通过普通静态方法调用一样。 如何响应这种调用,取决于扩展方法的实现。 尾注

上述规则意味着实例方法优先于扩展方法,内部命名空间声明中提供的扩展方法优先于外部命名空间声明中提供的扩展方法,并且直接在命名空间中声明的扩展方法优先于使用 using 命名空间指令导入到同一命名空间中的扩展方法。

示例

public static class E
{
    public static void F(this object obj, int i) { }
    public static void F(this object obj, string s) { }
}

class A { }

class B
{
    public void F(int i) { }
}

class C
{
    public void F(object obj) { }
}

class X
{
    static void Test(A a, B b, C c)
    {
        a.F(1);            // E.F(object, int)
        a.F("hello");      // E.F(object, string)
        b.F(1);            // B.F(int)
        b.F("hello");      // E.F(object, string)
        c.F(1);            // C.F(object)
        c.F("hello");      // C.F(object)
    }
}

在此示例中,B的方法优先于第一个扩展方法,C的方法优先于这两种扩展方法。

public static class C
{
    public static void F(this int i) => Console.WriteLine($"C.F({i})");
    public static void G(this int i) => Console.WriteLine($"C.G({i})");
    public static void H(this int i) => Console.WriteLine($"C.H({i})");
}

namespace N1
{
    public static class D
    {
        public static void F(this int i) => Console.WriteLine($"D.F({i})");
        public static void G(this int i) => Console.WriteLine($"D.G({i})");
    }
}

namespace N2
{
    using N1;

    public static class E
    {
        public static void F(this int i) => Console.WriteLine($"E.F({i})");
    }

    class Test
    {
        static void Main(string[] args)
        {
            1.F();
            2.G();
            3.H();
        }
    }
}

此示例的输出为:

E.F(1)
D.G(2)
C.H(3)

D.G 优先于 C.GE.F 优先于 D.FC.F

结束示例

12.8.10.4 委托调用

对于委托调用,invocation_expressionprimary_expression 应是 delegate_type 的值。 此外,将 delegate_type 视为与 delegate_type相同的参数列表的函数成员,delegate_type 应适用于 invocation_expressionargument_list§12.6.4.2)。

对形式为 D(A) 的委托调用(其中 Ddelegate_typeprimary_expressionA 是可选的 argument_list)的运行时处理包括以下步骤:

  • D 已计算。 如果此评估导致异常,则不会执行进一步的步骤。
  • 会对参数列表 A 进行计算。 如果此评估导致异常,则不会执行进一步的步骤。
  • 检查 D 的值是否有效。 如果 D 的值是 null,则抛出 System.NullReferenceException,并且不再执行后续步骤。
  • 否则,D 就是对委托实例的引用。 函数成员调用 (§12.6.6) 在委托调用列表中的每个可调用实体上执行。 对于包含实例和实例方法的可调用实体,调用的实例是可调用实体中包含的实例。

请参阅 §20.6,了解不含参数的多个调用列表的详细信息。

12.8.11 Null 条件调用表达式

null_conditional_invocation_expression 在语法上是 null_conditional_member_access (§12.8.8) 或 null_conditional_element_access (§12.8.13),其中最后的 dependent_access 是一个调用表达式 (§12.8.10)。

null_conditional_invocation_expression 出现在 statement_expression (§13.7)、anonymous_function_body (§12.19.1) 或 method_body (§15.6.1) 的上下文中。

与语法等效的 null_conditional_member_accessnull_conditional_element_access 不同,null_conditional_invocation_expression 可以被归类为 “无”。

null_conditional_invocation_expression
    : null_conditional_member_access null_forgiving_operator? '(' argument_list? ')'
    | null_conditional_element_access null_forgiving_operator? '(' argument_list? ')'
    ;

当且仅当 null_conditional_member_accessnull_conditional_element_access 具有 delegate_type 时,才可以包含可选的 null_forgiving_operator

null_conditional_invocation_expression 表达式 E 的形式为 P?A;其中 A 是语法等效的 null_conditional_member_accessnull_conditional_element_access 的其余部分,因此 A 将以 .[ 开头。 让 PA 表示 PA 的串联。

E 作为 statement_expression 出现时,E 的含义与 statement 的含义相同:

if ((object)P != null) PA

不同的是 P 只计算一次。

E 作为 anonymous_function_bodymethod_body 出现时,E 的含义取决于其分类:

  • 如果 E 分类为无,则其含义与 的含义相同:

    { if ((object)P != null) PA; }
    

    不同的是 P 只计算一次。

  • 否则,E 的含义与 的含义相同:

    { return E; }
    

    而这个的含义又取决于 E 在语法上是否等效于 null_conditional_member_access (§12.8.8) 或 null_conditional_element_access (§12.8.13)。

12.8.12 元素访问

12.8.12.1 常规

element_access 由一个 primary_no_array_creation_expression 组成,后跟一个 “[” 标记,后跟一个 argument_list,后跟一个 “]” 标记。 argument_list 由一个或多个参数组成,用逗号分隔。

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

不允许 element_accessargument_list 中包含 outref 参数。

如果以下至少一项成立,则 element_access 被动态绑定(§12.3.3)。

  • primary_no_array_creation_expression 具有编译时类型 dynamic
  • argument_list 中至少有一个表达式的编译时类型为 dynamic,且 primary_no_array_creation_expression 没有数组类型。

在这种情况下,编译器将 element_access 分类为类型 dynamic的值。 运行时将应用以下规则以确定 element_access 的含义。此过程中使用的是运行时类型,而不是 primary_no_array_creation_expression 的编译时类型,以及 argument_list 表达式中的那些具有编译时类型的类型 dynamic。 如果 primary_no_array_creation_expression 没有编译时类型 dynamic,则如 §12.6.5 中所述,元素访问将会经过有限的编译时检查。

如果 element_accessprimary_no_array_creation_expressionarray_type 的值,则 element_access 是数组访问 (§12.8.12.2)。 否则,primary_no_array_creation_expression 应是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下,element_access 是索引器访问 (§12.8.12.3)。

12.8.12.2 数组访问

对于数组访问,element_accessprimary_no_array_creation_expression 应是 array_type 的值。 此外,数组访问中的 argument_list 不允许包含命名参数。 argument_list 中的表达式数应与 array_type的级别相同,每个表达式的类型应为 intuintlongulong, 或隐式转换为其中一个或多个类型。

对数组访问进行求值的结果是一个数组元素类型的变量,即由 argument_list 中表达式的值选择的数组元素。

表单 P[A] 的数组访问的运行时处理包括以下步骤:其中 Parray_typeprimary_no_array_creation_expressionAargument_list

  • P 已计算。 如果此评估导致异常,则不会执行进一步的步骤。
  • argument_list 的索引表达式按从左到右的顺序计算。 对每个索引表达式进行计算后,将隐式转换(§10.2)转换为下列类型之一:intuintlongulong。 此选择列表中第一个存在隐式转换的类型。 例如,如果索引表达式的类型为 short 则执行到 int 的隐式转换,因为可以从 shortint 和从 shortlong 的隐式转换。 如果索引表达式或后续隐式转换的计算会导致异常,则不会计算进一步的索引表达式,也不会执行进一步的步骤。
  • 检查 P 的值是否有效。 如果 P 的值为 null,则会引发 System.NullReferenceException,并且不再执行其他步骤。
  • argument_list 中每个表达式的值都会与 P 引用的数组实例每个维度的实际边界进行核对。 如果一个或多个值超出范围,则会引发 System.IndexOutOfRangeException,并且不执行进一步的步骤。
  • 由索引表达式给出的数组元素的位置被计算出来,该位置成为数组访问的结果。

12.8.12.3 索引器访问

对于索引器访问,element_accessprimary_no_array_creation_expression 应为类、结构或接口类型的变量或值,并且该类型应实现一个或多个适用于 element_accessargument_list 的索引器。

对于形式为 P[A] 的索引访问,其中 P 为类、结构或接口类型 Tprimary_no_array_creation_expressionAargument_list 的绑定时处理包括以下步骤:

  • 构建由 T 提供的索引器集。 该集包括在 TT 的基类型中声明的所有索引器,这些索引器不是重写声明,并且在当前上下文中可以访问 (§7.5)。
  • 该集合被简化为适用且不被其他索引器隐藏的索引器。 以下规则应用于集合中的每个索引器 S.I,其中 S 是声明索引器 I 的类型:
    • 如果 I 不适用于 A§12.6.4.2),则将从集中删除 I
    • 如果 I 适用于 A§12.6.4.2),那么从该集合中删除所有在基类型 S 中声明的索引器。
    • 如果 I 适用于 A§12.6.4.2),并且 S 不是 object类类型,则会从集中删除在接口中声明的所有索引器。
  • 如果生成的候选索引器集为空,则不存在适用的索引器,并发生绑定时错误。
  • 候选索引器集的最佳索引器是使用 §12.6.4的重载解析规则标识的。 如果无法识别单个最佳索引器,则索引器访问不明确,并且会发生绑定时错误。
  • argument_list 的索引表达式按从左到右的顺序计算。 处理索引访问的结果是一个被归类为索引访问的表达式。 索引器访问表达式引用了在上述步骤中确定的索引器,且与之关联的实例表达式为 P,关联的参数列表为 A,关联的类型则是索引器的类型。 如果 T 是类类型,则从 T 开始搜索基类时发现的第一个索引器声明或重写中选择相关类型。

索引器访问会调用索引器的 get 访问器或 set 访问器,具体取决于使用的上下文。 如果索引器访问是分配的目标,则会调用 set 访问器来分配新值(§12.21.2)。 在所有其他情况下,调用 get 访问器以获取当前值(§12.2.2)。

12.8.13 Null 条件元素访问

null_conditional_element_accessprimary_no_array_creation_expression,后面依次包括“?”和“[”两个标记、一个 argument_list、一个“]”标记、零或多个 dependent_access 组成,其中任何一个标记前面都可以有一个 null_forgiving_operator

null_conditional_element_access
    : primary_no_array_creation_expression '?' '[' argument_list ']'
      (null_forgiving_operator? dependent_access)*
    ;

null_conditional_element_accesselement_access (§12.8.12) 的条件版本,如果结果类型是 void,则为绑定时错误。 关于结果类型可能是 void 的空条件表达式,请参阅 (§12.8.11)。

null_conditional_element_access 表达式 E 的形式是 P?[A]B;其中 Bdependent_access(如有)。 E 的含义如下:

  • 如果 P 的类型是可空值类型:

    设定 TP.Value[A]B表达式的类型。

    • 如果 T 是一个未知为引用类型或非 null 类型的类型参数,则会出现编译时错误。

    • 如果 T 是不可为 null 的值类型,则 E 的类型 T?E 的含义与以下含义相同:

      ((object)P == null) ? (T?)null : P.Value[A]B
      

      不同的是 P 只计算一次。

    • 否则,E 的类型是 TE 的含义与以下含义相同:

      ((object)P == null) ? null : P.Value[A]B
      

      不同的是 P 只计算一次。

  • 否则:

    TP[A]B表达式的类型。

    • 如果 T 是一个类型参数,并且不知道它是引用类型还是不可为 null 的值类型,则会发生编译时错误。

    • 如果 T 是不可为 null 的值类型,则 E 的类型 T?E 的含义与以下含义相同:

      ((object)P == null) ? (T?)null : P[A]B
      

      不同的是 P 只计算一次。

    • 否则,E 的类型是 TE 的含义与以下的含义相同:

      ((object)P == null) ? null : P[A]B
      

      不同的是 P 只计算一次。

注意:在一种形式的表达式中:

P?[A₀]?[A₁]

如果 P 的计算结果是 null,那么 A₀A₁ 都没有被计算。 如果表达式是 null_conditional_element_accessnull_conditional_member_access§12.8.8 运算的序列,则情况也是如此。

尾注

12.8.14 此访问权限

this_access 由关键字 this 组成。

this_access
    : 'this'
    ;

只有在实例构造函数、实例方法、实例访问器 (§12.2.1) 或终结器的中,才允许使用 this_access。 它具有以下含义之一:

  • 在类的实例构造函数中的 primary_expression 中使用 this 时,它将分类为值。 值的类型是发生用法的类的实例类型(§15.3.2),值为对所构造对象的引用。
  • 在类的实例方法或实例访问器中的 primary_expression 中使用 this 时,它被归类为值。 该数值的类型是用法发生所在类的实例类型(§15.3.2),而该数值是对被调用方法或访问器的对象的引用。
  • 在结构的实例构造函数中的 primary_expression 中使用 this 时,它被归类为变量。 变量的类型是在其中发生用法的结构的实例类型(§15.3.2),变量表示正在构造的结构。
    • 如果构造函数声明没有构造函数初始值设定项,则 this 变量的行为与结构类型的输出参数完全相同。 具体而言,这意味着变量应在实例构造函数的每个执行路径中明确分配。
    • 否则,this 变量的行为与结构类型的 ref 参数完全相同。 具体而言,这意味着该变量最初被视为已分配。
  • 在结构的实例方法或实例访问器中的 primary_expression 中使用 this 时,它将分类为变量。 变量的类型是发生用法的结构的实例类型(§15.3.2)。
    • 如果方法或访问器不是迭代器(§15.14)或异步函数(§15.15),则 this 变量表示调用方法或访问器的结构。
      • 如果结构是 readonly struct,则 this 变量的行为与结构类型的输入参数完全相同
      • 否则,this 变量的行为与结构类型的 ref 参数完全相同
    • 如果方法或访问器是迭代器或异步函数,则 变量表示调用方法或访问器的结构 副本,其行为与结构类型的 参数完全相同。

primary_expression 中使用 this 会导致编译时错误。 具体而言,不能在静态方法、静态属性访问器或字段声明的 variable_initializer 中引用 this

12.8.15 基地访问

base_access 包含关键字 base,后跟“.”标记、标识符和可选的 type_argument_list 或用方括号括起来的 argument_list

base_access
    : 'base' '.' identifier type_argument_list?
    | 'base' '[' argument_list ']'
    ;

base_access 用于访问被当前类或结构体中名称相似的成员所隐藏的基类成员。 base_access 仅允许在实例构造函数、实例方法、实例访问器 (§12.2.1) 或终结器的主体中使用。 当 base.I 出现在类或结构内时,我将将其标记为该类或结构的基类的一个成员。 同样,在类中发生 base[E] 时,基类中应存在适用的索引器。

在绑定时,形式为 base.Ibase[E]base_access 表达式的计算与写入 ((B)this).I((B)this)[E] 完全相同,其中 B 是出现该构造的类或结构的基类。 因此,base.Ibase[E] 对应于 this.Ithis[E],而this 被视为基类的一个实例。

base_access 引用一个虚拟函数成员(如方法、属性或索引器)时,对在运行时调用哪个函数成员的确定(§12.6.6)将会被更改。 要确定调用的函数成员,需要找到该函数成员相对于 B 的最派生实现 (§15.6.4)(而不是相对于 this 的运行时类型,这在非基类访问中很常见)。 因此,在重载一个虚拟函数成员时,可以使用 base_access 来调用该函数成员的继承实现。 如果 base_access 引用的函数成员是抽象的,则会发生绑定时错误。

注释:与 this不同,base 本身不是表达式。 该关键字仅在 base_accessconstructor_initializer 的上下文中使用(§15.11.2)。 尾注

12.8.16 后缀增量和减量运算符

post_increment_expression
    : primary_expression '++'
    ;

post_decrement_expression
    : primary_expression '--'
    ;

后缀递增或递减操作的操作数应为一个表达式,该表达式应被分类为变量、属性访问或索引器访问。 操作的结果是与操作数类型相同的值。

如果 primary_expression 具有编译时类型 dynamic,则运算符是动态绑定的(§12.3.3);post_increment_expressionpost_decrement_expression 具有编译时类型 dynamic,并且在运行时应用以下规则,使用 primary_expression的运行时类型。

如果后缀递增或递减操作的操作数是属性或索引器访问,那么该属性或索引器必须同时具有 get 和 set 两种访问器。 如果情况并非如此,则会发生绑定时错误。

一元运算符重载分辨率(§12.4.4)应用于选择特定的运算符实现。 以下类型存在预定义的 ++-- 运算符:sbytebyteshortushortintuintlongulongcharfloatdoubledecimal和任意枚举类型。 预定义的 ++ 运算符返回通过向操作数添加 1 生成的值,预定义的 -- 运算符返回通过从操作数减去 1 生成的值。 在受检上下文中,如果此加法或减法的结果超出结果类型的范围,并且结果类型是整型类型或枚举类型,则会抛出 System.OverflowException

应有从所选一元运算符的返回类型到 primary_expression类型的隐式转换,否则会发生编译时错误。

表单 x++x-- 的后缀递增或递减操作的运行时处理包括以下步骤:

  • 如果 x 被归类为变量:
    • x 进行评估以生成变量。
    • x 的值会被保存。
    • 保存的 x 值将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。
    • 运算符返回的值将转换为 x 的类型,并存储在先前 x计算给出的位置。
    • x 的已保存值将成为操作的结果。
  • 如果 x 被归类为属性或索引器访问:
    • x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问)将被求值,计算结果将用于随后的 get 和 set 访问器调用。
    • 调用 x 的 get 访问器,并保存返回值。
    • 保存的 x 值将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。
    • 运算符返回的值会转换为 x 的类型,并以该值作为值参数调用 x 的 set 访问器。
    • x 的已保存值将成为操作的结果。

++-- 运算符还支持前缀表示法(§12.9.6)。 的结果是在操作 之前 的值,而 的结果是在操作 后 的值。 在任一情况下,x 自身在操作后具有相同的值。

可以通过后缀或前缀表示法来调用运算符 ++ 或运算符 -- 的实现。 不能为这两个表示法使用单独的运算符实现。

12.8.17 new 运算符

12.8.17.1 常规

new 运算符用于创建新类型的实例。

有三种形式的新表达式:

  • 对象创建表达式和匿名对象创建表达式用于创建新的类类型和值类型的实例。
  • 数组创建表达式用于创建新数组类型的实例。
  • 委托创建表达式用于获取委托类型的实例。

new 运算符表示创建类型的实例,但不一定意味着内存分配。 具体而言,值类型的实例不需要超出它们所在的变量的附加内存,并且当使用 new 创建值类型的实例时,不会发生分配。

注意:委托创建表达式并不总是创建新实例。 当以与方法组转换(§10.8)或匿名函数转换(§10.7)相同的方式处理表达式时,可能会导致重用现有的委托实例。 尾注

12.8.17.2 对象创建表达式

object_creation_expression 用于创建 class_typevalue_type 的新实例。

object_creation_expression
    : 'new' type '(' argument_list? ')' object_or_collection_initializer?
    | 'new' type object_or_collection_initializer
    ;

object_or_collection_initializer
    : object_initializer
    | collection_initializer
    ;

object_creation_expressiontype 应为 class_typevalue_typetype_parametertype 不能是 tuple_type 或抽象或静态 class_type 类型。

仅当 类型class_typestruct_type时,才允许可选的 argument_list§12.6.2)。

如果对象创建表达式包含对象初始值设定项或集合初始值设定项,则可以省略构造函数参数列表并括住括号。 省略构造函数参数列表和封闭括号等效于指定空参数列表。

处理包含对象初始值设定项或集合初始值设定项的对象创建表达式包括先处理实例构造函数,然后处理由对象初始值设定项指定的成员或元素初始化(§12.8.17.3) 或集合初始值设定项(§12.8.17.4)。

如果可选的 argument_list 中的任何参数的编译时类型为 dynamic,那么 object_creation_expression 将被动态绑定 (§12.3.3),并在运行时使用 argument_list 中具有编译时类型 dynamic 的参数的运行时类型应用以下规则。 但是,正如 §12.6.5中所述,对象的创建会经历有限的编译时检查。

对形式为新 T(A)object_creation_expression(其中 Tclass_typevalue_type,而 A 是可选的 argument_list)的绑定时处理包括以下步骤:

  • 如果 Tvalue_type,并且不存在 A
    • object_creation_expression 是默认的构造函数调用。 object_creation_expression 的结果是 T 类型的值,即 §8.3.3 中定义的 T 的默认值。
  • 否则,如果 Ttype_parameter 且不存在 A
    • 如果未为 T指定任何值类型约束或构造函数约束(§15.2.5),则会发生绑定时错误。
    • object_creation_expression 的结果是类型参数绑定到的运行时类型的值,即调用该类型的默认构造函数的结果。 运行时类型可以是引用类型或值类型。
  • 否则,如果 Tclass_typestruct_type
    • 如果 T 是抽象或静态 class_type,则会发生编译时错误。
    • 要调用的实例构造函数是使用 §12.6.4的重载解析规则确定的。 候选实例构造函数集由 T中声明的所有可访问实例构造函数组成,这些构造函数适用于 A(§12.6.4.2)。 如果候选实例构造函数集为空,或者无法识别单个最佳实例构造函数,则会发生绑定时错误。
    • object_creation_expression 的结果是 T类型的值,即调用在上述步骤中确定的实例构造函数生成的值。
    • 否则,object_creation_expression 无效,并且会发生绑定时错误。

即使 object_creation_expression 是动态绑定的,编译时类型仍然是 T

对形式为新 T(A)object_creation_expression(其中 Tclass_typestruct_type,而 A 是可选的 argument_list)的运行时处理包括以下步骤:

  • 如果 Tclass_type
    • T 的新实例已分配。 如果没有足够的内存可用于分配新实例,则会引发 System.OutOfMemoryException,并且不会执行进一步的步骤。
    • 新实例的所有字段都初始化为其默认值(§9.3)。
    • 实例构造函数根据函数成员调用规则(§12.6.6)调用。 对新分配的实例的引用会自动传递给实例构造函数,可以从该构造函数中访问该实例,如下所示。
  • 如果 Tstruct_type
    • T 类型的实例是通过分配临时局部变量创建的。 由于 struct_type 的实例构造函数必须明确为要创建的实例的每个字段赋值,因此不需要初始化临时变量。
    • 实例构造函数根据函数成员调用规则(§12.6.6)调用。 对新分配的实例的引用会自动传递给实例构造函数,可以从该构造函数中访问该实例,如下所示。

12.8.17.3 对象初始值设定项

对象初始值设定项 指定对象零个或多个字段、属性或索引元素的值。

object_initializer
    : '{' member_initializer_list? '}'
    | '{' member_initializer_list ',' '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : initializer_target '=' initializer_value
    ;

initializer_target
    : identifier
    | '[' argument_list ']'
    ;

initializer_value
    : expression
    | object_or_collection_initializer
    ;

对象初始值设定项由一系列成员初始值设定项组成,由 {} 标记括起来,用逗号分隔。 每个 member_initializer 都应指定一个初始化目标。 标识符 应为要初始化的对象命名可访问的字段或属性,而括在方括号中的 argument_list 应为正在初始化的对象指定可访问索引器的参数。 如果对象初始值设定项为同一字段或属性包含多个成员初始值设定项,则属于错误。

注意:虽然不允许对象初始值设定项多次设置相同的字段或属性,但索引器没有此类限制。 对象初始值设定项可能包含引用索引器的多个初始值设定项目标,甚至可以多次使用相同的索引器参数。 尾注

每个 initializer_target 后面都有一个等号和一个表达式、一个对象初始值设定项或一个集合初始值设定项。 在对象初始化器中,表达式无法引用它所初始化的新创建的对象。

指定等号后表达式的成员初始值设定项的处理方式与目标赋值 (§12.21.2) 相同。

指定等号后对象初始值设定项的成员初始值设定项是嵌套对象初始值设定项,即嵌入对象的初始化。 嵌套对象初始值设定项中的赋值被视为对字段或属性成员的赋值,而不是为字段或属性分配新值。 嵌套对象初始化器不能用于值类型的属性,也不能用于值类型的只读字段。

在等号后指定集合初始化器的成员初始化器是对嵌入集合的初始化。 初始值设定项中给出的元素会被添加到目标引用的集合中,而不是为目标字段、属性或索引器分配一个新集合。 目标应为满足 §12.8.17.4中指定的要求的集合类型。

当初始值设定项目标引用索引器时,索引器的参数应当始终精确计算一次。 因此,即使参数最终从未被使用(例如,由于嵌套初始值设定项为空),也会对其副作用进行评估。

示例:以下类表示具有两个坐标的点:

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

可以创建和初始化 Point 实例,如下所示:

Point a = new Point { X = 0, Y = 1 };

这样的效果与以下内容相同

Point __a = new Point();
__a.X = 0;
__a.Y = 1;
Point a = __a;

其中,__a 是一个不可见且不可访问的临时变量。

以下类显示了从两个点创建的矩形,以及 Rectangle 实例的创建和初始化:

public class Rectangle
{
    public Point P1 { get; set; }
    public Point P2 { get; set; }
}

可以创建和初始化 Rectangle 实例,如下所示:

Rectangle r = new Rectangle
{
    P1 = new Point { X = 0, Y = 1 },
    P2 = new Point { X = 2, Y = 3 }
};

这样的效果与以下内容相同

Rectangle __r = new Rectangle();
Point __p1 = new Point();
__p1.X = 0;
__p1.Y = 1;
__r.P1 = __p1;
Point __p2 = new Point();
__p2.X = 2;
__p2.Y = 3;
__r.P2 = __p2;
Rectangle r = __r;

其中,__r__p1__p2 是临时变量,否则不可见且不可访问。

如果 Rectangle的构造函数分配两个嵌入式 Point 实例,则它们可用于初始化嵌入式 Point 实例,而不是分配新实例:

public class Rectangle
{
    public Point P1 { get; } = new Point();
    public Point P2 { get; } = new Point();
}

以下构造可用于初始化嵌入式 Point 实例,而不是分配新实例:

Rectangle r = new Rectangle
{
    P1 = { X = 0, Y = 1 },
    P2 = { X = 2, Y = 3 }
};

这样的效果与以下内容相同

Rectangle __r = new Rectangle();
__r.P1.X = 0;
__r.P1.Y = 1;
__r.P2.X = 2;
__r.P2.Y = 3;
Rectangle r = __r;

结束示例

12.8.17.4 集合初始值设定项

集合初始值设定项指定集合的元素。

collection_initializer
    : '{' element_initializer_list '}'
    | '{' element_initializer_list ',' '}'
    ;

element_initializer_list
    : element_initializer (',' element_initializer)*
    ;

element_initializer
    : non_assignment_expression
    | '{' expression_list '}'
    ;

expression_list
    : expression
    | expression_list ',' expression
    ;

集合初始值设定项由元素初始值设定项序列组成,由 {} 标记括起来,用逗号分隔。 每个元素初始值设定项指定要添加到要初始化的集合对象的元素,由 {} 标记和逗号分隔的表达式列表组成。 无需使用大括号即可编写单表达式元素初始化器,但此时不能是赋值表达式,以避免与成员初始化器混淆。 non_assignment_expression 生成式在 §12.22 中定义。

示例:下面是包含集合初始值设定项的对象创建表达式的示例:

List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

结束示例

应用集合初始值设定项的集合对象必须是实现 System.Collections.IEnumerable 的类型,否则将发生编译时错误。 对于每个指定元素,按从左到右的顺序排列,将应用普通成员查找以查找名为 Add的成员。 如果成员查找的结果不是方法组,则会发生编译时错误。 否则,将使用元素初始值设定项的表达式列表作为参数列表进行重载决策,然后集合初始值设定项会调用生成的方法。 因此,集合对象应包含一个适用的实例或扩展方法,对于每个元素初始值设定项,其方法名称为 Add

示例:下面显示了一个表示联系人的类,其中包含姓名和电话号码列表,以及 List<Contact> 的创建和初始化:

public class Contact
{
    public string Name { get; set; }
    public List<string> PhoneNumbers { get; } = new List<string>();
}

class A
{
    static void M()
    {
        var contacts = new List<Contact>
        {
            new Contact
            {
                Name = "Chris Smith",
                PhoneNumbers = { "206-555-0101", "425-882-8080" }
            },
            new Contact
            {
                Name = "Bob Harris",
                PhoneNumbers = { "650-555-0199" }
            }
        };
    }
}

其效果与以下内容相同

var __clist = new List<Contact>();
Contact __c1 = new Contact();
__c1.Name = "Chris Smith";
__c1.PhoneNumbers.Add("206-555-0101");
__c1.PhoneNumbers.Add("425-882-8080");
__clist.Add(__c1);
Contact __c2 = new Contact();
__c2.Name = "Bob Harris";
__c2.PhoneNumbers.Add("650-555-0199");
__clist.Add(__c2);
var contacts = __clist;

其中,__clist__c1__c2 是临时变量,否则不可见且不可访问。

结束示例

12.8.17.5 数组创建表达式

array_creation_expression 用于创建 array_type 的新实例。

array_creation_expression
    : 'new' non_array_type '[' expression_list ']' rank_specifier*
      array_initializer?
    | 'new' array_type array_initializer
    | 'new' rank_specifier array_initializer
    ;

第一种形式的数组创建表达式会分配一个数组实例,其类型是删除表达式列表中每个单独表达式后的结果。

示例:数组创建表达式 new int[10,20] 生成 int[,]类型的数组实例,并且数组创建表达式新 int[10][,] 生成 int[][,]类型的数组实例。 结束示例

表达式列表中的每个表达式的类型应为 intuintlongulong,或者隐式转换为其中一个或多个类型。 每个表达式的值确定新分配的数组实例中相应维度的长度。 由于数组维度的长度应为非负值,因此在表达式列表中具有具有负值的常量表达式是编译时错误。

除了不安全的上下文(§23.2),数组的布局未指定。

如果第一形式的数组创建表达式包含数组初始化器,则表达式列表中的每个表达式应为常量,并且表达式列表中指定的维数和维度长度应与数组初始化器的维数和维度长度相匹配。

在第二种或第三种形式的数组创建表达式中,指定的数组类型或排名说明符的排名应与数组初始值设定项的排名匹配。 从数组初始化器的每个相应嵌套级别中的元素数量推断各个维度的长度。 因此,以下声明中的初始值设定项表达式

var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};

完全对应于

var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};

第三种形式的数组创建表达式称为隐式类型化数组创建表达式。 这类似于第二种形式,只是未显式指定数组的元素类型,而是确定为数组初始值设定项集中表达式集的最佳常用类型(§12.6.3.15)。 对于多维数组,即 rank_specifier 中至少包含一个逗号的数组,该集合包括嵌套 array_initializer 中的所有 expression

数组初始值设定项将在 §17.7 中进一步介绍。

计算数组创建表达式的结果被分类为一个值,即对新分配的数组实例的引用。 数组创建表达式的运行时处理包括以下步骤:

  • expression_list 中的维度长度表达式从左到右依次求值。 对每个表达式进行计算后,将隐式转换(§10.2)转换为下列类型之一:intuintlongulong。 此列表中存在隐式转换的第一个类型将被选中。 如果表达式的计算或后续隐式转换导致异常,则不会计算进一步的表达式,也不会执行进一步的步骤。
  • 维度长度的计算值将得到验证,具体如下:如果一个或多个值小于零,则会触发 System.OverflowException,并且不会执行进一步的步骤。
  • 分配具有给定维度长度的数组实例。 如果没有足够的内存可用于分配新实例,则会引发 System.OutOfMemoryException,并且不会执行进一步的步骤。
  • 新数组实例的所有元素都初始化为其默认值(§9.3)。
  • 如果数组创建表达式包含数组初始值设定项,则计算数组初始值设定项中的每个表达式并将其分配给其相应的数组元素。 计算和赋值是按照表达式在数组初始化时的书写顺序进行的,换句话说,元素是按照递增的索引顺序初始化的,最右侧的维度首先递增。 如果对给定表达式或相应数组元素的后续赋值导致异常,则不会初始化其他元素(其余元素将具有其默认值)。

数组创建表达式允许实例化具有数组类型的元素的数组,但应手动初始化此类数组的元素。

示例:语句

int[][] a = new int[100][];

创建一个具有 100 个类型为 int[]的元素的单维数组。 每个元素的初始值 null。 同一数组创建表达式不可能同时实例化子数组和语句

int[][] a = new int[100][5]; // Error

会导致编译时错误。 子数组的实例化可以手动执行,如

int[][] a = new int[100][];
for (int i = 0; i < 100; i++)
{
    a[i] = new int[5];
}

结束示例

注意:当数组数组具有“矩形”形状时,即子数组的长度都相同时,使用多维数组会更有效。 在上面的示例中,数组数组的实例化将创建 101 个对象,即一个外部数组和 100 个子数组。 相比之下,

int[,] a = new int[100, 5];

仅创建一个对象,一个二维数组,并在单个语句中完成分配。

尾注

示例:下面是隐式类型数组创建表达式的示例:

var a = new[] { 1, 10, 100, 1000 };                     // int[]
var b = new[] { 1, 1.5, 2, 2.5 };                       // double[]
var c = new[,] { { "hello", null }, { "world", "!" } }; // string[,]
var d = new[] { 1, "one", 2, "two" };                   // Error

最后一个表达式会导致编译时错误,因为 intstring 都无法隐式转换为另一个表达式,因此没有最佳通用类型。 在这种情况下,必须使用显式声明类型的数组创建表达式,例如将类型指定为 object[]。 或者,可以将其中一个元素强制转换为通用的基类型,然后将其转换为推断的元素类型。

结束示例

隐式类型数组创建表达式可以与匿名对象初始值设定项(§12.8.17.7)结合使用来创建匿名类型的数据结构。

示例

var contacts = new[]
{
    new
    {
        Name = "Chris Smith",
        PhoneNumbers = new[] { "206-555-0101", "425-882-8080" }
    },
    new 
    {
        Name = "Bob Harris",
       PhoneNumbers = new[] { "650-555-0199" }
    }
};

结束示例

12.8.17.6 委托创建表达式

delegate_creation_expression 用于获取 delegate_type 的实例。

delegate_creation_expression
    : 'new' delegate_type '(' expression ')'
    ;

委托创建表达式的参数应为方法组、匿名函数或编译时类型 dynamicdelegate_type的值。 如果参数是方法组,它将标识出方法,并在实例方法的情况下,标识出要为其创建委托的对象。 如果参数是匿名函数,则直接定义委托目标的参数和方法主体。 如果参数是一个值,它将标识一个委托实例,以便为其创建一个副本。

如果 表达式 在编译时具有类型 dynamic,则 delegate_creation_expression 将进行动态绑定(§12.8.17.6),并在运行时根据 表达式的运行时类型应用以下规则。 否则,这些规则将在编译时应用。

形式为新 D(E)delegate_creation_expression 的绑定时处理过程(其中 Ddelegate_typeEexpression)包括以下步骤:

  • 如果 E 是方法组,则委托创建表达式的处理方式与方法组转换(§10.8)从 ED相同。

  • 如果 E 是匿名函数,则委托创建表达式的处理方式与从 ED的匿名函数转换(§10.7)相同。

  • 如果 E 是一个值,则 E 应与 D 兼容 (§20.2),其结果是对一个新创建的委托的引用,该委托有一个调用 E 的单项调用列表。

形式为新 D(E)delegate_creation_expression 的运行时处理过程(其中 Ddelegate_typeEexpression)包括以下步骤:

  • 如果 E 是方法组,则委托创建表达式将被视作一种方法组转换(§10.8),从 ED
  • 如果 E 是一个匿名函数,则委托创建将被评估为从 ED 的匿名函数转换(§10.7)。
  • 如果 Edelegate_type 的一个值:
    • E 已计算。 如果此评估导致异常,则不会执行进一步的步骤。
    • 如果 E 的值为 null,则会引发 System.NullReferenceException,并且不再执行其他步骤。
    • 将分配一个委托类型 D 的新实例。 如果没有足够的内存可用于分配新实例,则会引发 System.OutOfMemoryException,并且不会执行进一步的步骤。
    • 新委托实例通过调用 E 的单项调用列表进行初始化。

委托的调用列表在委托实例化时确定,并在委托的整个生命周期内保持不变。 换言之,委托一旦创建,就无法更改其目标可调用实体。

注意:请记住,当两个委托合并或从另一个委托中移除一个委托时,会产生一个新的委托;现有委托的内容不会改变。 尾注

无法创建一个指向属性、索引器、用户定义运算符、实例构造函数、终结器或静态构造函数的委托。

示例:如上所述,当从方法组创建委托时,委托的参数列表和返回类型将决定选择哪个重载方法。 在示例中

delegate double DoubleFunc(double x);

class A
{
    DoubleFunc f = new DoubleFunc(Square);

    static float Square(float x) => x * x;
    static double Square(double x) => x * x;
}

使用一个指向第二个 Square 方法的委托对 A.f 字段进行初始化,因为该方法的参数列表和返回类型与 DoubleFunc完全匹配。 如果没有第二个 Square 方法,则会发生编译时错误。

结束示例

12.8.17.7 匿名对象创建表达式

anonymous_object_creation_expression 用于创建匿名类型的对象。

anonymous_object_creation_expression
    : 'new' anonymous_object_initializer
    ;

anonymous_object_initializer
    : '{' member_declarator_list? '}'
    | '{' member_declarator_list ',' '}'
    ;

member_declarator_list
    : member_declarator (',' member_declarator)*
    ;

member_declarator
    : simple_name
    | member_access
    | null_conditional_projection_initializer
    | base_access
    | identifier '=' expression
    ;

匿名对象初始值设定项声明匿名类型并返回该类型的实例。 匿名类型是直接从 object继承的无名称类类型。 匿名类型的成员是从用于创建该类型的实例的匿名对象初始值设定项推断出的只读属性序列。 具体来说,匿名对象初始值设定项的形式为

new { p₁=e₁,p₂=e₂,pᵥ=eᵥ}

声明了一个匿名类型,其形式为

class __Anonymous1
{
    private readonly «T1» «f1»;
    private readonly «T2» «f2»;
    ...
    private readonly «Tn» «fn»;

    public __Anonymous1(«T1» «a1», «T2» «a2»,..., «Tn» «an»)
    {
        «f1» = «a1»;
        «f2» = «a2»;
        ...
        «fn» = «an»;
    }

    public «T1» «p1» { get { return «f1»; } }
    public «T2» «p2» { get { return «f2»; } }
    ...
    public «Tn» «pn» { get { return «fn»; } }
    public override bool Equals(object __o) { ... }
    public override int GetHashCode() { ... }
}

在这里,每个 «Tx» 都是 «ex» 表达式的相应类型。 member_declarator 中使用的表达式应具有类型。 因此,在 member_declarator 中,如果表达式是 null 或匿名函数,在编译时会导致错误。

匿名类型的名称及其 Equals 方法的名称由编译器自动生成,不能在程序文本中引用。

在同一程序中,两个匿名对象初始值设定项若指定具有相同名称和编译时类型的属性序列,并且顺序相同,将会生成同一匿名类型的实例。

示例:在示例中

var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;

因为 p1p2 具有相同的匿名类型,所以允许在最后一行进行分配。

结束示例

匿名类型的 EqualsGetHashcode 方法将替代从 object继承的方法,并在属性的 EqualsGetHashcode 方面定义,以便仅当其所有属性相等时,同一匿名类型的两个实例才相等。

成员声明符可以缩写为简单名称(§12.8.4)、成员访问(§12.8.7)、空条件投影初始化器(§12.8.8)或基本访问(§12.8.15)。 这称为投影初始值设定项,是同名属性声明和赋值的简称。 具体来说,以下形式的成员声明符

«identifier»«expr» . «identifier»«expr» ? . «identifier»

分别精确等效于:

«identifer» = «identifier»«identifier» = «expr» . «identifier»«identifier» = «expr» ? . «identifier»

因此,在投影初始值设定项中,标识符既选择值,也选择赋值的字段或属性。 直观地说,投影初始化器不仅投影一个值,还投影该值的名称。

12.8.18 typeof 运算符

typeof 运算符用于获取类型的 System.Type 对象。

typeof_expression
    : 'typeof' '(' type ')'
    | 'typeof' '(' unbound_type_name ')'
    | 'typeof' '(' 'void' ')'
    ;

unbound_type_name
    : identifier generic_dimension_specifier?
    | identifier '::' identifier generic_dimension_specifier?
    | unbound_type_name '.' identifier generic_dimension_specifier?
    ;

generic_dimension_specifier
    : '<' comma* '>'
    ;

comma
    : ','
    ;

typeof_expression 的第一种形式由 typeof 关键字和括号内的类型组成。 此类表达式的结果是所指示类型的 System.Type 对象。 对于任何给定类型,只有一个 System.Type 对象。 这意味着对于类型 Ttypeof(T) == typeof(T) 始终为 True。 类型不能是 dynamic

typeof_expression 的第二种形式由 typeof 关键字和括号中的 unbound_type_name 组成。

注意unbound_type_nametype_name (§7.8) 非常相似,除了 unbound_type_name 包含 generic_dimension_specifiertype_name 包含 type_argument_list 之外。 尾注

typeof_expression 的操作数是满足 unbound_type_nametype_name语法的标记序列时,即当它既不包含 generic_dimension_specifier 也不包含 type_argument_list时,标记序列被视为 type_nameunbound_type_name 的含义确定如下:

  • 通过将每个 generic_dimension_specifier 替换为 type_argument_list,其中每个 type_argument 具有相同数量的逗号和关键字 object,以便将标记序列转换为 type_name
  • 计算 type_name 的结果,同时忽略所有类型参数约束。
  • unbound_type_name 解析为与所构造的类型相关联的非绑定泛型 (§8.4)。

如果类型名称是可 null 引用类型,则属于错误。

typeof_expression 的结果是生成的非绑定泛型的 System.Type 对象。

typeof_expression 的第三种形式由 typeof 关键字和括号内的 void 关键字组成。 此类形式的表达式结果是表示缺少类型的 System.Type 对象。 typeof(void) 返回的类型对象不同于任何类型返回的类型对象。

注释:这种特殊的 System.Type 对象在允许语言中对方法进行反射的类库中非常有用,其中这些方法希望通过 System.Type实例来表示任何方法(包括 void 方法)的返回类型。 尾注

typeof 运算符可用于类型参数。 如果已知类型名称为可以为 null 的引用类型,则为编译时错误。 结果是与类型参数绑定的运行时类型的 System.Type 对象。 如果运行时类型是可为 null 的引用类型,则结果是相应的不可为 null 的引用类型。 typeof 运算符还可用于构造类型或未绑定泛型类型(§8.4.4)。 未绑定泛型类型的 System.Type 对象与实例类型的 System.Type 对象不同(§15.3.2)。 实例类型始终是运行时的封闭构造类型,因此其 System.Type 对象依赖于正在使用的运行时类型参数。 另一方面,未绑定的泛型类型没有类型参数,无论运行时类型参数如何,都会生成相同的 System.Type 对象。

示例:示例

class X<T>
{
    public static void PrintTypes()
    {
        Type[] t =
        {
            typeof(int),
            typeof(System.Int32),
            typeof(string),
            typeof(double[]),
            typeof(void),
            typeof(T),
            typeof(X<T>),
            typeof(X<X<T>>),
            typeof(X<>)
        };
        for (int i = 0; i < t.Length; i++)
        {
            Console.WriteLine(t[i]);
        }
    }
}

class Test
{
    static void Main()
    {
        X<int>.PrintTypes();
    }
}

生成以下输出:

System.Int32
System.Int32
System.String
System.Double[]
System.Void
System.Int32
X`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]

请注意,intSystem.Int32 的类型相同。 typeof(X<>) 的结果不依赖于类型参数,而是 typeof(X<T>) 的结果。

结束示例

12.8.19 sizeof 运算符

sizeof 运算符返回给定类型的变量占用的 8 位字节数。 作为 sizeof 的操作数指定的类型应是 unmanaged_type (§8.8)。

sizeof_expression
    : 'sizeof' '(' unmanaged_type ')'
    ;

对于某些预定义类型,sizeof 运算符生成常量 int 值,如下表所示:

表达式 结果
sizeof(sbyte) 1
sizeof(byte) 1
sizeof(short) 2
sizeof(ushort) 2
sizeof(int) 4
sizeof(uint) 4
sizeof(long) 8
sizeof(ulong) 8
sizeof(char) 2
sizeof(float) 4
sizeof(double) 8
sizeof(bool) 1
sizeof(decimal) 16

对于枚举类型 T,表达式 sizeof(T) 的结果是一个等于其基础类型大小的常量值,如上所示。 对于所有其他操作数类型,sizeof 运算符在 §23.6.9中指定。

12.8.20 checked 和 unchecked 运算符

checkedunchecked 运算符用于控制整型算术运算和转换的溢出检查上下文。

checked_expression
    : 'checked' '(' expression ')'
    ;

unchecked_expression
    : 'unchecked' '(' expression ')'
    ;

checked 运算符在受限上下文中计算包含的表达式,而 unchecked 运算符则在非受限上下文中计算包含的表达式。 checked_expressionunchecked_expressionparenthesized_expression (§12.8.5) 完全对应,只是所包含的表达式在给定的溢出检查上下文中进行求值。

溢出检查上下文也可以通过 checkedunchecked 语句(§13.12)来控制。

下列操作会受到由 checked 和 unchecked 运算符和语句建立的溢出检查上下文的影响:

  • 当操作数为整型或枚举类型时,使用预定义的 ++-- 运算符(即§12.8.16§12.9.6)。
  • 当操作数为整型时,使用预定义的 - 一元运算符 (§12.9.3)。
  • 当两个操作数都是积分或枚举类型时,使用预定义的 +-*/ 二元操作数 (§12.10)。
  • 显式数值转换(§10.3.2)从一个整型或枚举类型转换为另一个整型或枚举类型,或者从 floatdouble 转换为整型或枚举类型。

当上述操作之一生成的结果太大而无法在目标类型中表示时,操作所处的上下文将决定结果的行为:

  • checked 上下文中,如果操作是常量表达式(§12.23),则会发生编译时错误。 否则,当在运行时进行操作时,会抛出 System.OverflowException
  • 通过丢弃不适合目标类型的高序位可在 unchecked 上下文中将结果截断。

对于未被任何 checkedunchecked 运算符或语句括起来的非常数表达式 (§12.23)(在运行时求值的表达式),默认的溢出检查上下文为 unchecked,除非外部因素(如编译器开关和执行环境配置)要求进行 checked 求值。

对于常量表达式 (§12.23)(可在编译时完全计算的表达式),将始终检查默认的溢出检查上下文。 除非在 unchecked 上下文中显式放置常量表达式,否则在编译时间计算表达式过程中出现的溢出将始终导致编译时错误。

匿名函数的主体不受其出现时的 checkedunchecked 上下文的影响。

示例:在以下代码中

class Test
{
    static readonly int x = 1000000;
    static readonly int y = 1000000;

    static int F() => checked(x * y);    // Throws OverflowException
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Depends on default
}

编译时不会报告错误,因为这两个表达式都无法在编译时进行评估。 在运行时,F 方法会引发 System.OverflowExceptionG 方法返回 -727379968(超出范围结果的低 32 位)。 H 方法的行为取决于编译的默认溢出检查上下文,但它与 F 相同或与 G相同。

结束示例

示例:在以下代码中

class Test
{
    const int x = 1000000;
    const int y = 1000000;

    static int F() => checked(x * y);    // Compile-time error, overflow
    static int G() => unchecked(x * y);  // Returns -727379968
    static int H() => x * y;             // Compile-time error, overflow
}

在计算 FH 中的常量表达式时发生的溢出会导致报告编译时错误,因为这些表达式是在 checked 上下文中计算的。 在 G中计算常量表达式时也会发生溢出,但由于计算发生在 unchecked 上下文中,因此不会报告溢出。

结束示例

checkedunchecked 运算符只会影响文本包含在“(”和“)”标记中的运算的溢出检查上下文。 运算符对函数成员没有影响,这些函数成员是在对所含表达式进行求值后被调用的。

示例:在以下代码中

class Test
{
    static int Multiply(int x, int y) => x * y;

    static int F() => checked(Multiply(1000000, 1000000));
}

在 F 中使用 checked 不会影响 Multiplyx * y 的求值,因此 x * y 将在默认的溢出检查上下文中求值。

结束示例

在十六进制表示法中编写带符号整型类型的常量时,unchecked 运算符很方便。

示例

class Test
{
    public const int AllBits = unchecked((int)0xFFFFFFFF);
    public const int HighBit = unchecked((int)0x80000000);
}

上述两个十六进制常量都属于类型 uint。 由于常量超出了 int 的范围,如果没有 unchecked 运算符,将常量强制转换为 int 会产生编译时错误。

结束示例

注释checkedunchecked 运算符和语句允许程序员控制某些数值计算的某些方面。 但是,某些数值运算符的行为取决于其操作数的数据类型。 例如,两个小数相乘总是会在溢出时出现异常,即使在显式未检查的结构中也是如此。 同样,即使在显式检查的结构中,两个浮点数相乘也不会出现溢出异常。 此外,其他运算符永远不会受到检查模式的影响,无论是默认还是显式。 尾注

12.8.21 默认值表达式

默认值表达式用于获取类型的默认值(§9.3)。

default_value_expression
    : explictly_typed_default
    | default_literal
    ;

explictly_typed_default
    : 'default' '(' type ')'
    ;

default_literal
    : 'default'
    ;

default_literal 表示默认值 (§9.3)。 它没有类型,但可以通过默认文本转换(§10.2.16)转换为任何类型。

default_value_expression 的结果是 explictly_typed_default 中显式类型的默认值 (§9.3),或者是 default_value_expression 转换的目标类型。

如果类型为下列类型之一,则 default_value_expression 是常量表达式 (§12.23):

  • 一个引用类型
  • 已知为引用类型的类型参数 (§8.2);
  • 以下值类型之一:sbytebyteshortushortintuintlongulongcharfloatdoubledecimalbool,;或
  • 任何枚举类型。

12.8.22 堆栈分配

堆栈分配表达式从执行堆栈分配内存块。 执行堆栈 是存储局部变量的内存区域。 执行堆栈不是托管堆的一部分。 当当前函数返回时,将自动恢复用于本地变量存储的内存。

堆栈分配表达式的安全上下文规则在 §16.4.12.7中介绍。

stackalloc_expression
    : 'stackalloc' unmanaged_type '[' expression ']'
    | 'stackalloc' unmanaged_type? '[' constant_expression? ']' stackalloc_initializer
    ;

stackalloc_initializer
     : '{' stackalloc_initializer_element_list '}'
     ;

stackalloc_initializer_element_list
     : stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
     ;
    
stackalloc_element_initializer
    : expression
    ;

unmanaged_type (§8.8) 表示将存储在新分配位置的项目类型,expression 表示这些项目的数量。 这些组合在一起,指定了所需的分配大小。 表达式 的类型应隐式转换为类型 int

由于堆栈分配的大小不能为负值,因此将项目数指定为求值为负值的 constant_expression 是一个编译时错误。

在运行时,如果要分配的项数为负值,则行为是未定义的。 如果为零,则不进行分配,返回的值是实现定义的。 如果没有足够的内存来分配项目,就会引发 System.StackOverflowException

当存在 堆栈分配初始化器 时:

  • 如果省略 unmanaged_type,则将根据 stackalloc_element_initializer 的集合的最佳通用类型规则 (§12.6.3.15) 来推断。
  • 如果省略 constant_expression,则推断为 stackalloc_element_initializer 的数量。
  • 如果 constant_expression 存在,它应等于 stackalloc_element_initializer 的数量。

每个 stackalloc_element_initializer 都应隐式转换为 unmanaged_type (§10.2)。 stackalloc_element_initializer 从索引为零的元素开始,按递增顺序初始化已分配内存中的元素。 如果没有 stackalloc_initializer,新分配的内存的内容是未定义的。

如果 stackalloc_expression 直接作为 local_variable_declaration (§13.6.2) 的初始化表达式出现,其中 local_variable_type 是指针类型 (§23.3) 或推断类型 (var),那么 stackalloc_expression 的结果就是 T* 类型的指针 (§23.9)。 在这种情况下,stackalloc_expression 必须出现在不安全的代码中。 否则,stackalloc_expression 的结果是 Span<T> 类型的实例,其中 Tunmanaged_type 类型:

  • Span<T> (§C.3) 是一个 ref 结构类型 (§16.2.3),它将一个内存块(这里是由 stackalloc_expression 分配的内存块)显示为类型 (T) 项目的可索引集合。
  • 结果的 Length 属性返回分配的项数。
  • 结果的索引器 (§15.9) 会返回一个 variable_reference (§9.5),指向已分配块中的一个项目,并进行范围检查。

catchfinally 块中不允许使用堆栈分配初始化器(§13.11)。

注意:无法显式释放使用 stackalloc分配的内存。 尾注

在函数成员执行过程中创建的所有堆栈分配的内存块,都会在该函数成员返回时自动丢弃。

stackalloc 运算符外,C# 不提供用于管理非垃圾回收内存的预定义构造。 此类服务通常通过支持类库或直接从基础操作系统导入来提供。

示例

// Memory uninitialized
Span<int> span1 = stackalloc int[3];
// Memory initialized
Span<int> span2 = stackalloc int[3] { -10, -15, -30 };
// Type int is inferred
Span<int> span3 = stackalloc[] { 11, 12, 13 };
// Error; result is int*, not allowed in a safe context
var span4 = stackalloc[] { 11, 12, 13 };
// Error; no conversion from Span<int> to Span<long>
Span<long> span5 = stackalloc[] { 11, 12, 13 };
// Converts 11 and 13, and returns Span<long> 
Span<long> span6 = stackalloc[] { 11, 12L, 13 };
// Converts all and returns Span<long>
Span<long> span7 = stackalloc long[] { 11, 12, 13 };
// Implicit conversion of Span<T>
ReadOnlySpan<int> span8 = stackalloc int[] { 10, 22, 30 };
// Implicit conversion of Span<T>
Widget<double> span9 = stackalloc double[] { 1.2, 5.6 };

public class Widget<T>
{
    public static implicit operator Widget<T>(Span<double> sp) { return null; }
}

span8的情况下,stackalloc 产生一个 Span<int>,由隐式运算符转换为 ReadOnlySpan<int>。 同样,对于 span9,生成的 Span<double> 将使用转换转换为用户定义的类型 Widget<double>,如图所示。 结束示例

12.8.23 nameof 运算符

nameof_expression 用于以常量字符串形式获取程序实体的名称。

nameof_expression
    : 'nameof' '(' named_entity ')'
    ;
    
named_entity
    : named_entity_target ('.' identifier type_argument_list?)*
    ;
    
named_entity_target
    : simple_name
    | 'this'
    | 'base'
    | predefined_type 
    | qualified_alias_member
    ;

由于 nameof 不是关键字,因此 nameof_expression 在调用简单名称 nameof时总是语法上含糊不清。 出于兼容性原因,如果名称 nameof 的名称查找 (§12.8.4) 成功,则表达式将被视为 invocation_expression - 无论调用是否有效。 否则,它就是一个 nameof_expression

在编译时对 named_entity 执行简单名称和成员访问查找,遵循 §12.8.4§12.8.7中所述的规则。 但是,当 §12.8.4§12.8.7 中描述的查找会因为在静态上下文中找到实例成员而导致错误时,nameof_expression 不会产生此类错误。

如果指定方法组的 named_entity 具有 type_argument_list,则属于编译时错误。 如果 named_entity_target 的类型为 dynamic,则属于编译时错误。

nameof_expressionstring类型的常量表达式,在运行时不起作用。 具体来说,它的 named_entity 不会被求值,在进行定赋值分析 (§9.4.4.22) 时也会被忽略。 其值是在可选的最终 type_argument_list 之前的 named_entity 的最后一个标识符,转换方式如下:

  • 前缀“@”(如果使用)将被删除。
  • 每个 unicode_escape_sequence 都会被转换成相应的 Unicode 字符。
  • 任何 formatting_characters 都会被删除。

在测试标识符之间的相等性时,§6.4.3 中应用了这些转换。

示例:假设在 System.Collections.Generic 命名空间中声明了一个泛型 List<T>,下面说明了各种 nameof 表达式的结果:

using TestAlias = System.String;

class Program
{
    static void Main()
    {
        var point = (x: 3, y: 4);

        string n1 = nameof(System);                      // "System"
        string n2 = nameof(System.Collections.Generic);  // "Generic"
        string n3 = nameof(point);                       // "point"
        string n4 = nameof(point.x);                     // "x"
        string n5 = nameof(Program);                     // "Program"
        string n6 = nameof(System.Int32);                // "Int32"
        string n7 = nameof(TestAlias);                   // "TestAlias"
        string n8 = nameof(List<int>);                   // "List"
        string n9 = nameof(Program.InstanceMethod);      // "InstanceMethod"
        string n10 = nameof(Program.GenericMethod);      // "GenericMethod"
        string n11 = nameof(Program.NestedClass);        // "NestedClass"

        // Invalid
        // string x1 = nameof(List<>);            // Empty type argument list
        // string x2 = nameof(List<T>);           // T is not in scope
        // string x3 = nameof(GenericMethod<>);   // Empty type argument list
        // string x4 = nameof(GenericMethod<T>);  // T is not in scope
        // string x5 = nameof(int);               // Keywords not permitted
        // Type arguments not permitted for method group
        // string x6 = nameof(GenericMethod<Program>);
    }

    void InstanceMethod() { }

    void GenericMethod<T>()
    {
        string n1 = nameof(List<T>); // "List"
        string n2 = nameof(T);       // "T"
    }

    class NestedClass { }
}

此示例中可能令人惊讶的部分是将 nameof(System.Collections.Generic) 解析为“Generic”而不是完整命名空间,nameof(TestAlias) 为“TestAlias”而不是“String”。 结束示例

12.8.24 匿名方法表达式

anonymous_method_expression 是定义匿名函数的两种方法之一。 §12.19中进一步介绍了这些内容。

12.9 一元运算符

12.9.1 常规

+-!(仅限逻辑否定 §12.9.4)、~++--、强制转换和 await 运算符称为一元运算符。

注意:后缀 null 宽容运算符 (§12.8.9) !,由于其编译时不可重载的性质,未包含在上述列表中。 尾注

unary_expression
    : primary_expression
    | '+' unary_expression
    | '-' unary_expression
    | logical_negation_operator unary_expression
    | '~' unary_expression
    | pre_increment_expression
    | pre_decrement_expression
    | cast_expression
    | await_expression
    | pointer_indirection_expression    // unsafe code support
    | addressof_expression              // unsafe code support
    ;

pointer_indirection_expression§23.6.2)和 addressof_expression§23.6.5)仅在不安全代码中适用(§23)。

如果 unary_expression 的操作数在编译时具有类型 dynamic,则会进行动态绑定(§12.3.3)。 在这种情况下,unary_expression 的编译时类型是 dynamic,下面描述的解析将在运行时使用操作数的运行时类型进行。

12.9.2 一元加运算符

对于表单 +x的操作,将应用一元运算符重载解析(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 预定义的一元加运算符为:

int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);

对于每个运算符,结果只是操作数的值。

还预定义了上面定义的未提升预定义一元加运算符的提升 (§12.4.8) 形式。

12.9.3 一元减运算符

对于表单 –x的操作,将应用一元运算符重载解析(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 预定义的一元减运算符为:

  • 整数求反:

    int operator –(int x);
    long operator –(long x);
    

    通过从零减去 X 来计算结果。 如果 X 的值是操作数类型的最小可表示值(对于 int 为 −2³¹,或对于 long为 −2⁶³),那么 X 的数学取反在操作数类型内不可表示。 如果发生在 checked 上下文中,则会引发 System.OverflowException;如果发生在 unchecked 上下文中,则结果将是操作数的值,并且不会报告溢出。

    如果求反运算符的操作数的类型为 uint,则转换为类型 long,结果的类型被转换为 long。 允许将 int−2147483648 (−2³¹) 写成十进制整数字面的规则是个例外 (§6.4.5.3)。

    如果求反运算符的操作数的类型为 ulong,则会发生编译时错误。 允许将 long−9223372036854775808 (−2⁶³) 写成十进制整数字面的规则是个例外 (§6.4.5.3)

  • 浮点求反:

    float operator –(float x);
    double operator –(double x);
    

    结果是符号反转的 X 值。 如果 xNaN,则结果也为 NaN

  • 十进制求反:

    decimal operator –(decimal x);
    

    通过从零减去 X 来计算结果。 十进制求反等效于使用类型为 System.Decimal的一元减号运算符。

还预定义了上面定义的未提升预定义一元减运算符的提升 (§12.4.8) 形式。

12.9.4 逻辑求反运算符

对于表单 !x的操作,将应用一元运算符重载解析(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 只有一个预定义的逻辑否定运算符存在:

bool operator !(bool x);

此运算符计算操作数的逻辑求反:如果操作数为 true,则结果为 false。 如果操作数是 false,则结果是 true

还预定义了上面定义的逻辑非运算符的提升 (§12.4.8) 形式。

注意:前缀逻辑非运算符和后缀 null 包容运算符 (§12.9.4) 虽然用同一个词性标记 (!) 表示,但它们是不同的。 尾注

12.9.5 按位求补运算符

对于表单 ~x的操作,将应用一元运算符重载解析(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 预定义的按位求补运算符包括:

int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);

对于每个运算符,运算结果都是 x 的按位求补。

每个枚举类型 E 都隐式提供以下按位求补运算符:

E operator ~(E x);

计算 ~x的结果(其中 X 是具有基础类型 U的枚举类型 E 的表达式,与计算 (E)(~(U)x)完全相同,但对 E 的转换始终像在 unchecked 上下文中一样执行(§12.8.20)。

还预定义了上面定义的未提升预定义按位求补运算符的提升 (§12.4.8) 形式。

12.9.6 前缀增量和减量运算符

pre_increment_expression
    : '++' unary_expression
    ;

pre_decrement_expression
    : '--' unary_expression
    ;

前缀递增或递减操作的操作数应是一个被归类为变量、属性访问或索引器访问的表达式。 操作的结果是与操作数类型相同的值。

如果前缀递增或递减操作的操作数是属性或索引器访问,则属性或索引器应同时具有 get 和 set 访问器。 如果情况并非如此,则会发生绑定时错误。

一元运算符重载分辨率(§12.4.4)应用于选择特定的运算符实现。 以下类型存在预定义的 ++-- 运算符:sbytebyteshortushortintuintlongulongcharfloatdoubledecimal和任意枚举类型。 预定义的 ++ 运算符返回通过向操作数添加 1 生成的值,预定义的 -- 运算符返回通过从操作数减去 1 生成的值。 在 checked 上下文中,如果加或减的结果超出了结果类型的范围,并且结果类型是整数型或枚举型,则会引发 System.OverflowException

应有从所选一元运算符的返回类型到 unary_expression类型的隐式转换,否则会发生编译时错误。

对于格式为 ++x--x 的前缀递增或递减操作,其运行时处理包括以下步骤:

  • 如果 x 被归类为变量:
    • x 会被求值以生成变量。
    • x 的值转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。
    • 运算符返回的值将转换为 x的类型。 生成的值存储在由 x计算给出的位置。
    • 并成为运算的结果。
  • 如果 x 被归类为属性或索引器访问:
    • 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),并在后续的 get 和 set 访问器调用中使用这些结果。
    • x 的 get 访问器会被调用。
    • get 访问器返回的值会转换为所选运算符的操作数类型,运算符会以该值作为参数被调用。
    • 运算符返回的值将转换为 x的类型。 调用 x 的 set 访问器时,该值将作为其值参数。
    • 此值也将成为操作的结果。

++-- 运算符还支持后缀表示法(§12.8.16)。 x++x-- 的结果是操作前 x 的值,而 ++x--x 的结果是操作后 x 的值。 在任一情况下,x 自身在操作后具有相同的值。

运算符 ++ 或运算符 -- 的实现可以使用后缀或前缀表示法来调用。 不能为这两个表示法使用单独的运算符实现。

还预定义了上面定义的未提升预定义前缀递增和递减运算符的提升 (§12.4.8) 形式。

12.9.7 强制转换表达式

cast_expression 用于将一个表达式显式转换为给定类型。

cast_expression
    : '(' type ')' unary_expression
    ;

形式为 (T)Ecast_expression 会将 E 的值显式转换 (§10.3) 为类型 T,其中 T 是一个类型,E 是一个 unary_expression。 如果不存在从 ET的显式转换,则会发生绑定时错误。 否则,结果是由显式转换生成的值。 即使 E 表示变量,结果也始终被分类为值。

cast_expression 的语法会导致某些语法歧义。

示例:表达式 (x)–y 既可以解释为 cast_expression(将 –y 强制转换为类型 x),也可以解释为 additive_expressionparenthesized_expression 结合(计算值 x – y)。 结束示例

若要解决 cast_expression 歧义,存在以下规则:仅当以下至少一个为 true 时,括在括号中的一个或多个标记(§6.4)序列被视为 cast_expression 的开始:

  • 标记序列是类型的正确语法,但不适用于表达式。
  • 标记序列是类型的正确语法,右括号后面的标记是标记“~”, 标记“!”,标记“(”,标识符(§6.4.3)、文本(§6.4.5),或任何关键字(§6.4.4)(asis除外)。

上述术语“正确语法”仅表示令牌序列应符合特定的语法生成规则。 它特别不考虑任何构成标识符的实际含义。

示例:如果 xy 是标识符,则即使 x.y 实际上没有表示类型,x.y 也是正确的语法。 结束示例

注意:根据消除歧义的规则,如果 xy 是标识符,那么 (x)y(x)(y)(x)(-y)cast_expression,但 (x)-y 不是,即使 x 标识的是一个类型。 但是,如果 x 是标识预定义类型的关键字(如 int),则所有四种形式都是 cast_expression(因为此类关键字本身不可能是表达式)。 尾注

12.9.8 Await 表达式

12.9.8.1 常规

await 运算符用于暂停对封闭异步函数的求值,直到操作数所代表的异步操作完成为止。

await_expression
    : 'await' unary_expression
    ;

await_expression 仅允许出现在异步函数的主体中 (§15.15)。 在最近的封闭异步函数中,await_expression 不得出现在以下位置:

  • 嵌套(非异步)匿名函数内部
  • lock_statement 块内
  • 在匿名函数转换为表达式树类型时 (§10.7.3)
  • 在不安全的上下文中

注意await_expression 不能出现在 query_expression 中的大多数位置,因为这些表达式在语法上被转换为使用非异步 lambda 表达式。 尾注

在异步函数中,await 不得用作 available_identifier,但可以使用逐字标识符 @await。 因此,await_expressions 和涉及标识符的各种表达式之间没有语法歧义。 在异步函数之外,await 充当正常标识符。

await_expression 的操作数称为 task。 它表示异步操作,在对 await_expression 求值时可能已完成,也可能未完成。 await 运算符的目的是暂停执行封闭异步函数,直到等待的任务完成,然后获取其结果。

12.9.8.2 可等待表达式

一个 await_expression 的任务必须可等待。 如果以下条件之一成立,则表达式 t 是可等待的:

  • t 是编译时类型 dynamic
  • t 具有一个名为 GetAwaiter 的可访问实例或扩展方法,没有参数,也没有类型参数,并且返回类型 A,以下所有参数均保留:
    • A 实现接口 System.Runtime.CompilerServices.INotifyCompletion(以下简称为 INotifyCompletion
    • A 具有一个可访问、可读的实例属性 IsCompleted,该属性的类型为 bool
    • A 具有可访问的实例方法,GetResult 没有参数,也没有类型参数

GetAwaiter 方法的目的是为任务获取 awaiter。 类型 A 称为 await 表达式的 awaiter 类型

IsCompleted 属性的目的是确定任务是否已完成。 如果是这样,则无需暂停评估。

INotifyCompletion.OnCompleted 方法的目的是为任务注册一个“延续”,即任务完成后将调用的委托(类型为 System.Action)。

GetResult 方法的目的是在任务完成后获取任务的结果。 这一结果可能是成功完成,可能带有结果值,也可能是 GetResult 方法引发的异常。

12.9.8.3 await 表达式的分类

表达式 await t 的分类方式与表达式 (t).GetAwaiter().GetResult()相同。 因此,如果 GetResult 的返回类型是 void,则 await_expression 将被归类为空。 如果它具有除void 之外的返回类型 T,那么 await_expression 被归类为 T类型的值。

12.9.8.4 await 表达式的运行时计算

在运行时,表达式 await t 的计算方式如下:

  • 通过对表达式 (t).GetAwaiter() 进行求值,可以获得 awaiter a
  • boolb 是通过对表达式 (a).IsCompleted 求值来获取。
  • 如果 bfalse,则评估取决于 a 是否实现接口 System.Runtime.CompilerServices.ICriticalNotifyCompletion(以下简称 ICriticalNotifyCompletion)。 此检查是在绑定时进行的;即,如果 a 在运行时具有编译时类型 dynamic,则在运行时进行检查,否则在编译时进行检查。 让 r 表示恢复委托 (§15.15):
    • 如果 a 没有实现 ICriticalNotifyCompletion,则对表达式 ((a) as INotifyCompletion).OnCompleted(r) 进行求值。
    • 如果 a 实现了 ICriticalNotifyCompletion,则对表达式 ((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r) 进行求值。
    • 然后暂停评估,并将控件返回到异步函数的当前调用方。
  • 紧接着(如果 btrue)或稍后调用恢复委托(如果 bfalse)时,表达式 (a).GetResult() 将被求值。 如果返回一个值,则该值是 await_expression的结果。 否则,结果为零。

awaiter 对接口方法 INotifyCompletion.OnCompletedICriticalNotifyCompletion.UnsafeOnCompleted 的实现应导致委托 r 最多被调用一次。 否则,未定义封闭异步函数的行为。

12.10 算术运算符

12.10.1 常规

*/%+- 运算符称为算术运算符。

multiplicative_expression
    : unary_expression
    | multiplicative_expression '*' unary_expression
    | multiplicative_expression '/' unary_expression
    | multiplicative_expression '%' unary_expression
    ;

additive_expression
    : multiplicative_expression
    | additive_expression '+' multiplicative_expression
    | additive_expression '-' multiplicative_expression
    ;

如果算术运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型为 dynamic,下面所述的解析过程将在运行时,使用那些编译时类型为 dynamic的操作数的运行时类型进行。

12.10.2 乘法运算符

对于表单 x * y的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

下面列出了预定义乘法运算符。 所有运算符都计算 xy 的乘积。

  • 整数乘法:

    int operator *(int x, int y);
    uint operator *(uint x, uint y);
    long operator *(long x, long y);
    ulong operator *(ulong x, ulong y);
    

    checked 上下文中,如果乘积超出结果类型的范围,就会引发 System.OverflowException。 在 unchecked 上下文中,不会报告溢出,并且会丢弃结果类型范围之外的任何重要高阶位。

  • 浮点乘法:

    float operator *(float x, float y);
    double operator *(double x, double y);
    

    该产品是根据 IEC 60559 算术规则计算的。 下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,xy 是正有限值。 zx * y 的结果,并四舍五入为最接近的可表示值。 如果结果的大小对于目标类型太大,则 z 为无穷大。 由于舍入,即使 xy 都不为零,z 也可能为零。

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +0 -0 +∞ -∞ NaN
    -x -z +z -0 +0 -∞ +∞ NaN
    +0 +0 -0 +0 -0 NaN NaN NaN
    -0 -0 +0 -0 +0 NaN NaN NaN
    +∞ +∞ -∞ NaN NaN +∞ -∞ NaN
    -∞ -∞ +∞ NaN NaN -∞ +∞ NaN
    NaN NaN NaN NaN NaN NaN NaN NaN

    (除非另有说明,在 §12.10.2的浮点表中 –§12.10.6 使用“+”表示值为正值;使用“-”表示值为负;缺少符号意味着该值可能是正值或负数,或者没有符号(NaN)。

  • 十进制乘法:

    decimal operator *(decimal x, decimal y);
    

    如果生成的值的大小太大而无法以小数格式表示,则会引发 System.OverflowException。 由于舍入,即使两个操作数都不为零,结果也可能为零。 在四舍五入之前,结果的精度是两个操作数的精度之和。 十进制乘法等效于使用 System.Decimal类型的乘法运算符。

还预定义了上面定义的未提升预定义乘法运算符的提升 (§12.4.8) 形式。

12.10.3 除法运算符

对于表单 x / y的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

下面列出了预定义的除法运算符。 所有运算符都计算 xy 的商。

  • 整数除法:

    int operator /(int x, int y);
    uint operator /(uint x, uint y);
    long operator /(long x, long y);
    ulong operator /(ulong x, ulong y);
    

    如果右操作数的值为零,则会引发 System.DivideByZeroException

    除法将结果舍入为零。 因此,结果的绝对值是小于或等于两个操作数商的绝对值的可能最大整数。 当两个操作数具有相同的符号,当两个操作数具有相反的符号时,结果为零或正。

    如果左操作数是最小可表示的 intlong 值,并且右操作数是 –1,则会发生溢出。 在 checked 上下文中,这会引发 System.ArithmeticException(或其子类)。 在 unchecked 上下文中,是引发 System.ArithmeticException(或其子类),还是不报告溢出,而是将结果值作为左操作数的值,这取决于具体的实现定义。

  • 浮点除法:

    float operator /(float x, float y);
    double operator /(double x, double y);
    

    商是根据 IEC 60559 算术规则计算得出的。 下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,xy 是正有限值。 zx / y 的结果,并四舍五入为最接近的可表示值。

    +y -y +0 -0 +∞ -∞ NaN
    +x +z -z +∞ -∞ +0 -0 NaN
    -x -z +z -∞ +∞ -0 +0 NaN
    +0 +0 -0 NaN NaN +0 -0 NaN
    -0 -0 +0 NaN NaN -0 +0 NaN
    +∞ +∞ -∞ +∞ -∞ NaN NaN NaN
    -∞ -∞ +∞ -∞ +∞ NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • 小数除法:

    decimal operator /(decimal x, decimal y);
    

    如果右操作数的值为零,则会引发 System.DivideByZeroException。 如果生成的值的大小太大而无法以小数格式表示,则会引发 System.OverflowException。 由于舍入,即使第一个操作数不是零,结果也可能为零。 在四舍五入之前,结果的精度最接近于首选精度,从而保留与精确结果相等的结果。 首选精度等于 x 精度减去 y 精度。

    十进制除法等效于使用类型为 System.Decimal的除法运算符。

还预定义了上面定义的未提升预定义除法运算符的提升 (§12.4.8) 形式。

12.10.4 余数运算符

对于表单 x % y的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

下面列出了预定义的余数运算符。 运算符都计算 xy之间的除法余数。

  • 整数余数:

    int operator %(int x, int y);
    uint operator %(uint x, uint y);
    long operator %(long x, long y);
    ulong operator %(ulong x, ulong y);
    

    x % y 的结果是由 x – (x / y) * y 生成的值。 如果 y 为零,则会引发 System.DivideByZeroException

    如果左侧操作数是最小的 intlong 值,而右侧操作数是 –1,那么只有当 x / y 引发异常时,才会引发 System.OverflowException

  • 浮点余数:

    float operator %(float x, float y);
    double operator %(double x, double y);
    

    下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,xy 是正有限值。 zx % y 的结果,计算为 x – n * y,其中 n 是小于或等于 x / y的最大可能整数。 计算余数的方法类似于用于整数操作数的方法,但不同于 IEC 60559 定义(其中 n 是最接近 x / y的整数)。

    +y -y +0 -0 +∞ -∞ 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
    +∞ NaN NaN NaN NaN NaN NaN NaN
    -∞ NaN NaN NaN NaN NaN NaN NaN
    NaN NaN NaN NaN NaN NaN NaN NaN
  • 十进制余数:

    decimal operator %(decimal x, decimal y);
    

    如果右操作数的值为零,则会引发 System.DivideByZeroException。 当 System.ArithmeticException(或其子类)被引发时,它是由实现定义的。 如果 x / y 不引发异常,则一致实现不应引发 x % y 的异常。 在四舍五入之前,结果的精度是两个操作数精度中较大的一个,如果结果的符号不为零,则与 x 的符号相同。

    十进制余数等效于使用类型为 System.Decimal的余数运算符。

    注意:这些规则确保所有类型的结果永远不会与左操作数的符号相反。 尾注

还预定义了上面定义的未提升预定义余数运算符的提升 (§12.4.8) 形式。

12.10.5 加法运算符

对于表单 x + y的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

下面列出了预定义的加法运算符。 对于数值和枚举类型,预定义加法运算符计算两个操作数的总和。 当一个或两个操作数的类型为 string时,预定义的加法运算符将连接操作数的字符串表示形式。

  • 整数加法:

    int operator +(int x, int y);
    uint operator +(uint x, uint y);
    long operator +(long x, long y);
    ulong operator +(ulong x, ulong y
    

    checked 上下文中,如果总和超出结果类型的范围,则会抛出 System.OverflowException。 在 unchecked 上下文中,不会报告溢出,并且会丢弃结果类型范围之外的任何重要高阶位。

  • 浮点加法:

    float operator +(float x, float y);
    double operator +(double x, double y);
    

    根据 IEC 60559 算术规则计算总和。 下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,xy 是非零有限值,zx + y的结果。 如果 xy 具有相同的震级但相反的迹象,则 z 为正零。 如果 x + y 太大而不能用目标类型表示,则 z 是一个无穷大,其符号与 x + y相同。

    y +0 -0 +∞ -∞ NaN
    x z x x +∞ -∞ NaN
    +0 y +0 +0 +∞ –∞ NaN
    -0 y +0 -0 +∞ -∞ NaN
    +∞ +∞ +∞ +∞ +∞ NaN NaN
    -∞ -∞ -∞ -∞ NaN -∞ NaN
    NaN NaN NaN NaN NaN NaN NaN
  • 小数加法:

    decimal operator +(decimal x, decimal y);
    

    如果生成的值的大小太大而无法以小数格式表示,则会引发 System.OverflowException。 在四舍五入之前,结果的精度是两个操作数的精度较大者。

    小数加法等效于使用类型为 System.Decimal的加法运算符。

  • 枚举加法。 每个枚举类型都隐式提供以下预定义运算符,其中 E 为枚举类型,UE的基础类型:

    E operator +(E x, U y);
    E operator +(U x, E y);
    

    在运行时,这些运算符的运算方式与 (E)((U)x + (U)y 完全相同)。

  • 字符串串联:

    string operator +(string x, string y);
    string operator +(string x, object y);
    string operator +(object x, string y);
    

    二进制 + 运算符的这些重载执行字符串串联。 如果字符串串连的一个操作数是 null,则会替换为空字符串。 否则,任何非string 操作数都会通过调用从类型 object继承的虚拟 ToString 方法转换为其字符串表示形式。 如果 ToString 返回 null,则替换空字符串。

    示例

    class Test
    {
        static void Main()
        {
            string s = null;
            Console.WriteLine("s = >" + s + "<");  // Displays s = ><
    
            int i = 1;
            Console.WriteLine("i = " + i);         // Displays i = 1
    
            float f = 1.2300E+15F;
            Console.WriteLine("f = " + f);         // Displays f = 1.23E+15
    
            decimal d = 2.900m;
            Console.WriteLine("d = " + d);         // Displays d = 2.900
       }
    }
    

    注释中显示的输出是 US-English 系统上的典型结果。 精确的输出可能取决于执行环境的区域设置。 字符串串联运算符本身在每种情况下的行为方式相同,但在执行期间隐式调用的 ToString 方法可能会受到区域设置的影响。

    结束示例

    字符串串连运算符的结果是一个 string,由左操作数的字符和右操作数的字符组成。 字符串串联运算符永远不会返回 null 值。 如果没有足够的内存可用来分配生成的字符串,可能会抛出 System.OutOfMemoryException

  • 委托组合。 每个委托类型都隐式提供以下预定义运算符,其中 D 是委托类型:

    D operator +(D x, D y);
    

    如果第一个操作数是 null,则操作的结果是第二个操作数的值(即使这也是 null)。 否则,如果第二个操作数 null,则该操作的结果为第一个操作数的值。 否则,操作的结果是一个新的委托实例,其调用列表是由第一个操作数的调用列表中的元素开始,接着是第二个操作数的调用列表中的元素。 也就是说,结果委托的调用列表是两个操作数的调用列表的串连。

    注意:有关委托组合的示例,请参阅 §12.10.6§20.6。 由于 System.Delegate 不是委托类型,因此未为其定义运算符 +。尾注

还预定义了上面定义的未提升预定义加法运算符的提升 (§12.4.8) 形式。

12.10.6 减法运算符

对于表单 x – y的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

下面列出了预定义减法运算符。 运算符都是从 x 中减去 y

  • 整数减法:

    int operator –(int x, int y);
    uint operator –(uint x, uint y);
    long operator –(long x, long y);
    ulong operator –(ulong x, ulong y
    

    checked 上下文中,如果差超出结果类型的范围,就会引发 System.OverflowException。 在 unchecked 上下文中,不会报告溢出,并且会丢弃结果类型范围之外的任何重要高阶位。

  • 浮点减法:

    float operator –(float x, float y);
    double operator –(double x, double y);
    

    根据 IEC 60559 算术规则计算差异。 下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,xy 是非零有限值,zx – y的结果。 如果 xy 相等,则 z 为正零。 如果 x – y 大到无法在目标类型中表示,则 z 是一个无穷大,并且其符号与 x – y相同。

    y +0 -0 +∞ -∞ NaN
    x z x x -∞ +∞ NaN
    +0 -y +0 +0 -∞ +∞ NaN
    -0 -y -0 +0 -∞ +∞ NaN
    +∞ +∞ +∞ +∞ NaN +∞ NaN
    -∞ -∞ -∞ -∞ -∞ NaN NaN
    NaN NaN NaN NaN NaN NaN NaN

    (在上表中,-y 项表示 y相反数,而并非值为负。)

  • 十进制减法:

    decimal operator –(decimal x, decimal y);
    

    如果生成的值的大小太大而无法以小数格式表示,则会引发 System.OverflowException。 在四舍五入之前,结果的精度是两个操作数的精度较大者。

    小数减法等效于使用 System.Decimal类型的减法运算符。

  • 枚举减法。 每个枚举类型都隐式提供以下预定义运算符,其中 E 为枚举类型,UE的基础类型:

    U operator –(E x, E y);
    

    此运算符的计算方式与 (U)((U)x – (U)y)完全相同。 换句话说,运算符计算 xy的序号值之间的差异,结果的类型是枚举的基础类型。

    E operator –(E x, U y);
    

    此运算符的计算方式与 (E)((U)x – y)完全相同。 换句话说,运算符从枚举的基础类型中减去一个值,从而生成枚举的值。

  • 委托删除。 每个委托类型都隐式提供以下预定义运算符,其中 D 是委托类型:

    D operator –(D x, D y);
    

    语义如下:

    • 如果第一个操作数是 null,则操作的结果是 null
    • 否则,如果第二个操作数 null,则该操作的结果为第一个操作数的值。
    • 否则,两个操作数都代表非空调用列表 (§20.2)。
      • 如果根据委托相等运算符 (§12.12.9) 确定列表比较相等,则运算结果为 null
      • 否则,该操作的结果是一个新的调用列表,其中包含第一个操作数的列表,其中删除了第二个操作数的条目,前提是第二个操作数的列表是第一个操作数的子列表。 (要确定子列表是否相等,需要像委托相等运算符一样比较相应的条目。)如果第二个操作数的列表与第一个操作数列表中多个连续条目的子列表匹配,则删除最后一个匹配的连续条目子列表。
      • 否则,操作结果就是左操作数的值。

    在此过程中,操作数的列表(如有)都不会改变。

    示例

    delegate void D(int x);
    
    class C
    {
        public static void M1(int i) { ... }
        public static void M2(int i) { ... }
    }
    
    class Test
    {
        static void Main()
        {
            D cd1 = new D(C.M1);
            D cd2 = new D(C.M2);
            D list = null;
    
            list = null - cd1;                             // null
            list = (cd1 + cd2 + cd2 + cd1) - null;         // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - cd1;          // M1 + M2 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2);  // M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd2);  // M1 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd2 + cd1);  // M1 + M2
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd1);  // M1 + M2 + M2 + M1
            list = (cd1 + cd2 + cd2 + cd1) - (cd1 + cd2 + cd2 + cd1);  // null
        }
    }
    

    结束示例

还预定义了上面定义的未提升预定义减法运算符的提升 (§12.4.8) 形式。

12.11 移位运算符

<<>> 运算符用于执行位移运算。

shift_expression
    : additive_expression
    | shift_expression '<<' additive_expression
    | shift_expression right_shift additive_expression
    ;

如果 shift_expression 的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型是 dynamic,下面描述的解析将在运行时使用具有编译时类型 dynamic 的操作数的运行时类型。

对于表单 x << countx >> count的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

声明重载移位运算符时,第一个操作数的类型应始终为包含运算符声明的类或结构,第二个操作数的类型应始终 int

下面列出了预定义的移位运算符。

  • 左移:

    int operator <<(int x, int count);
    uint operator <<(uint x, int count);
    long operator <<(long x, int count);
    ulong operator <<(ulong x, int count);
    

    <<运算符x将按照下面描述的方式左移若干位。

    结果类型 x 范围之外的高阶位会被丢弃,其余位左移,低阶空位置 0。

  • 右移:

    int operator >>(int x, int count);
    uint operator >>(uint x, int count);
    long operator >>(long x, int count);
    ulong operator >>(ulong x, int count);
    

    >>运算符x将按照下面描述的方式右移若干位。

    x 的类型为 intlong时,将丢弃 x 的低序位,其余位向右移动,如果 x 为非负位,则高阶空位位置设置为零,如果 x 为负位,则设置为 1。

    x 的类型为 uintulong 时,x 的低阶位会被丢弃,其余位被右移,高阶空位位置被置零。

对于预定义的运算符,要移动的位数计算如下:

  • x 的类型为 intuint时,移位计数由 count的低序五位决定。 换句话说,移位计数是从 count & 0x1F计算得出的。
  • x 的类型为 longulong时,移位计数由 count的低序六位决定。 换句话说,移位计数是从 count & 0x3F计算得出的。

如果生成的移位计数为零,则移位运算符仅返回 x的值。

移位运算绝对不会导致溢出并在选中和未选中上下文中产生相同结果。

>> 运算符的左操作数是带符号整型时,运算符会执行算术右移,其中操作数最有效位(符号位)的值会传播到高阶空位位置。 当 >> 运算符的左操作数为无符号整型时,该运算符将执行逻辑右移,其中高阶空位位置始终置零。 要执行与操作数类型相反的操作,可以使用显式强制转换。

示例:如果 xint类型的变量,则操作 unchecked ((int)((uint)x >> y)) 执行 x的逻辑移位。 结束示例

还预定义了上面定义的未提升预定义移位运算符的提升 (§12.4.8) 形式。

12.12 关系运算符和类型测试运算符

12.12.1 常规

==!=<><=>=isas 运算符称为关系运算符和类型测试运算符。

relational_expression
    : shift_expression
    | relational_expression '<' shift_expression
    | relational_expression '>' shift_expression
    | relational_expression '<=' shift_expression
    | relational_expression '>=' shift_expression
    | relational_expression 'is' type
    | relational_expression 'is' pattern
    | relational_expression 'as' type
    ;

equality_expression
    : relational_expression
    | equality_expression '==' relational_expression
    | equality_expression '!=' relational_expression
    ;

注意:查找 is 运算符的右操作数必须首先作为一种类型进行测试,然后作为可能跨越多个符号的 expression 进行测试。 在操作数为 expreesion 的情况下,模式表达式的优先级必须至少与 shift_expression 相同。 尾注

is 运算符在 §12.12.12 中介绍,as 运算符在 §12.12.13中介绍。

==!=<><=>= 运算符是比较运算符

如果将 default_literal§12.8.21)用作 <><=>= 运算符的操作数,则会发生编译时错误。 如果 default_literal 被用作 ==!= 运算符的两个操作数,则会出现编译时错误。 如果将 default_literal 用作 isas 运算符的左操作数,则会发生编译时错误。

如果比较运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型为 dynamic,下面所述的解决方法将在运行时使用那些编译时类型为 dynamic的操作数的运行时类型。

对于形式为 x «op» y的操作,其中 «op» 是一个比较运算符,将应用重载解析(§12.4.5)来选择一个特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 如果 equality_expression 的两个操作数都是 null 字面量,则不执行重载决策,并且表达式根据运算符是 == 还是 != 来对常量值 truefalse 进行求值。

以下子项描述了预定义的比较运算符。 所有预定义的比较运算符都返回布尔类型的结果,如下表所述。

运算 结果
x == y 如果 x 等于 y,则为 true,否则 false
x != y 如果 x 不等于 y,则 true;否则 false
x < y 如果 x 小于 y,则为 true,否则为 false
x > y 如果 x 大于 y,则 true 否则为 false
x <= y 如果 x 小于或等于 ytrue;否则 false
x >= y 如果 x 大于或等于 y,返回 true;否则返回 false

12.12.2 整数比较运算符

预定义的整数比较运算符为:

bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);

bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);

bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);

bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);

bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);

bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);

其中每个运算符比较两个整数操作数的数值,并返回一个 bool 值,该值指示特定关系是 true 还是 false

还预定义了上面定义的未提升预定义整数比较运算符的提升 (§12.4.8) 形式。

12.12.3 浮点比较运算符

预定义的浮点比较运算符为:

bool operator ==(float x, float y);
bool operator ==(double x, double y);

bool operator !=(float x, float y);
bool operator !=(double x, double y);

bool operator <(float x, float y);
bool operator <(double x, double y);

bool operator >(float x, float y);
bool operator >(double x, double y);

bool operator <=(float x, float y);
bool operator <=(double x, double y);

bool operator >=(float x, float y);
bool operator >=(double x, double y);

运算符根据 IEC 60559 标准的规则对操作数进行比较:

如果任一操作数为 NaN,对于除 !=之外的所有运算符,结果是 false,而对于 !=,结果是 true。 对于任何两个操作数,x != y 始终生成与 !(x == y)相同的结果。 但是,当一个操作数或两个操作数都是 NaN 时,<><=>= 运算符不会生成与相反运算符的逻辑否定相同的结果。

示例:如果任一 xy 为 NaN,则 x < yfalse,但 !(x >= y)true结束示例

当两个操作数都不为 NaN 时,运算符将两个浮点操作数的值与排序进行比较

–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞

其中,minmax 是可以采用给定浮点格式表示的最小和最大的正有限值。 此排序的显著影响包括:

  • 负零和正零被视为相等。
  • 负无穷大被视为小于所有其他值,但等于另一个负无穷大。
  • 正无穷大被视为大于所有其他值,但等于另一个正无穷大。

还预定义了上面定义的未提升预定义浮点比较运算符的提升 (§12.4.8) 形式。

12.12.4 十进制比较运算符

预定义的小数比较运算符为:

bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);

每个运算符比较两个小数操作数的数值,并返回一个 bool 值,该值指示特定关系是 true 还是 false。 每个小数比较等效于使用 System.Decimal类型的相应关系运算符或相等运算符。

还预定义了上面定义的未提升预定义小数比较运算符的提升 (§12.4.8) 形式。

12.12.5 布尔相等运算符

预定义的布尔相等运算符为:

bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);

如果 xy 都是 true,或者 xy 都是 false,则 == 的结果是 true。 否则,结果为 false

如果 xy 都是 true,或者 xy 都是 false,则 != 的结果是 false。 否则,结果为 true。 当操作数的类型为 bool时,!= 运算符将生成与 ^ 运算符相同的结果。

还预定义了上面定义的未提升预定义布尔值相等运算符的提升 (§12.4.8) 形式。

12.12.6 枚举比较运算符

每个枚举类型都隐式提供以下预定义的比较运算符

bool operator ==(E x, E y);
bool operator !=(E x, E y);

bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);

评估 x «op» y的结果,其中 x 和 y 是具有基础类型 U的枚举类型 E 的表达式,并且 «op» 是比较运算符之一,其结果与评估 ((U)x) «op» ((U)y)完全相同。 换句话说,枚举类型比较运算符只是比较两个操作数的基础整数值。

还预定义了上面定义的未提升预定义枚举比较运算符的提升 (§12.4.8) 形式。

12.12.7 引用类型相等运算符

每个类类型 C 隐式提供以下预定义的引用类型相等运算符:

bool operator ==(C x, C y);
bool operator !=(C x, C y);

除非 C 存在预定义的相等运算符(例如,当 CstringSystem.Delegate时)。

运算符返回比较两个引用的相等性或非相等性的结果。 operator == 仅当 xy 引用同一实例或两者都是 null时返回 true,而 operator != 仅当具有相同操作数的 operator == 返回 false时返回 true

除了正常的适用性规则(§12.6.4.2),预定义的引用类型相等运算符还需要下列之一才能适用:

  • 两个操作数都是已知类型为 reference_type 或字面量 null 的值。 此外,从任一操作数到另一操作数的类型之间存在着标识转换或显式引用转换 (§10.3.5)。
  • 一个操作数是文本 null,另一个操作数是类型 T 的值,其中 T 是一个未知值类型的 type_parameter,并且没有值类型约束。
    • 如果在运行时 T 是不可为 null 的值类型,则 == 的结果是 false,且 != 的结果是 true
    • 如果运行时 T 为可以为 null 的值类型,则结果根据操作数的 HasValue 属性计算,如 (§12.12.10中所述)。
    • 如果在运行时 T 是引用类型,并且操作数是 null,则结果为 true;否则结果为 false

除非其中一个条件为 true,否则会出现绑定时错误。

注意:这些规则的显著影响包括:

  • 使用预定义的引用类型相等运算符来比较两个在绑定时已知不同的引用是一个绑定时错误。 例如,如果操作数的绑定时类型是两个类类型,如果两个操作数都不派生自另一个类类型,则两个操作数不可能引用同一对象。 因此,该操作被视为绑定时错误。
  • 预定义的引用类型相等运算符不允许对值类型操作数进行比较(类型参数与 null 比较时除外,这种情况会特殊处理)。
  • 预定义引用类型相等运算符的操作数从不会装箱。 执行这种装箱操作毫无意义,因为对新分配的装箱实例的引用必然与所有其他引用不同。

对于形式 x == yx != y的运算,如果存在任何适用的用户定义的 operator ==operator !=,运算符重载解析规则(§12.4.5)将选择该运算符,而不是使用预定义的引用类型相等运算符。 通过将一个或两个操作数显式强制转换为 object 类型,始终可以选择预定义的引用类型相等运算符。

尾注

示例:以下示例检查无约束类型参数的参数类型是否为 null

class C<T>
{
   void F(T x)
   {
      if (x == null)
      {
          throw new ArgumentNullException();
      }
      ...
   }
}

即使 T 可以表示一个不可为 null 的值类型,x == null 构造仍然是允许的,并且当 T 是不可为 null 的值类型时,结果被简单地定义为 false

结束示例

对于形式为 x == yx != y 的操作,如果存在任何适用的 operator ==operator !=,运算符重载决策 (§12.4.5) 规则将选择该运算符,而不是预定义的引用类型相等运算符。

注意:通过将两个操作数都显式强制转换为 object 类型,始终可以选择预定义的引用类型相等运算符。 尾注

示例:示例

class Test
{
    static void Main()
    {
        string s = "Test";
        string t = string.Copy(s);
        Console.WriteLine(s == t);
        Console.WriteLine((object)s == t);
        Console.WriteLine(s == (object)t);
        Console.WriteLine((object)s == (object)t);
    }
}

生成输出

True
False
False
False

st 变量引用两个包含相同字符的不同字符串实例。 由于两个操作数的类型都是 string,当选择预定义的字符串相等运算符(§12.12.8)时,第一次比较会输出 True。 其余的比较均输出 False,因为当操作数的绑定时间类型为 object 时,string 类型中 operator == 的重载不适用。

请注意,上述技术对值类型没有意义。 示例

class Test
{
    static void Main()
    {
        int i = 123;
        int j = 123;
        Console.WriteLine((object)i == (object)j);
    }
}

输出 False,因为强制转换创建了对装箱 int 值的两个单独实例的引用。

结束示例

12.12.8 字符串相等运算符

预定义的字符串相等运算符为:

bool operator ==(string x, string y);
bool operator !=(string x, string y);

当下列值之一为 true 时,两个 string 值被视为相等:

  • 两个值都是 null
  • 这两个值都是对字符串实例的非 null 引用,这些字符串实例的长度和每个字符位置上的字符完全相同。

字符串相等运算符比较字符串值,而不是字符串引用。 当两个单独的字符串实例包含完全相同的字符序列时,字符串的值相等,但引用不同。

注释:如 §12.12.7中所述,引用类型相等运算符可用于比较字符串引用而不是字符串值。 尾注

12.12.9 委托相等运算符

预定义的委托相等运算符包括:

bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);

两个委托实例被视为相等,如下所示:

  • 如果委托实例中的任何一个是 null,那么当且仅当它们都是 null 时,它们才相等。
  • 如果委托具有不同的运行时类型,则它们永远不会相等。
  • 如果两个委托实例都有调用列表(§20.2),则仅当它们的调用列表长度相同时,这些实例才相等,并且其中一个调用列表中的每个条目都按顺序等于另一个调用列表中的相应条目(如下所述)。

以下规则控制调用列表条目的相等性:

  • 如果两个调用列表条目都引用相同的静态方法,则条目相等。
  • 如果两个调用列表项都引用同一目标对象(由引用相等运算符定义)上的同一非静态方法,则这些条目是相等的。
  • 允许(但不要求)语义完全相同的匿名函数 (§12.19) 的调用列表条目与捕获的外部变量实例集相同(可能为空)。

如果运算符重载决策为任一委托相等运算符,并且两个操作数的绑定时间类型都是 §20 中描述的委托类型,而不是 System.Delegate,并且绑定类型操作数类型之间没有进行身份转换,则会发生绑定时错误。

注意:此规则可防止比较中由于引用了不同类型委托的实例而永远无法将非 null 值视为相等。 尾注

12.12.10 可以为 null 的值类型与 null 字面值之间的相等运算符

==!= 运算符允许一个操作数是可为 null 的值类型,另一个操作数是 null 字面量,即使该操作没有预定义或用户定义的运算符(未提升或已提升的形式)。

对于其中一种形式的运算

x == null    null == x    x != null    null != x

如果 x 是可为 null 的值类型的表达式,并且运算符重载分辨率(§12.4.5)找不到适用的运算符,则结果将从 xHasValue 属性中计算。 具体而言,前两种形式将转换为 !x.HasValue,最后两种形式将转换为 x.HasValue

12.12.11 元组相等运算符

元组相等运算符按词法顺序成对应用于元组操作数的元素。

如果 ==!= 运算符的每个操作数 xy 都被归类为元组或具有元组类型的值 (§8.3.11),那么该运算符就是一个元组相等运算符

如果操作数 e 分类为元组,则 e1...en 的元素应是计算元组表达式的元素表达式的结果。 否则,如果 e 是元组类型的值,则元素应为 t.Item1...t.Itemn,其中 t 是计算 e的结果。

元组相等运算符的操作数 xy 应具有相同的 arity,否则会出现编译时错误。 对于每一对元素 xiyi,应使用相同的相等运算符,并产生 booldynamic 类型的结果,或隐式转换为 bool 的类型,或定义了 truefalse 运算符的类型。

元组相等运算符 x == y 的求值过程如下:

  • 对左侧操作数 x 进行求值。
  • 对右侧操作数 y 进行求值。
  • 对于按词法顺序排列的每对元素 xiyi
    • 运算符 xi == yi 被求值后,会以如下方式得到 bool 类型的结果:
      • 如果比较得到了 bool,那么这就是结果。
      • 否则,如果比较结果为 dynamic,则会动态地调用运算符 false,而生成的 bool 值使用逻辑求反运算符(!)取反。
      • 否则,如果比较的类型可以隐式转换为 bool,则会应用该转换。
      • 否则,如果比较的类型具有运算符 false,则调用该运算符,并对生成的 bool 值应用逻辑求反运算符(!)。
    • 如果生成的 boolfalse,则不会再进行计算,元组相等运算符的结果是 false
  • 如果所有元素比较的结果都是 true,那么元组相等运算符的结果是 true

元组相等运算符 x != y 的计算方式如下:

  • 对左侧操作数 x 进行求值。
  • 对右侧操作数 y 进行求值。
  • 对于每对元素 xiyi 按词法顺序排列:
    • 运算符 xi != yi 被求值后,会以如下方式得到 bool 类型的结果:
      • 如果比较产生了 bool,那么这就是结果。
      • 否则,如果比较产生了 dynamic,则会动态调用运算符 true,生成的 bool 值为结果。
      • 否则,如果比较的类型可以隐式转换为 bool,则会应用这种转换。
      • 否则,如果比较的类型具有运算符 true,则会调用该运算符,并且生成的 bool 值为结果。
    • 如果生成的 booltrue,那么不会再进行进一步的评估,元组相等运算符的结果为 true
  • 如果所有元素的比较结果都是 false,那么元组相等运算符的结果是 false

12.12.12 is 运算符

is 运算符有两种形式。 一种是 is-type 运算符,它的右侧有一个类型。 另一种是 is-pattern 运算符,它的右侧有一个模式。

12.12.12.1 is-type 运算符

is-type 运算符 用于检查对象的运行时类型是否与给定类型兼容。 检查在运行时进行。 操作 E is T的结果,其中 E 是表达式,而 T 是一种不同于 dynamic的类型,返回一个布尔值,表示 E 是否为非空,并且可以通过引用转换、装箱转换、取消装箱转换、包装转换或取消包装转换成功转换为类型 T

此运算的计算方式如下:

  1. 如果 E 是匿名函数或方法组,则会发生编译时错误
  2. 如果 Enull 字面值,或者 E 的值是 null,则结果为 false
  3. 否则:
  4. RE的运行时类型。
  5. D 派生自 R,如下所示:
  6. 如果 R 为可以为 null 的值类型,则 DR的基础类型。
  7. 否则,DR
  8. 结果取决于 DT,如下所示:
  9. 如果 T 是引用类型,则在以下情况下结果为 true
    • DT 之间存在标识转换,
    • D 是引用类型,并且存在从 DT 的隐式引用转换,或者
    • 任一:D 是一种值类型,并且存在从 DT 的装箱转换。
      或者:D 是一种值类型,T 是由 D实现的接口类型。
  10. 如果 T 是可为 null 的值类型,并且 DT的基础类型,则结果为 true
  11. 如果 T 是不可为 null 的值类型,则如果 DT 的类型相同,则结果 true
  12. 否则,结果为 false

is 运算符不考虑用户定义的转换。

注意:由于 is 运算符是在运行时求值的,所有类型参数都已被替换,因此没有开放类型 (§8.4.3) 需要考虑。 尾注

注意is 运算符在编译时类型和转换方面可以理解,如下所示,其中 CE的编译时类型:

  • 如果 e 的编译时类型与 T相同,或者存在从 E 的编译时类型到 T的隐式引用转换(§10.2.8)、装箱转换(§10.2.9)、包装转换(§10.6),或显式解包转换(§10.6):
    • 如果 C 为不可为 null 的值类型,则操作的结果是 true
    • 否则,该操作的结果等效于计算 E != null
  • 否则,如果存在从 CT的显式引用转换(§10.3.5)或取消装箱转换(§10.3.7),或者如果 CT 是开放类型(§8.4.3),则应执行如前所述的运行时检查。
  • 否则,无法将 E 引用、装箱、包装或解包转换为类型 T,操作结果为 false。 编译器可以根据编译时类型进行优化。

尾注

12.12.12.2 is-pattern 运算符

is-pattern 运算符 用于检查表达式 计算的值是否与给定模式 匹配(§11)。 检查在程序运行时执行。 如果值与模式匹配,则 is-pattern 运算符的结果为 true;否则为 false。

对于形式为 E is P的表达式,其中 E 是类型为 T 的关系表达式,P 是一种模式,如果存在以下任一情况,则为编译时错误:

  • E 不指定值或没有类型。
  • 模式 P 不适用于类型 T§11.2)。

12.12.13 as 运算符

as 运算符用于将值显式转换为给定的引用类型或可空值类型。 与强制转换表达式 (§12.9.7) 不同,as 运算符不会引发异常。 如果无法进行指定的转换,生成的值为 null

在表单 E as T的操作中,E 应为表达式,T 应为引用类型、已知为引用类型的类型参数或可为 null 的值类型。 此外,至少应为下列其中一个,否则会发生编译时错误:

如果 E 的编译时类型不是 dynamic,则运算 E as T 产生的结果与以下相同

E is T ? (T)(E) : (T)null

不同的是 E 只计算一次。 编译器可以优化 E as T,以最多只需进行一次运行时类型检查,而不是如上所述扩展中隐含的两次运行时类型检查。

如果 E 的编译时类型是 dynamic,与强制转换运算符不同,as 运算符不会动态绑定 (§12.3.3)。 因此,本例中的扩展为:

E is T ? (T)(object)(E) : (T)null

请注意,某些转换(如用户定义的转换)无法使用 as 运算符,而应使用强制转换表达式。

示例:在示例中

class X
{
    public string F(object o)
    {
        return o as string;  // OK, string is a reference type
    }

    public T G<T>(object o)
        where T : Attribute
    {
        return o as T;       // Ok, T has a class constraint
    }

    public U H<U>(object o)
    {
        return o as U;       // Error, U is unconstrained
    }
}

已知 G 的类型参数 T 是引用类型,因为它具有类约束。 但是,H 的类型参数 U 不是;因此不允许在 H 中使用 as 运算符。

结束示例

12.13 逻辑运算符

12.13.1 概述

&,^| 运算符称为逻辑运算符。

and_expression
    : equality_expression
    | and_expression '&' equality_expression
    ;

exclusive_or_expression
    : and_expression
    | exclusive_or_expression '^' and_expression
    ;

inclusive_or_expression
    : exclusive_or_expression
    | inclusive_or_expression '|' exclusive_or_expression
    ;

如果逻辑运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型为 dynamic,下面所述的解决方法将在运行时根据那些具有编译时类型为 dynamic的操作数的运行时类型进行。

对于表单 x «op» y的操作,其中 «op» 是逻辑运算符之一,重载解析(§12.4.5) 将应用于选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。

以下子项描述了预定义的逻辑运算符。

12.13.2 整数逻辑运算符

预定义的整数逻辑运算符为:

int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);

int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);

int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);

& 运算符计算两个操作数的位逻辑 AND,| 运算符计算两个操作数的位逻辑 OR,^ 运算符计算两个操作数的按位逻辑排他 OR。 这些操作不可能产生溢出。

还预定义了上面定义的未提升预定义整数逻辑运算符的提升 (§12.4.8) 形式。

12.13.3 枚举逻辑运算符

每个枚举类型 E 隐式提供以下预定义的逻辑运算符:

E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);

计算 x «op» y的结果,其中 xy 是枚举类型 E,其基础类型为 U,并且 «op» 是逻辑运算符之一,其计算结果与 (E)((U)x «op» (U)y)完全相同。 换句话说,枚举类型逻辑运算符只需对两个操作数的基础类型执行逻辑操作。

还预定义了上面定义的未提升预定义枚举逻辑运算符的提升 (§12.4.8) 形式。

12.13.4 布尔逻辑运算符

预定义的布尔逻辑运算符为:

bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);

如果 xy 都为 true,则 x & y 的结果为 true。 否则,结果为 false

如果 xytrue,则 x | y 的结果是 true。 否则,结果为 false

如果 xtrueyfalse,或者 xfalseytrue,那么 x ^ y 的结果是 true。 否则,结果为 false。 当操作数的类型为 bool时,^ 运算符将计算与 != 运算符相同的结果。

12.13.5 可为 Null 的布尔值 & 和 | 运算符

可以为 null 的布尔类型 bool? 可以表示三个值,truefalsenull

与其他二进制运算符一样,逻辑运算符的提升形式 &|§12.13.4)也已预定义:

bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);

下表定义了提升 &| 运算符的语义:

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

注意bool? 类型在概念上类似于 SQL 中用于布尔表达式的三值类型。 上表遵循与 SQL 相同的语义,但将 §12.4.8 的规则应用于 &| 运算符则不符合。 §12.4.8 中的规则已经为提升的 ^ 运算符提供了类似 SQL 的语义。 尾注

12.14 条件逻辑运算符

12.14.1 常规

&&|| 运算符称为条件逻辑运算符。 它们也被称为“短路”逻辑运算符。

conditional_and_expression
    : inclusive_or_expression
    | conditional_and_expression '&&' inclusive_or_expression
    ;

conditional_or_expression
    : conditional_and_expression
    | conditional_or_expression '||' conditional_and_expression
    ;

&&|| 运算符是 &| 运算符的条件版本:

  • 运算 x && y 与运算 x & y 相对应,但 y 只有在 x 不是 false 的情况下才会被求值。
  • 运算 x || y 与运算 x | y 相对应,但 y 只有在 x 不是 true 的情况下才会被求值。

注意:短路使用“非真实”和“非假”条件的原因是为了使用户定义的条件运算符能够界定短路何时适用。 用户定义的类型可能处于 operator true 返回 falseoperator false 返回 false的状态。 在这些情况下,&&|| 都不会短路。 尾注

如果条件逻辑运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型是 dynamic,下面描述的解析将在运行时使用具有编译时类型 dynamic 的操作数的运行时类型。

通过应用重载分辨率(§12.4.5)来处理形式为 x && yx || y 的操作,就好像操作被写成 x & yx | y一样。 那么:

  • 如果重载解析找不到单个最佳运算符,或者如果重载解析选择预定义的整数逻辑运算符之一或可为 null 的布尔逻辑运算符(§12.13.5),则会发生绑定时错误。
  • 否则,如果所选运算符是预定义的布尔逻辑运算符之一(§12.13.4),则会按照 §12.14.2中所述处理该操作。
  • 否则,所选运算符是用户定义的运算符,并且按照 §12.14.3中所述处理该操作。

无法直接重载条件逻辑运算符。 但是,由于条件逻辑运算符根据常规逻辑运算符进行评估,因此常规逻辑运算符的重载具有某些限制,也被视为条件逻辑运算符的重载。 §12.14.3对此进行进一步介绍。

12.14.2 布尔条件逻辑运算符

如果 &&|| 的操作数类型是 bool,或者操作数的类型没有定义适用的 operator &operator |,但确实定义了到 bool的隐式转换,则对此操作按如下所示进行处理:

  • 运算 x && y 的计算结果为 x ? y : false。 换句话说,首先计算 x 并将其转换为类型 bool。 然后,如果 xtrue,则会计算 y 并将其转换为类型 bool,这将成为操作的结果。 否则,操作的结果为 false
  • 运算 x || y 的计算结果为 x ? true : y。 换句话说,首先计算 x 并将其转换为类型 bool。 然后,如果 xtrue,则操作的结果为 true。 否则,y 将被求值并转换为类型 bool,而它将成为运算的结果。

12.14.3 用户定义的条件逻辑运算符

&&|| 的操作数属于声明了适用的用户定义 operator &operator | 的类型时,以下两项均应为 true,其中 T 是声明所选操作数的类型:

  • 所选运算符的返回类型和每个参数的类型应为 T。 换言之,运算符应计算 T 类型的两个操作数的逻辑 AND 或逻辑 OR,并返回 T 类型的结果。
  • T 应包含 operator trueoperator false 的声明。

如果未满足上述任一要求,则会发生绑定时错误。 否则,对 &&|| 操作的计算是通过将用户定义的 operator trueoperator false 与所选的用户定义运算符结合来完成的。

  • 操作 x && y 计算为 T.false(x) ? x : T.&(x, y),其中 T.false(x) 调用在 T中声明的 operator falseT.&(x, y) 调用所选 operator &。 换言之,首先对 x 进行求值,然后在结果上调用 operator false 以确定 x 是否肯定为 false。 然后,如果 x 绝对为 false,则操作的结果是之前为 x计算的值。 否则,将对 y 进行求值,并在先前为 x 计算的值和为 y 计算的值上调用选定的 operator & 以产生运算结果。
  • 操作 x || y 计算为 T.true(x) ? x : T.|(x, y),其中 T.true(x) 调用在 T中声明的 operator trueT.|(x, y) 调用所选 operator |。 换言之,首先对 x 进行求值,然后在结果上调用 operator true 以确定 x 是否肯定为 true。 然后,如果 x 绝对正确,则操作的结果是之前为 x计算的值。 否则,将对 y 进行评估,然后使用之前为 x 计算的值和为 y 计算的值调用选定的 operator |,以产生操作的结果。

在这两种操作中,x 所给出的表达式只被求值一次,而 y 所给出的表达式要么不被求值,要么正好被求值一次。

12.15 空合并运算符

?? 运算符称为 null 合并运算符。

null_coalescing_expression
    : conditional_or_expression
    | conditional_or_expression '??' null_coalescing_expression
    | throw_expression
    ;

在表单 a ?? b的 null 合并表达式中,如果 a 为非null,则结果为 a;否则,结果为 b。 只有当 anull 时,运算才会对 b 求值。

null 合并运算符是右关联运算符,这意味着运算是从右向左分组的。

示例:形式为 a ?? b ?? c 的表达式会被作为一个 ?? (b ?? c) 进行求值。 一般情况下,E1 ?? E2 ?? ... ?? EN 形式的表达式返回非null操作数的第一个操作数,如果所有操作数都 null,则返回 null结束示例

表达式的类型 a ?? b 取决于操作数上可用的隐式转换。 根据首选项,a ?? b 的类型为 A₀AB,其中 Aa 类型(前提是 a 具有类型),Bb的类型(前提是 b 具有类型),A₀A 的基础类型(如果 A 为可为 null 的值类型,则为 A。 具体来说,a ?? b 的处理过程如下:

  • 如果 A 存在且不是可为 null 的值类型或引用类型,则会发生编译时错误。
  • 否则,如果 A 存在并且 b 是动态表达式,则结果类型 dynamic。 在运行时,首先对 a 进行求值。 如果 a 不等于 null,则将 a 转换为 dynamic,结果将是这个。 否则,将对 b 进行求值,并将其作为结果。
  • 否则,如果 A 存在并且是可为 null 的值类型,并且存在从 bA₀的隐式转换,则结果类型 A₀。 在运行时,首先对 a 进行求值。 如果 a 不是 null,则 a 将被解包为类型 A₀,并将其作为结果。 否则,b 将被求值并转换为类型 A₀,并将其作为结果。
  • 否则,如果存在 A,并且存在从 bA的隐式转换,则结果类型为 A。 在运行时,首先对 a 进行求值。 如果 a 不是 null,则 a 成为结果。 否则,b 将被求值并转换为类型 A,并将其作为结果。
  • 否则,如果 A 存在并且是可为 null 的值类型,则 b 具有类型 B,并且存在从 A₀B的隐式转换,则结果类型 B。 在运行时,首先对 a 进行求值。 如果 a 不是 null,那么 a 将解包至类型 A₀ 并转换为类型 B,这将成为结果。 否则,将对 b 求值,并将其作为结果。
  • 否则,如果 b 具有类型 B,并且存在从 aB的隐式转换,则结果类型为 B。 在运行时,首先对 a 进行求值。 如果 a 不是 null,那么 a 将被转换为类型 B,并成为结果。 否则,将对 b 求值,并将其作为结果。

否则,ab 不兼容,并且会发生编译时错误 a

12.16 throw 表达式运算符

throw_expression
    : 'throw' null_coalescing_expression
    ;

throw_expression 会抛出通过对 null_coalescing_expression 求值而得出的值。 表达式应隐式转换为 System.Exception,并且在抛出之前,表达式求值的结果将转换为 System.Exception。 运行时对 throw 表达式求值的行为与 throw 语句 (§13.10.6) 的行为相同。

throw_expression 没有类型。 throw_expression 可以通过隐式 throw 转换来转换为各种类型。

引发表达式 只能出现在以下语法上下文中:

  • 作为三元条件运算符 (?:) 的第二个或第三个操作数。
  • 作为 null 合并运算符 (??) 的第二个操作数。
  • 作为表达式的 lambda 或成员的主体。

12.17 声明表达式

声明表达式声明局部变量。

declaration_expression
    : local_variable_type identifier
    ;

local_variable_type
    : type
    | 'var'
    ;

如果简单名称查找找不到关联的声明(§12.8.4),则 simple_name_ 也被视为声明表达式。 作为声明表达式使用时,_ 被称为 simple discard。 它在语义上等效于 var _,但允许在更多地方使用。

声明表达式应仅在以下语法上下文中发生:

  • 作为 argument_list 中的 outargument_value
  • 作为一个构成简单赋值 (§12.21.2) 左侧的简单丢弃 _
  • 作为一个或多个递归嵌套 tuple_expression 中的 tuple_element,其中最外层包含一个解构赋值的左侧。 一个 deconstruction_expression 会在这个位置产生声明表达式,即使声明表达式在语法上并不存在。

注意:这意味着无法括号化声明表达式。 尾注

如果使用 declaration_expression 声明的隐式类型变量在其被声明的 argument_list 中被引用,则属于错误。

对于使用 declaration_expression 声明的变量,如果在出现该变量的解构赋值中被引用,则属于错误。

如果声明表达式是一个简单丢弃,或者 local_variable_type 是标识符,则 var 将被归类为 implicitly typed 变量。 表达式没有类型,根据语法上下文推断局部变量的类型,如下所示:

  • argument_list 中,变量的推断类型是其对应参数的声明类型。
  • 作为简单赋值的左侧,变量的推断类型就是赋值右侧的类型。
  • 在简单赋值左侧的 tuple_expression 中,变量的推断类型是赋值右侧(解构后)相应元组元素的类型。

否则,声明表达式被归类为 显式类型化 变量,表达式的类型以及声明的变量应由 local_variable_type指定。

带有标识符 _ 的声明表达式是一个丢弃式 (§9.2.9.2),并且不会引入变量的名称。 带有 _ 之外标识符的声明表达式会将该名称引入最近的外层局部变量声明空间 (§7.3)。

示例

string M(out int i, string s, out bool b) { ... }

var s1 = M(out int i1, "One", out var b1);
Console.WriteLine($"{i1}, {b1}, {s1}");
// Error: i2 referenced within declaring argument list
var s2 = M(out var i2, M(out i2, "Two", out bool b2), out b2);
var s3 = M(out int _, "Three", out var _);

s1 的声明显示了显式和隐式类型的声明表达式。 b1 的推断类型 bool,因为这是 M1中相应输出参数的类型。 随后的 WriteLine 能够访问 i1b1,它们已被引入到封闭范围中。

s2 的声明显示了在嵌套调用 M 时使用 i2 的尝试,这是不允许的,因为引用发生在声明 i2 的参数列表中。 另一方面,在最终参数中引用 b2 是允许的,因为它出现在嵌套参数列表的末尾,而 b2 是在该参数列表中声明的。

s3 的声明显示,隐式和显式类型的声明表达式都会被弃用。 由于丢弃不声明命名变量,因此允许多次出现标识符 _

(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);

本例展示了在解构赋值中对变量和弃码使用隐式和显式类型的声明表达式。 未找到 _ 声明时,simple_name_ 等效于 var _

void M1(out int i) { ... }

void M2(string _)
{
    M1(out _);      // Error: `_` is a string
    M1(out var _);
}

本示例展示了当 _ 不可用时,使用 var _ 提供隐式类型的丢弃,因为它指定了封闭范围中的一个变量。

结束示例

12.18 条件运算符

?: 运算符称为条件运算符。 有时也称为三元运算符。

conditional_expression
    : null_coalescing_expression
    | null_coalescing_expression '?' expression ':' expression
    | null_coalescing_expression '?' 'ref' variable_reference ':'
      'ref' variable_reference
    ;

如果存在 ref,则不允许在条件运算符中使用引发表达式(§12.16)。

条件表达式 b ? x : y 首先计算条件 b。 然后,如果 btrue,则 x 将被求值并成为运算结果。 否则,y 将被求值并成为运算结果。 条件表达式从不同时计算 xy

条件运算符是右关联运算符,这意味着运算是从右向左分组的。

示例:形式为 a ? b : c ? d : e 的表达式会被作为 a ? b : (c ? d : e) 进行求值。 结束示例

?: 运算符的第一个操作数应是可以隐式转换为 bool的表达式,或者是实现 operator true的类型表达式。 如果这两项要求均未满足,则会发生编译时错误。

如果 ref 存在:

  • 标识转换应存在于两个 variable_reference的类型之间,结果的类型可以是任一类型。 如果任一类型为 dynamic,则类型推理更喜欢 dynamic§8.7)。 如果任一类型是元组类型(§8.3.11),则当两个元组中具有相同序号位置的元素名称匹配时,类型推理将包括元素名称。
  • 结果是一个变量引用,如果两个 variable_reference 均为可写入,则该变量引用为可写入。

注释:当存在 ref 时,conditional_expression 将返回一个变量引用,该引用可以使用 = ref 运算符或作为引用/输入/输出参数传递给引用变量。 尾注

如果不存在 ref,则 ?: 运算符的第二个和第三个操作数(xy)控制条件表达式的类型:

  • 如果 x 具有类型 X,并且 y 具有类型 Y
    • 如果标识转换存在于 XY之间,则结果是一组表达式的最佳常见类型(§12.6.3.15)。 如果任一类型为 dynamic,则类型推理更喜欢 dynamic§8.7)。 如果任一类型是元组类型(§8.3.11),则当两个元组中具有相同序号位置的元素名称匹配时,类型推理将包括元素名称。
    • 否则,如果隐式转换(§10.2)从 XY存在,但不存在从 YX,则 Y 是条件表达式的类型。
    • 否则,如果隐式枚举转换(§10.2.4)从 XY存在,则 Y 是条件表达式的类型。
    • 否则,如果隐式枚举转换(§10.2.4)从 YX存在,则 X 是条件表达式的类型。
    • 否则,如果隐式转换(§10.2)从 YX存在,但不存在从 XY,则 X 是条件表达式的类型。
    • 否则,无法确定表达式类型,并且会发生编译时错误。
  • 如果 xy 中只有一个确实有类型,并且 xy 都可以隐式转换为该类型,则该条件表达式的类型为该类型。
  • 否则,无法确定表达式类型,并且会发生编译时错误。

形式为 b ? ref x : ref y 的 ref 条件表达式的运行时处理包括以下步骤:

  • 首先,计算 b,并确定 bbool 值:
    • 如果存在从 b 类型到 bool 的隐式转换,则执行此隐式转换以生成 bool 值。
    • 否则,将调用 b 类型定义的 operator true 以生成 bool 值。
  • 如果上述步骤生成的 bool 值为 true,那么将对 x 进行求值,由此产生的变量引用将成为条件表达式的结果。
  • 否则,将对 y 进行求值,由此产生的变量引用将成为条件表达式的结果。

表单 b ? x : y 的条件表达式的运行时处理包括以下步骤:

  • 首先,计算 b,并确定 bbool 值:
    • 如果存在从 b 类型到 bool 的隐式转换,则执行此隐式转换以生成 bool 值。
    • 否则,将调用 b 类型定义的 operator true 以生成 bool 值。
  • 如果上述步骤生成的 bool 值为 true,那么就评估 x 并将其转换为条件表达式的类型,并成为条件表达式的结果。
  • 否则,将对 y 进行求值并转换为条件表达式的类型,成为条件表达式的结果。

12.19 匿名函数表达式

12.19.1 一般规定

匿名函数 是表示“内联”方法定义的表达式。 匿名函数本身没有值或类型,但可转换为兼容的委托或表达式树类型。 匿名函数转换的求值取决于转换的目标类型:如果是委托类型,则转换的求值结果是引用匿名函数定义的方法的委托值。 如果它是表达式树类型,则转换结果将是一个表达式树,该树以对象结构的形式表示方法的结构。

注意:出于历史原因,匿名函数有两种语法风格,即 lambda_expressionanonymous_method_expression。 几乎在所有情况下,lambda_expression都比 anonymous_method_expression 更简洁、更具表现力,后者保留在语言中是为了向后兼容。 尾注

lambda_expression
    : 'async'? anonymous_function_signature '=>' anonymous_function_body
    ;

anonymous_method_expression
    : 'async'? 'delegate' explicit_anonymous_function_signature? block
    ;

anonymous_function_signature
    : explicit_anonymous_function_signature
    | implicit_anonymous_function_signature
    ;

explicit_anonymous_function_signature
    : '(' explicit_anonymous_function_parameter_list? ')'
    ;

explicit_anonymous_function_parameter_list
    : explicit_anonymous_function_parameter
      (',' explicit_anonymous_function_parameter)*
    ;

explicit_anonymous_function_parameter
    : anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'ref'
    | 'out'
    | 'in'
    ;

implicit_anonymous_function_signature
    : '(' implicit_anonymous_function_parameter_list? ')'
    | implicit_anonymous_function_parameter
    ;

implicit_anonymous_function_parameter_list
    : implicit_anonymous_function_parameter
      (',' implicit_anonymous_function_parameter)*
    ;

implicit_anonymous_function_parameter
    : identifier
    ;

anonymous_function_body
    : null_conditional_invocation_expression
    | expression
    | 'ref' variable_reference
    | block
    ;

在识别 anonymous_function_body 时,如果 null_conditional_invocation_expressionexpression 替代项均适用,则应选择前者。

注意:此处的替代项的重叠和优先级只是为了描述性便利;可以详细说明语法规则以删除重叠。 ANTLR 和其他语法系统采用相同的便利性,因此 anonymous_function_body 自动具有指定的语义。 尾注

注意:当作为 expression 处理时,如果 M 的结果类型是 void (§12.8.13),则 x?.M() 这样的语法形式将是错误的。 但是,当作为 null_conditional_invocation_expression 处理时,结果类型允许为 void尾注

示例List<T>.Reverse 的结果类型为 void。 在下面的代码中,匿名表达式的主体是 null_conditional_invocation_expression,因此它不是一个错误。

Action<List<int>> a = x => x?.Reverse();

结束示例

=> 运算符的优先级与赋值(=)相同,并且是右关联运算符。

具有 async 修饰符的匿名函数是异步函数,遵循 §15.15中所述的规则。

lambda_expression 形式出现的匿名函数的参数可以显式或隐式键入。 在显式类型化参数列表中,显式声明每个参数的类型。 在隐式类型化参数列表中,参数的类型是从匿名函数发生的上下文推断的,具体而言,当匿名函数转换为兼容的委托类型或表达式树类型时,该类型提供参数类型(§10.7)。

在具有单个隐式类型参数的 lambda_expression 中,可以从参数列表中省略括号。 换句话说,表单的匿名函数

( «param» ) => «expr»

可以缩写为

«param» => «expr»

匿名函数的参数列表在 anonymous_method_expression 形式中是可选的。 如果指定,则应显式键入参数。 否则,匿名函数可转换为一个委托,其参数列表不包含输出参数。

匿名函数的主体始终可访问 (§13.2)。

示例:下面是匿名函数的一些示例:

x => x + 1                             // Implicitly typed, expression body
x => { return x + 1; }                 // Implicitly typed, block body
(int x) => x + 1                       // Explicitly typed, expression body
(int x) => { return x + 1; }           // Explicitly typed, block body
(x, y) => x * y                        // Multiple parameters
() => Console.WriteLine()              // No parameters
async (t1,t2) => await t1 + await t2   // Async
delegate (int x) { return x + 1; }     // Anonymous method expression
delegate { return 1 + 1; }             // Parameter list omitted

结束示例

除以下几点外,lambda_expressionanonymous_method_expression 的行为相同:

  • anonymous_method_expression 允许完全省略参数列表,从而可以转换为任何值参数列表的委托类型。
  • lambda_expression允许省略和推断参数类型,而 anonymous_method_expression要求显式声明参数类型。
  • lambda_expression 的主体可以是表达式或块,而 anonymous_method_expression 的主体应为块。
  • 只有 lambda_expression 可以转换为兼容的表达式树类型 (§8.6)。

12.19.2 匿名函数签名

匿名函数的 anonymous_function_signature 定义了匿名函数的参数名称和可选的类型。 匿名函数的参数范围是 anonymous_function_body§7.7)。 匿名方法主体与参数列表(如果给定)一起构成声明空间(§7.3)。 因此,如果匿名函数的参数名称与其范围内包括 anonymous_method_expressionlambda_expression的局部变量、局部常量或参数的名称匹配,则会产生编译时错误。

如果匿名函数具有 explicit_anonymous_function_signature,那么兼容的委托类型和表达式树类型集将仅限于具有相同参数类型和相同顺序修饰符的类型 (§10.7)。 与方法组转换(§10.8)相比,不支持匿名函数参数类型的逆变。 如果匿名函数没有 anonymous_function_signature,那么兼容的委托类型和表达式树类型集将仅限于没有输出参数的类型。

请注意,anonymous_function_signature 不能包含属性或参数数组。 不过,anonymous_function_signature 可能与参数列表包含参数数组的委托类型兼容。

另请注意,即使兼容,转换到表达式树类型仍可能在编译时失败(§8.6)。

12.19.3 匿名函数主体

匿名函数的正文(表达式)受以下规则的约束:

  • 如果匿名函数包含签名,则签名中指定的参数在正文中可用。 如果匿名函数没有签名,则可以转换为具有参数的委托类型或表达式类型(§10.7),但不能在正文中访问参数。
  • 除了在最邻近的封闭匿名函数的签名(如有)中指定的按引用参数外,主体访问按引用参数属于编译时错误。
  • 除了在最近的封闭匿名函数的签名(如有)中指定的参数外,主体访问 ref struct 类型的参数是一个编译时错误。
  • this 的类型为结构类型时,主体访问 this 会导致编译时错误。 无论是显式访问(如 this.x 中)还是隐式访问(如 x 中,其中 x 是结构的实例成员)都是如此。 此规则只是禁止此类访问,并不影响成员查找是否会产生结构成员的结果。
  • 主体可以访问匿名函数的外部变量 (§12.19.6)。 对外部变量的访问将引用在 lambda_expressionanonymous_method_expression 被求值时处于活动状态的变量实例 (§12.19.7)。
  • 如果正文包含 goto 语句、break 语句或 continue 语句,而这些语句的目标在主体之外或包含在匿名函数的主体之内,则属于编译时错误。
  • 主体中的 return 语句从调用最近的封闭匿名函数返回控制,而不是从外层函数成员返回控制。

除了求值和调用 lambda_expressionanonymous_method_expression 之外,是否还有其他方法来执行匿名函数的块,这一点没有明确说明。 具体而言,编译器可以选择通过合成一个或多个命名方法或类型来实现匿名函数。 任何此类合成元素的名称应是保留供编译器使用的格式(§6.4.3)。

12.19.4 重载决策

参数列表中的匿名函数参与类型推理和重载解析。 有关确切规则,请参阅 §12.6.3§12.6.4

示例:以下示例说明了匿名函数对重载解析的影响。

class ItemList<T> : List<T>
{
    public int Sum(Func<T, int> selector)
    {
        int sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }

    public double Sum(Func<T, double> selector)
    {
        double sum = 0;
        foreach (T item in this)
        {
            sum += selector(item);
        }
        return sum;
    }
}

ItemList<T> 类有两个 Sum 方法。 每个函数都采用一个 selector 参数,该参数用于从列表项中提取需要求和的值。 提取的值可以是 intdouble,生成的总和同样是 intdouble

例如,Sum 方法可用于按顺序计算详细信息行列表中的总和。

class Detail
{
    public int UnitCount;
    public double UnitPrice;
    ...
}

class A
{
    void ComputeSums()
    {
        ItemList<Detail> orderDetails = GetOrderDetails( ... );
        int totalUnits = orderDetails.Sum(d => d.UnitCount);
        double orderTotal = orderDetails.Sum(d => d.UnitPrice * d.UnitCount);
        ...
    }

    ItemList<Detail> GetOrderDetails( ... )
    {
        ...
    }
}

在第一次调用 orderDetails.Sum时,这两个 Sum 方法都适用,因为匿名函数 d => d.UnitCountFunc<Detail,int>Func<Detail,double>兼容。 不过,重载决策会选择第一个 Sum 方法,因为转换为 Func<Detail,int> 比转换为 Func<Detail,double> 更好。

orderDetails.Sum的第二次调用中,只有第二个 Sum 方法适用,因为匿名函数 d => d.UnitPrice * d.UnitCount 生成 double类型的值。 因此,重载决策会为该调用选择第二个 Sum 方法。

结束示例

12.19.5 匿名函数和动态绑定

匿名函数不能是动态绑定操作的接收方、参数或操作数。

12.19.6 外部变量

12.19.6.1 常规

任何范围包括 lambda_expressionanonymous_method_expression 的局部变量、值参数或参数数组都被称为匿名函数的外侧变量。 在类的实例函数成员中,此值被视为值参数,并且是函数成员中包含的任何匿名函数的外部变量。

12.19.6.2 捕获的外部变量

当匿名函数引用外部变量时,据说外部变量已被匿名函数 捕获 。 通常,局部变量的生存期仅限于对其关联的块或语句的执行(§9.2.9.1)。 但是,被捕获的外部变量的生存期至少会延长到匿名函数创建的委托或表达式树符合垃圾回收条件为止。

示例:在示例中

delegate int D();

class Test
{
    static D F()
    {
        int x = 0;
        D result = () => ++x;
        return result;
    }

    static void Main()
    {
        D d = F();
        Console.WriteLine(d());
        Console.WriteLine(d());
        Console.WriteLine(d());
    }
}

局部变量 x 被匿名函数捕获,而 x 的生存期至少会被延长到 F 返回的委托符合垃圾回收条件为止。 由于匿名函数的每个调用都对 x的同一实例进行操作,因此示例的输出为:

1
2
3

结束示例

当匿名函数捕获局部变量或值参数时,局部变量或参数不再被视为固定变量(§23.4),而是被视为可移动变量。 但是,捕获的外部变量不能用于 fixed 语句(§23.7),因此无法获取捕获的外部变量的地址。

注意:与未捕获的变量不同,捕获的局部变量可以同时向多个执行线程公开。 尾注

12.19.6.3 局部变量的实例化

当执行进入局部变量的范围时,该变量会被视为已实例化

示例:例如,调用以下方法时,将实例化局部变量 x 并初始化三次,每次循环迭代一次。

static void F()
{
    for (int i = 0; i < 3; i++)
    {
        int x = i * 2 + 1;
        ...
    }
}

但是,将 x 的声明移到循环之外会导致 x 的单一实例化:

static void F()
{
    int x;
    for (int i = 0; i < 3; i++)
    {
        x = i * 2 + 1;
        ...
    }
}

结束示例

在没有捕获的情况下,无法准确观察局部变量的实例化频率 — 因为实例化的生存期并不相连,所以每个实例化都有可能使用相同的存储位置。 但是,当匿名函数捕获局部变量时,实例化的效果变得明显。

示例:示例

delegate void D();
class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            int x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
    }

    static void Main()
    {
        foreach (D d in F())
        {
            d();
        }
    }
}

生成输出:

1
3
5

但是,当 x 的声明被移到循环之外时:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        int x;
        for (int i = 0; i < 3; i++)
        {
            x = i * 2 + 1;
            result[i] = () => Console.WriteLine(x);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

输出为:

5
5
5

请注意,允许编译器(但不需要)将三个实例优化为单个委托实例(§10.7.2)。

结束示例

如果 for循环 声明了一个迭代变量,该变量本身被视为是在循环外部声明的。

示例:因此,如果示例被更改为捕获迭代变量本身:

delegate void D();

class Test
{
    static D[] F()
    {
        D[] result = new D[3];
        for (int i = 0; i < 3; i++)
        {
            result[i] = () => Console.WriteLine(i);
        }
        return result;
   }

   static void Main()
   {
       foreach (D d in F())
       {
           d();
       }
   }
}

只捕获迭代变量的一个实例,这会生成输出:

3
3
3

结束示例

匿名函数委托有可能共享某些捕获的变量,但对其他变量却各自有单独的实例。

示例:例如,如果将 F 更改为

static D[] F()
{
    D[] result = new D[3];
    int x = 0;
    for (int i = 0; i < 3; i++)
    {
        int y = 0;
        result[i] = () => Console.WriteLine($"{++x} {++y}");
    }
    return result;
}

这三个委托捕获了同一个 x 实例,但分别捕获了 y 实例,并且输出为:

1 1
2 1
3 1

结束示例

单独的匿名函数可以捕获外部变量的同一实例。

示例:在示例中:

delegate void Setter(int value);
delegate int Getter();

class Test
{
    static void Main()
    {
        int x = 0;
        Setter s = (int value) => x = value;
        Getter g = () => x;
        s(5);
        Console.WriteLine(g());
        s(10);
        Console.WriteLine(g());
    }
}

这两个匿名函数捕获本地变量 x的同一实例,因此可以通过该变量“通信”。 示例的输出为:

5
10

结束示例

12.19.7 匿名函数表达式的求值

匿名函数 F 应始终转换为委托类型 D 或表达式树类型 E,直接或通过执行委托创建表达式 new D(F)。 此转换确定匿名函数的结果,如 §10.7中所述。

12.19.8 实现示例

此子条款仅供参考。

此小节使用其他 C# 构造来描述匿名函数转换的可能实现。 此处所述的实现基于商业 C# 编译器使用的相同原则,但绝不是授权实现,也不是唯一可能的实现。 它只简要提到对表达式树的转换,因为他们的确切语义超出了此规范的范围。

此子引用的其余部分提供了几个包含具有不同特征的匿名函数的代码示例。 对于每个示例,都提供了与只使用其他 C# 结构的代码相对应的转换。 在以下示例中,标识符 D 被假定为代表如下的委托类型:

public delegate void D();

匿名函数的最简单形式是不捕获任何外部变量的函数。

delegate void D();

class Test
{
    static void F()
    {
        D d = () => Console.WriteLine("test");
    }
}

这可以转换为委托实例化,该实例化引用编译器生成的静态方法,在该方法中放置匿名函数的代码:

delegate void D();

class Test
{
    static void F()
    {
        D d = new D(__Method1);
    }

    static void __Method1()
    {
        Console.WriteLine("test");
    }
}

在以下示例中,匿名函数引用 this的实例成员:

delegate void D();

class Test
{
    int x;

    void F()
    {
        D d = () => Console.WriteLine(x);
    }
}

这可以转换为编译器生成的实例方法,其中包含匿名函数的代码:

delegate void D();

class Test
{
   int x;

   void F()
   {
       D d = new D(__Method1);
   }

   void __Method1()
   {
       Console.WriteLine(x);
   }
}

在此示例中,匿名函数捕获局部变量:

delegate void D();

class Test
{
    void F()
    {
        int y = 123;
        D d = () => Console.WriteLine(y);
    }
}

局部变量的生命周期现在必须至少扩展到匿名函数委托的生命周期。 这可以通过将局部变量“提升”到编译器生成的类的字段中来实现。 局部变量的实例化(§12.19.6.3),然后对应于创建编译器生成的类的实例,访问局部变量对应于访问编译器生成的类实例中的字段。 此外,匿名函数将成为编译器生成的类的实例方法:

delegate void D();

class Test
{
    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.y = 123;
        D d = new D(__locals1.__Method1);
    }

    class __Locals1
    {
        public int y;

        public void __Method1()
        {
            Console.WriteLine(y);
        }
    }
}

最后,以下匿名函数捕获 this,以及两个具有不同生存期的局部变量:

delegate void D();

class Test
{
   int x;

   void F()
   {
       int y = 123;
       for (int i = 0; i < 10; i++)
       {
           int z = i * 2;
           D d = () => Console.WriteLine(x + y + z);
       }
   }
}

在这里,会为每个捕获局部变量的块创建编译器生成的类,以便不同块中的局部变量可以具有独立的生存期。 编译器为内部块生成的类 __Locals2实例包含局部变量 z 和引用 __Locals1实例的字段。 编译器为外部块生成的类 __Locals1的实例包含局部变量 y 和引用封闭函数成员 this 的字段。 借助这些数据结构,可以通过 __Local2实例访问所有捕获的外部变量,因此匿名函数的代码可以作为该类的实例方法实现。

delegate void D();

class Test
{
    int x;

    void F()
    {
        __Locals1 __locals1 = new __Locals1();
        __locals1.__this = this;
        __locals1.y = 123;
        for (int i = 0; i < 10; i++)
        {
            __Locals2 __locals2 = new __Locals2();
            __locals2.__locals1 = __locals1;
            __locals2.z = i * 2;
            D d = new D(__locals2.__Method1);
        }
    }

    class __Locals1
    {
        public Test __this;
        public int y;
    }

    class __Locals2
    {
        public __Locals1 __locals1;
        public int z;

        public void __Method1()
        {
            Console.WriteLine(__locals1.__this.x + __locals1.y + z);
        }
    }
}

将匿名函数转换为表达式树时,也可以使用此处应用的相同方法来捕获局部变量:对编译器生成的对象的引用可以存储在表达式树中,对局部变量的访问可以表示为对这些对象的字段访问。 这种方法的优点是可以在委托和表达式树之间共享“提升”的局部变量。

信息性文本的结尾。

12.20 查询表达式

12.20.1 常规

查询表达式 为类似于 SQL 和 XQuery 等关系查询和分层查询语言的查询提供语言集成的语法。

query_expression
    : from_clause query_body
    ;

from_clause
    : 'from' type? identifier 'in' expression
    ;

query_body
    : query_body_clauses? select_or_group_clause query_continuation?
    ;

query_body_clauses
    : query_body_clause
    | query_body_clauses query_body_clause
    ;

query_body_clause
    : from_clause
    | let_clause
    | where_clause
    | join_clause
    | join_into_clause
    | orderby_clause
    ;

let_clause
    : 'let' identifier '=' expression
    ;

where_clause
    : 'where' boolean_expression
    ;

join_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression
    ;

join_into_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression 'into' identifier
    ;

orderby_clause
    : 'orderby' orderings
    ;

orderings
    : ordering (',' ordering)*
    ;

ordering
    : expression ordering_direction?
    ;

ordering_direction
    : 'ascending'
    | 'descending'
    ;

select_or_group_clause
    : select_clause
    | group_clause
    ;

select_clause
    : 'select' expression
    ;

group_clause
    : 'group' expression 'by' expression
    ;

query_continuation
    : 'into' identifier query_body
    ;

查询表达式以 from 子句开头,以 selectgroup 子句结尾。 初始 from 子句后面可以有零或多个 fromletwherejoinorderby 子句。 每个 from 子句都是一个生成器,它引入了一个 范围变量,该变量的范围位于 序列的元素上。 每个 let 子句都会引入一个范围变量,该变量表示由以前的范围变量计算的值。 每个 where 条件都是一个筛选器,用于从结果中排除项目。 每个 join 子句将源序列的指定键与其他序列的键进行比较,从而生成匹配对。 每个 orderby 子句根据指定的条件对项重新排序。最终 selectgroup 子句指定结果的形状(以范围变量为单位)。 最后,通过将一个查询的结果视为后续查询中的生成器,可以使用 into 子句来“连接”查询。

12.20.2 查询表达式中的歧义性

查询表达式使用许多上下文关键字(§6.4.4):ascendingbydescendingequalsfromgroupintojoinletonorderbyselectwhere

为了避免使用这些标识符作为关键字和简单名称而引起的歧义,这些标识符在查询表达式中的任何位置都被视为关键字,除非它们以“@”(§6.4.4)作为前缀,在这种情况下,这些标识符被视为标识符。 为此,查询表达式是以“from标识符”开头,并后接任何一个标记,但不包括“;”、“=”或“,”。

12.20.3 查询表达式转换

12.20.3.1 概述

C# 语言未指定查询表达式的执行语义。 相反,查询表达式会被转换为调用符合查询表达式语法的方法(§12.20.4)。 具体而言,查询表达式将转换为名为 WhereSelectSelectManyJoinGroupJoinOrderByOrderByDescendingThenByThenByDescendingGroupByCast的方法调用。 这些方法应具有特定的签名和返回类型,如 §12.20.4中所述。 这些方法可以是要查询的对象实例方法或对象外部的扩展方法。 这些方法实现查询的实际执行。

从查询表达式到方法调用的转换是在执行任何类型绑定或重载解析之前发生的语法映射。 查询表达式转换后,生成的方法调用将作为常规方法调用进行处理,这反过来可能会发现编译时错误。 这些错误条件包括但不限于不存在的方法、错误的类型的参数以及类型推理失败的泛型方法。

通过反复应用以下转换来处理查询表达式,直到没有进一步的简化可能为止。 翻译按应用顺序列出:每个部分假定前面的翻译经过充分执行,一旦执行完毕,后续处理同一查询表达式时将不再重复访问这些部分。

在编译时,如果查询表达式包含对范围变量的赋值,或将范围变量用作引用或输出参数,这将产生错误。

某些转换会注入带有透明标识符(以 * 表示)的范围变量。 §12.20.3.8进一步介绍了这些内容。

12.20.3.2 包含延续的查询表达式

带有查询主体后续部分的查询表达式

from «x1» in «e1» «b1» into «x2» «b2»

被转换为

from «x2» in ( from «x1» in «e1» «b1» ) «b2»

以下部分中的翻译假定查询没有延续。

示例:示例:

from c in customers
group c by c.Country into g
select new { Country = g.Key, CustCount = g.Count() }

被转换为:

from g in
   (from c in customers
   group c by c.Country)
select new { Country = g.Key, CustCount = g.Count() }

其最终翻译为:

customers.
GroupBy(c => c.Country).
Select(g => new { Country = g.Key, CustCount = g.Count() })

结束示例

12.20.3.3 显式范围变量类型

显式指定范围变量类型的 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»

以下部分中的翻译假定查询没有显式范围变量类型。

示例:示例

from Customer c in customers
where c.City == "London"
select c

被转换为

from c in (customers).Cast<Customer>()
where c.City == "London"
select c

其最终翻译为

customers.
Cast<Customer>().
Where(c => c.City == "London")

结束示例

注意:显式范围变量类型可用于查询实现非泛型 IEnumerable 接口的集合,但不适用于泛型 IEnumerable<T> 接口。 在上面的示例中,如果客户的类型为 ArrayList,则情况就是这样。 尾注

12.20.3.4 退化查询表达式

查询表达式为

from «x» in «e» select «x»

被转换为

( «e» ) . Select ( «x» => «x» )

示例:示例

from c in customers
select c

被转换为

(customers).Select(c => c)

结束示例

退化的查询表达式是一个简单选择源元素的表达式。

注意:翻译的后续阶段(§12.20.3.6§12.20.3.7)通过用其来源替换由其他翻译步骤引入的退化查询来去除这些查询。 但是,请务必确保查询表达式的结果绝不是源对象本身。 否则,返回此类查询的结果可能会无意中向调用方公开私有数据(例如元素数组)。 因此,此步骤通过在源代码中显式调用 Select 来保护那些直接编写在源代码中的退化查询。 这就取决于 Select 和其他查询运算符的实现者,确保这些方法绝不会返回源对象本身。 尾注

12.20.3.5 From、let、where、join 和 orderby 子句

包含第二个 from 子句并后跟一个 select 子句的查询表达式

from «x1» in «e1»  
from «x2» in «e2»  
select «v»

被转换为

( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )

示例:示例

from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }

被转换为

(customers).
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)

结束示例

带有第二个 from 子句的查询表达式,其后是包含一组非空查询主体子句的查询主体 Q

from «x1» in «e1»
from «x2» in «e2»
Q

被转换为

from * in («e1») . SelectMany( «x1» => «e2» ,
                              ( «x1» , «x2» ) => new { «x1» , «x2» } )
Q

示例:示例

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

被转换为

from * in (customers).
   SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.OrderID, o.Total }

其最终翻译为

customers.
SelectMany(c => c.Orders, (c,o) => new { c, o }).
OrderByDescending(x => x.o.Total).
Select(x => new { x.c.Name, x.o.OrderID, x.o.Total })

其中 x 是编译器生成的标识符,否则不可见且不可访问。

结束示例

一个 let 表达式及其前面的 from 子句:

from «x» in «e»  
let «y» = «f»  
...

被转换为

from * in ( «e» ) . Select ( «x» => new { «x» , «y» = «f» } )  
...

示例:示例

from o in orders
let t = o.Details.Sum(d => d.UnitPrice * d.Quantity)
where t >= 1000
select new { o.OrderID, Total = t }

被转换为

from * in (orders).Select(
    o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
where t >= 1000
select new { o.OrderID, Total = t }

其最终翻译为

orders
    .Select(o => new { o, t = o.Details.Sum(d => d.UnitPrice * d.Quantity) })
    .Where(x => x.t >= 1000)
    .Select(x => new { x.o.OrderID, Total = x.t })

其中 x 是编译器生成的标识符,否则不可见且不可访问。

结束示例

一个 where 表达式及其前面的 from 子句:

from «x» in «e»  
where «f»  
...

被转换为

from «x» in ( «e» ) . Where ( «x» => «f» )  
...

一个 join 子句后跟一个 select 子句

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
select «v»

被转换为

( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )

示例:示例

from c in customersh
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }

被转换为

(customers).Join(
   orders,
   c => c.CustomerID, o => o.CustomerID,
   (c, o) => new { c.Name, o.OrderDate, o.Total })

结束示例

一个 join 子句,后跟一个查询主体子句:

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2»  
...

被转换为

from * in ( «e1» ) . Join(  
«e2» , «x1» => «k1» , «x2» => «k2» ,
( «x1» , «x2» ) => new { «x1» , «x2» })  
...

一个 join-into 子句后跟一个 select 子句

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into «g»  
select «v»

被转换为

( «e1» ) . GroupJoin( «e2» , «x1» => «k1» , «x2» => «k2» ,
                     ( «x1» , «g» ) => «v» )

一个 join into 子句,后跟一个查询主体子句

from «x1» in «e1»  
join «x2» in «e2» on «k1» equals «k2» into *g»  
...

被转换为

from * in ( «e1» ) . GroupJoin(  
   «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «g» ) => new { «x1» , «g» })
...

示例:示例

from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

被转换为

from * in (customers).GroupJoin(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, co) => new { c, co })
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }

其最终翻译为

customers
    .GroupJoin(
        orders,
        c => c.CustomerID,
        o => o.CustomerID,
        (c, co) => new { c, co })
    .Select(x => new { x, n = x.co.Count() })
    .Where(y => y.n >= 10)
    .Select(y => new { y.x.c.Name, OrderCount = y.n })

其中,xy 是编译器生成的标识符,否则不可见且不可访问。

结束示例

一个 orderby 子句及其前面的 from 子句:

from «x» in «e»  
orderby «k1» , «k2» , ... , «kn»  
...

被转换为

from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...

如果 ordering 子句指定了降序方向指示符,则会改为生成 OrderByDescendingThenByDescending 调用。

示例:示例

from o in orders
orderby o.Customer.Name, o.Total descending
select o

最终的转换为

(orders)
    .OrderBy(o => o.Customer.Name)
    .ThenByDescending(o => o.Total)

结束示例

以下转换假定每个查询表达式中没有 letwherejoinorderby 子句,并且也没有多个初始 from 子句。

12.20.3.6 Select 子句

查询表达式为

from «x» in «e» select «v»

被转换为

( «e» ) . Select ( «x» => «v» )

除非 «v» 是标识符 «x»,翻译就很简单

( «e» )

示例:示例

from c in customers.Where(c => c.City == "London")
select c

会被直接转换为

(customers).Where(c => c.City == "London")

结束示例

12.20.3.7 Group 子句

一个 group 子句

from «x» in «e» group «v» by «k»

被转换为

( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )

除非当 «v» 是标识符 «x» 时,转换为

( «e» ) . GroupBy ( «x» => «k» )

示例:示例

from c in customers
group c.Name by c.Country

被转换为

(customers).GroupBy(c => c.Country, c => c.Name)

结束示例

12.20.3.8 透明标识符

某些转换会注入带有透明标识符(以 * 表示)的范围变量。 透明标识符仅作为查询表达式转换过程中的中间步骤存在。

当查询翻译注入透明标识符时,进一步的翻译步骤会将透明标识符传播到匿名函数和匿名对象初始值设定项。 在这些上下文中,透明标识符具有以下行为:

  • 当透明标识符作为匿名函数的参数出现时,关联的匿名类型的成员将在匿名函数的函数体范围内自动生效。
  • 当具有透明标识符的成员处于范围中时,该成员的成员也处于范围中。
  • 当透明标识符在匿名对象初始化器中用作成员声明符时,它会引入一个含有透明标识符的成员。

在上述翻译步骤中,透明标识符始终与匿名类型一起引入,目的是将多个范围变量捕获为单个对象的成员。 允许 C# 的某种实现使用不同于匿名类型的机制来集合多个范围变量。 以下翻译示例假定使用匿名类型,并展示透明标识符的其中一种可能翻译。

示例:示例

from c in customers
from o in c.Orders
orderby o.Total descending
select new { c.Name, o.Total }

被转换为

from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o })
orderby o.Total descending
select new { c.Name, o.Total }

进一步转换为

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(* => o.Total)
    .Select(\* => new { c.Name, o.Total })

在清除透明标识符时,等效于

customers
    .SelectMany(c => c.Orders, (c,o) => new { c, o })
    .OrderByDescending(x => x.o.Total)
    .Select(x => new { x.c.Name, x.o.Total })

其中 x 是编译器生成的标识符,否则不可见且不可访问。

示例

from c in customers
join o in orders on c.CustomerID equals o.CustomerID
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

被转换为

from * in (customers).Join(
    orders,
    c => c.CustomerID,
    o => o.CustomerID,
    (c, o) => new { c, o })
join d in details on o.OrderID equals d.OrderID
join p in products on d.ProductID equals p.ProductID
select new { c.Name, o.OrderDate, p.ProductName }

进一步简化为

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d })
    .Join(products, * => d.ProductID, p => p.ProductID,
        (*, p) => new { c.Name, o.OrderDate, p.ProductName })

其最终翻译为

customers
    .Join(orders, c => c.CustomerID,
        o => o.CustomerID, (c, o) => new { c, o })
    .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d })
    .Join(products, y => y.d.ProductID, p => p.ProductID,
        (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })

其中,xy 是编译器生成的标识符,否则不可见且不可访问。 结束示例

12.20.4 查询表达式模式

查询表达式模式建立了一种类型可以实现的方法模式,以支持查询表达式。

泛型类型 C<T> 支持查询表达式模式(如果其公共成员方法和可公开访问的扩展方法可以替换为以下类定义)。 成员和可访问的扩展方法被称为泛型 C<T> 的“形状”。 泛型类型用于说明参数和返回类型之间的适当关系,但也可以实现非泛型类型的模式。

delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);

class C
{
    public C<T> Cast<T>() { ... }
}

class C<T> : C
{
    public C<T> Where(Func<T,bool> predicate) { ... }
    public C<U> Select<U>(Func<T,U> selector) { ... }
    public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
        Func<T,U,V> resultSelector) { ... }
    public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
    public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
        Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
    public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
    public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
    public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
    public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
        Func<T,E> elementSelector) { ... }
}

class O<T> : C<T>
{
    public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
    public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}

class G<K,T> : C<T>
{
    public K Key { get; }
}

上述方法使用泛型委托类型 Func<T1, R>Func<T1, T2, R>,但它们同样可以使用在参数和返回类型中具有相同关系的其他委托或表达式树类型。

注意:建议使用 C<T>O<T> 之间的关系,以确保 ThenByThenByDescending 方法仅适用于 OrderByOrderByDescending 的结果。 尾注

注意:推荐的 GroupBy 结果的形态是一个序列的序列,其中每个内部序列都有一个额外的 Key 属性。 尾注

注意:由于查询表达式通过语法映射转换为方法调用,因此类型在实现任何或全部查询表达式模式的方式方面具有相当大的灵活性。 例如,模式的方法可以作为实例方法或扩展方法实现,因为两者具有相同的调用语法,并且方法可以请求委托或表达式树,因为匿名函数可转换为两者。 仅实现部分查询表达式模式的类型只支持映射到该类型支持的方法的查询表达式转换。 尾注

注释System.Linq 命名空间为实现 System.Collections.Generic.IEnumerable<T> 接口的任何类型提供查询表达式模式的实现。 尾注

12.21 赋值运算符

12.21.1 常规

除了一个赋值运算符,所有赋值运算符都会向变量、属性、事件或索引器元素赋值。 例外 = ref 将变量引用 (§9.5) 赋值给引用变量 (§9.7)。

assignment
    : unary_expression assignment_operator expression
    ;

assignment_operator
    : '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
    | right_shift_assignment
    ;

赋值的左操作数应是被归类为变量的表达式,或(= ref 除外)属性访问、索引器访问、事件访问或元组。 声明表达式不能直接用作左操作数,但可以作为解构赋值的求值步骤。

= 运算符称为 简单赋值运算符。 它将右操作数的值分配给左操作数指定的变量、属性、索引器元素或元组元素。 简单赋值运算符的左操作数不应为事件访问(除非在 §15.8.2中另有说明)。 简单赋值运算符在 §12.21.2中介绍。

运算符 = ref 被称为 ref 赋值运算符。 它使右操作数成为左操作数指定的引用变量的参照,而右操作数应为 variable_reference (§9.5)。 ref 赋值运算符在 §12.21.3中介绍。

== ref 运算符以外的赋值运算符称为 复合赋值运算符。 这些运算符对两个操作数执行指示的操作,然后将结果值分配给左操作数给定的变量、属性或索引器元素。 复合赋值运算符在 §12.21.4中介绍。

作为左操作数的事件访问表达式的 +=-= 运算符称为 事件赋值运算符。 以事件访问作为左操作数时,其他赋值运算符均无效。 事件赋值运算符在 §12.21.5中介绍。

赋值运算符是右关联运算符,这意味着运算是从右向左分组的。

示例:形式为 a = b = c 的表达式被评估为 a = (b = c)结束示例

12.21.2 简单赋值

= 运算符称为简单赋值运算符。

如果简单赋值的左操作数的形式是 E.PE[Ei],而 E 具有编译时类型 dynamic,那么该赋值将动态绑定(§12.3.3)。 在这种情况下,赋值表达式的编译时类型是 dynamic,下面描述的解析将在运行时根据 E的运行时类型进行。 如果左侧操作数的格式为 E[Ei],其中至少有一个 Ei 元素具有编译时类型 dynamic,并且 E 的编译时类型不是数组,则生成的索引器访问是动态绑定的,但具有有限的编译时检查(§12.6.5)。

左操作数被归类为元组的简单赋值也称为析构赋值。 如果左侧操作数的任何元组元素具有元素名称,则会发生编译时错误。 如果左操作数的任何元组元素是 declaration_expression 而任何其他元素不是 declaration_expression 或简单丢弃,则会出现编译时错误。

简单赋值 x = y 的类型就是 yx 赋值的类型,其递归确定方法如下:

  • 如果 x 是元组表达式 (x1, ..., xn),并且 y 能够被解构为包含 n 个元素的元组表达式 (y1, ..., yn)§12.7),且对 yi 的每个赋值具有类型 Ti,那么赋值给 xi 便具有类型 (T1, ..., Tn)
  • 否则,如果将 x 分类为变量,则变量不会 readonlyx 具有类型 T,并且 y 隐式转换为 T,则赋值具有类型 T
  • 否则,如果将 x 分类为隐式类型化变量(即隐式类型声明表达式),并且 y 具有类型 T,则变量的推断类型 T,并且赋值具有类型 T
  • 否则,如果 x 被分类为属性或索引器访问,则属性或索引器具有可访问的集访问器,x 具有类型 T,并且 y 隐式转换为 T,则赋值具有类型 T
  • 否则,分配无效且发生绑定时错误。

对类型为 Tx = y 形式的简单赋值的运行时处理,是作为对 T 类型 yx 的赋值进行的,包括以下递归步骤:

  • 如果 x 还尚未求值,则会对其进行求值。
  • 如果 x 被归类为变量,则会计算 y,并根据需要通过隐式转换(§10.2)转换为 T
    • 如果 x 给定的变量是 reference_type的数组元素,则执行运行时检查以确保为 y 计算的值与 x 是元素的数组实例兼容。 如果 ynull,或者从 y 引用的实例类型到包含 x 的数组实例的实际元素类型之间存在隐式引用转换 (§10.2.8),则检查成功。 否则,将会引发 System.ArrayTypeMismatchException
    • 由对 y 的求值和转换得到的值存储在对 x求值后确定的位置,并在赋值时作为结果返回。
  • 如果 x 被归类为属性或索引器访问:
    • 计算 y,如果需要,将其通过隐式转换(§10.2)转换为 T
    • 在调用 x 的 set 访问器时,会将 y 的求值和转换结果作为其值参数。
    • y 的求值和转换结果将作为赋值的结果。
  • 如果 x 被归类为具有元组数 n 的元组 (x1, ..., xn)
    • yn 元素一起解构为元组表达式 e
    • 通过使用隐式元组转换将 e 转换为 T 来创建结果元组 t
    • 从左到右依次对每个 xi 执行赋值到 t.Itemixi,但不再对 xi 进行求值。
    • 赋值的结果是 t

注意:如果 x 的编译时间类型是 dynamic,并且从 y 的编译时间类型到 dynamic存在隐式转换,则无需运行时解析。 尾注

注意:数组共同方差规则(§17.6)允许数组类型的值 A[] 作为对数组类型的实例 B[]的引用,前提是存在从 BA的隐式引用转换。 由于这些规则,reference_type 的数组元素的赋值需要运行时检查,以确保所分配的值与数组实例兼容。 在示例中

string[] sa = new string[10];
object[] oa = sa;
oa[0] = null;              // OK
oa[1] = "Hello";           // OK
oa[2] = new ArrayList();   // ArrayTypeMismatchException

最后一个赋值会导致 System.ArrayTypeMismatchException 抛出,因为 ArrayList 的引用不能存储在 string[] 的元素中。

尾注

struct_type 中声明的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式应归类为变量。 如果实例表达式被分类为值,则会发生绑定时错误。

注意:由于 §12.8.7,同样的规则也适用于字段。 尾注

示例:给定声明:

struct Point
{
   int x, y;

   public Point(int x, int y)
   {
      this.x = x;
      this.y = y;
   }

   public int X
   {
      get { return x; }
      set { x = value; }
   }

   public int Y {
      get { return y; }
      set { y = value; }
   }
}

struct Rectangle
{
    Point a, b;

    public Rectangle(Point a, Point b)
    {
        this.a = a;
        this.b = b;
    }

    public Point A
    {
        get { return a; }
        set { a = value; }
    }

    public Point B
    {
        get { return b; }
        set { b = value; }
    }
}

在示例中

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

允许 p.Xp.Yr.Ar.B 的赋值,因为 pr 是变量。 但是,在示例中

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

赋值全部无效,因为 r.Ar.B 不是变量。

结束示例

12.21.3 Ref 赋值

= ref 运算符被称为 ref 赋值运算符。

左操作数应是绑定到引用变量(§9.7)、引用参数(非 this)、输出参数或输入参数的表达式。 右操作数应是一个表达式,它能产生一个 variable_reference,从而指定一个与左操作数类型相同的值。

如果左操作数的 ref-safe-context (§9.7.2) 宽于右操作数的 ref-safe-context,则属于编译时错误。

右操作数应在 ref 赋值时确定赋值。

当左操作数绑定到输出参数时,如果在 ref 赋值运算符的开头未明确分配该输出参数,则会出现错误。

如果左操作数是一个可写的 ref(即指定 ref readonly 局部参数或输入参数以外的其他参数),则右操作数应是可写入 variable_reference。 如果右操作数变量是可写的,那么左操作数可以是可写的或只读的引用。

该操作使左操作数成为右操作数变量的别名。 即使右操作数变量是可写入的,也可以将别名设置为只读。

ref 赋值操作符会产生一个被赋值类型的 variable_reference。 如果左操作数可写入,它就是可写入的。

ref 赋值运算符不应读取右操作数引用的存储位置。

示例:下面是使用 = ref的一些示例:

public static int M1() { ... }
public static ref int M2() { ... }
public static ref uint M2u() { ... }
public static ref readonly int M3() { ... }
public static void Test()
{
int v = 42;
ref int r1 = ref v; // OK, r1 refers to v, which has value 42
r1 = ref M1();      // Error; M1 returns a value, not a reference
r1 = ref M2();      // OK; makes an alias
r1 = ref M2u();     // Error; lhs and rhs have different types
r1 = ref M3();    // error; M3 returns a ref readonly, which r1 cannot honor
ref readonly int r2 = ref v; // OK; make readonly alias to ref
r2 = ref M2();      // OK; makes an alias, adding read-only protection
r2 = ref M3();      // OK; makes an alias and honors the read-only
r2 = ref (r1 = ref M2());  // OK; r1 is an alias to a writable variable,
              // r2 is an alias (with read-only access) to the same variable
}

结束示例

注意:在使用 = ref 运算符读取代码时,可能很容易将 ref 部分误读为操作数的一部分。 当操作数是条件 ?: 表达式时,这尤其令人困惑。 例如,在读取 ref int a = ref b ? ref x : ref y; 时,务必要将它读成 = ref 是运算符,而 b ? ref x : ref y 是右运算符:ref int a = ref (b ? ref x : ref y);。 重要的是,表达式 ref b 是该语句的一部分,尽管一眼看起来可能是这样。 尾注

12.21.4 复合赋值

如果复合赋值左操作数采用 E.PE[Ei] 形式,其中 E 具有编译时类型 dynamic,则赋值将动态绑定(§12.3.3)。 在这种情况下,赋值表达式的编译时类型是 dynamic,并且下面所述的解析将在运行时根据 E的运行时类型进行。 如果左侧操作数的格式为 E[Ei],其中至少有一个 Ei 元素具有编译时类型 dynamic,并且 E 的编译时类型不是数组,则生成的索引器访问是动态绑定的,但具有有限的编译时检查(§12.6.5)。

通过应用二进制运算符重载决策 (§12.4.5) 来处理形式为 x «op»= y 的运算,就好像该运算被写为 x «op» y。 那么:

  • 如果所选运算符的返回类型可以隐式转换为 x类型,则该操作将如 x = x «op» y进行计算,只是 x 仅会被计算一次。
  • 否则,如果所选运算符是预定义运算符,如果所选运算符的返回类型显式转换为 x 类型,并且如果 y 隐式转换为 x 类型或运算符为 shift 运算符,则将将操作计算为 x = (T)(x «op» y),其中 Tx类型, 只计算 x 一次。
  • 否则,复合赋值无效,并且会发生绑定时错误。

术语“只计算一次”表示,在评估 x «op» y的过程中,x 的任何组成表达式的结果会被临时保存,然后在对 x进行赋值时再次使用。

示例:在赋值 A()[B()] += C()中,其中 A 是返回 int[]的方法,BC 是返回 int的方法,方法仅按顺序 ABC调用一次。 结束示例

当复合赋值的左操作数是属性访问或索引器访问时,属性或索引器应同时具有 get 访问器和 set 访问器。 如果情况并非如此,则会发生绑定时错误。

上面的第二个规则允许在某些上下文中将 x «op»= y 评估为 x = (T)(x «op» y)。 规则存在,因此,当左侧操作数的类型为 sbytebyteshortushortchar时,预定义运算符可用作复合运算符。 即使这两个参数都是其中一种类型,预定义运算符也会生成类型 int的结果,如 §12.4.7.3中所述。 因此,如果不进行强制转换,就无法将结果赋值给左操作数。

预定义运算符规则的直观效果是,如果 x «op» yx = y 都是允许的,那么 x «op»= y 就是允许的。

示例:在以下代码中

byte b = 0;
char ch = '\0';
int i = 0;
b += 1;           // OK
b += 1000;        // Error, b = 1000 not permitted
b += i;           // Error, b = i not permitted
b += (byte)i;     // OK
ch += 1;          // Error, ch = 1 not permitted
ch += (char)1;    // OK

每个错误的直观原因是,相应的简单赋值也会出错。

结束示例

注意:这也意味着复合赋值操作支持提升运算符。 由于复合赋值 x «op»= y 会被计算为 x = x «op» yx = (T)(x «op» y),因此计算规则隐式涵盖了提升运算符。 尾注

12.21.5 事件分配

如果 a += or -= 运算符的左操作数被归类为事件访问,那么表达式的求值过程如下:

  • 对事件访问的实例表达式(如有)进行求值。
  • +=-= 运算符的右操作数进行求值,并在需要时,通过隐式转换(§10.2)将其转换为左操作数的类型。
  • 调用事件的事件访问器,参数列表由上一步中计算的值组成。 如果运算符 +=,则调用 add 访问器;如果运算符 -=,则调用 remove 访问器。

事件赋值表达式不生成值。 因此,事件赋值表达式仅在 statement_expression 上下文中有效(§13.7)。

12.22 表达式

expression 要么是 non_assignment_expression,要么是 assignment

expression
    : non_assignment_expression
    | assignment
    ;

non_assignment_expression
    : declaration_expression
    | conditional_expression
    | lambda_expression
    | query_expression
    ;

12.23 常量表达式

常量表达式是必须在编译时完全求值的表达式。

constant_expression
    : expression
    ;

常量表达式应具有值 null 或下列类型之一:

  • sbytebyteshortushortintuintlongulongcharfloatdoubledecimalboolstring;
  • 枚举类型;或
  • 引用类型的默认值表达式 (§12.8.21)。

常量表达式中只允许以下构造:

  • 字面量(包括 null 字面量)。
  • 对类和结构类型的 const 成员的引用。
  • 对枚举类型成员的引用。
  • 对本地常量的引用。
  • 带有圆括号的子表达式,其本身是常量表达式。
  • 强制转换表达式。
  • checkedunchecked 表达式。
  • nameof 表达式。
  • 预定义的 +-!(逻辑求反)和 ~ 一元运算符。
  • 预定义的 +-*/%<<>>&|^&&||==!=<><=>= 二进制运算符。
  • ?: 条件运算符。
  • ! null 包容运算符 (§12.8.9)。
  • sizeof 表达式,条件是非托管类型是 §23.6.9 中指定的类型之一,其中 sizeof 返回一个常量值。
  • 默认值表达式,前提是类型是上述类型之一。

常量表达式中允许以下转换:

  • 身份转换
  • 数值转换
  • 枚举转换
  • 常量表达式转换
  • 隐式和显式引用转换,前提是转换的源是计算结果为 null 值的常量表达式。

注意:常量表达式中不允许其他转换,包括非 null 值的装箱、拆箱和隐式引用转换。 尾注

示例:在以下代码中

class C
{
    const object i = 5;         // error: boxing conversion not permitted
    const object str = "hello"; // error: implicit reference conversion
}

i 的初始化是错误的,因为需要进行装箱转换。 对 str 进行初始化是一个错误,因为需要从非 null 值进行隐式引用转换。

结束示例

只要表达式满足上述要求,就会在编译时对表达式进行求值。 即使表达式是包含非常量构造的较大表达式的子表达式,也是如此。

常量表达式的编译时计算使用与非常量表达式的运行时计算相同的规则,除非运行时计算会引发异常,编译时计算会导致编译时错误发生。

除非将常量表达式明确置于 unchecked 上下文中,否则在表达式的编译时求值过程中,整型算术运算和转换中出现的溢出总是会导致编译时错误 (§12.8.20)。

常量表达式在下面列出的上下文中是必需的,这在语法中使用 constant_expression表示。 在这些上下文中,如果在编译时无法完全计算表达式,则会发生编译时错误。

  • 常量声明(§15.4
  • 枚举成员声明 (§19.4)
  • 参数列表的默认参数(§15.6.2
  • switch 语句 (§13.8.3) 的 case 标签。
  • goto case 语句 (§13.10.4)
  • 包含初始值设定项的数组创建表达式 (§12.8.17.5) 中的维度长度。
  • 特性(§22
  • constant_pattern (§11.2.3) 中

隐式常量表达式转换(§10.2.11)允许 int 类型的常量表达式转换为 sbytebyteshortushortuintulong,前提是常量表达式的值在目标类型范围内。

12.24 布尔表达式

boolean_expression 是一个能产生 bool 类型结果的表达式;既可以直接产生,也可以在某些上下文中通过应用 operator true 来产生,具体如下:

boolean_expression
    : expression
    ;

if_statement (§13.8.2)、while_statement (§13.9.2)、do_statement (§13.9.3) 或 for_statement (§13.9.4) 的控制条件表达式是一个 boolean_expression?: 运算符(§12.18)的控制条件表达式遵循与 boolean_expression相同的规则,但由于运算符优先级的原因被归类为 null_coalescing_expression

需要 boolean_expressionE 才能生成 bool 类型的值,如下所示:

  • 如果 E 可隐式转换为 bool 则在运行时应用隐式转换。
  • 否则,一元运算符重载分辨率(§12.4.4)用于在 E上查找 operator true 的唯一最佳实现,并在运行时应用该实现。
  • 如果未找到此类运算符,则会发生绑定时错误。