12.1 一般
表达式是一系列运算符和操作数。 此子句定义操作数和运算符的计算顺序以及表达式的含义。
12.2 表达式分类
12.2.1 总则
表达式的结果被归类为下列结果之一:
- 一个 值。 每个值都有一个关联的类型。
- 变量。 除非另有说明,否则变量是显式类型,并有一个相关类型,即变量的声明类型。 隐式类型变量没有相关类型。
- null 字面量。 具有此分类的表达式可以隐式转换为引用类型或可为 null 的值类型。
- 匿名函数。 具有此分类的表达式可以隐式转换为兼容的委托类型或表达式树类型。
- 元组。 每个元组都有固定数量的元素,每个元素都有一个表达式和一个可选的元组元素名称。
- 属性访问。 每个属性访问都有一个相关类型,即属性类型。 此外,属性访问可能具有关联的实例表达式。 在调用实例属性访问的访问器时,实例表达式的求值结果将成为
this所代表的实例 (§12.8.14)。 - 索引器访问。 每个索引器访问都具有关联的类型,即索引器的元素类型。 此外,索引器访问具有关联的实例表达式和关联的参数列表。 当调用索引器访问器时,计算实例表达式的结果将成为由
this(§12.8.14)表示的实例,而计算参数列表的结果将成为调用的参数列表。 - 无。 当表达式调用返回类型为
void的方法时,就会发生这种情况。 分类为无值的表达式仅在 statement_expression (§13.7)的上下文或 lambda_expression 正文(§12.20)中有效。
对于作为较大表达式的子表达式的表达式,按照所述限制,结果也可以归类为下列类别之一:
- 命名空间。 具有这种分类的表达式只能作为 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.5)中允许,并且可以隐式转换为兼容的委托类型(§10.8)。 在任何其他上下文中,分类为方法组的表达式会导致编译时错误。 - 事件访问。 每个事件访问都有一个关联类型,即事件的类型。 此外,事件访问可能有一个相关的实例表达式。 事件访问可能显示为运算符
+=(-=)的左作数。 在任何其他上下文中,分类为事件访问的表达式会导致编译时错误。 在调用实例事件访问的存取器时,实例表达式的计算结果将变为由this(§12.8.14)表示的实例。 - 一个引发表达式,该表达式可用于多个上下文,以在表达式中引发异常。 throw 表达式可通过隐式转换为任何类型。
通过调用 get 访问器或 set 访问器,属性访问或索引器访问总是会被重新分类为值。 特定访问器由属性或索引器访问的上下文确定:如果访问是分配的目标,则会调用 set 访问器来分配新值(§12.22.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ᵥ) - 重载的一元运算符:
+、-、!(只能用于逻辑否定)、~、++、--、true、false - 重载二进制运算符:
+、-、*、/、%、&、&&、|、||、??、^<<、>>、==、!=、>、<、>=、<= - 赋值运算符:
=,= 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)中,使用旧值F调用方法i,然后使用旧值G调用方法i,最后,使用 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.11 乘法性的 */%§12.11 累加性 +-§12.12 Shift <<>>§12.13 关系和类型测试 <><=>=isas§12.13 平等 ==!=§12.14 逻辑与 &§12.14 逻辑“异或” ^§12.14 逻辑或 \|§12.15 条件“与” &&§12.15 条件“或” \|\|§12.16 和 §12.17 Null 合并和 throw 表达式 ??throw x§12.19 有條件的 ?:§12.22 和 §12.20 赋值和 lambda 表达式 == ref*=/=%=+=-=<<=>>=&=^=\|==>??=尾注
当操作数出现在优先级相同的两个运算符之间时,运算符的 结合性 决定了操作执行的顺序:
- 除赋值运算符、范围运算符和 null 合并运算符外,所有二进制运算符都是 左关联运算符,这意味着从左到右执行作。
示例:
x + y + z被评估为(x + y) + z。 结束示例 - 赋值运算符、null 合并运算符和条件运算符 (
?:) 为右结合运算符,即从右向左执行运算。示例:
x = y = z被评估为x = (y = z)。 结束示例 - range 运算符 是非关联运算符,这意味着范围运算符的左右作数都不能是 range_expression。
示例:这两者都
x..y..zx..(y..z)无效,与非关联一样..。 结束示例
可以使用括号控制优先级和关联性。
示例:
x + y * z先y乘以z,然后将结果添加到x,但(x + y) * z先添加xy,然后将结果乘以z。 结束示例
12.4.3 运算符重载
所有一元运算符和二进制运算符都具有预定义的实现。 此外,还可以通过在类和结构中包含运算符声明(§15.10)来引入用户定义的实现。 用户定义的运算符实现始终优先于预定义运算符实现:只有在不存在适用的用户定义的运算符实现时,才会考虑预定义运算符实现,如 §12.4.4 和 §12.4.5中所述。
可重载的一元运算符包括:
+ - !(仅逻辑否定)~ ++ -- true false
只有上述运算符可以被重载。 具体而言,无法重载 null 放弃运算符(后缀 !、 §12.8.9)或一元帽子运算符(前缀 ^,12.9.6)。
注意:尽管
true表达式中未显式使用(false因此未包含在 §12.4.2 中的优先表中),但它们被视为运算符,因为它们是在多个表达式上下文中调用的:布尔表达式(§12.25)和涉及条件逻辑运算符(§12.19)和条件逻辑运算符(§12.15)。 尾注
可重载的二进制运算符是:
+ - * / % & | ^ << >> == != > < <= >=
只有上述运算符可以被重载。 具体而言,无法重载成员访问、方法调用或..、、、=、&&||???:=>checkeduncheckednew、typeof、和defaultas运算符。 is
重载二进制运算符时,相应的复合赋值运算符(如果有)也会隐式重载。
示例:运算符
*的重载同时也是运算符*=的重载。 这在 §12.22 中进一步介绍。 结束示例
赋值运算符 (=) 本身无法重载。 赋值始终将值简单存储到变量(§12.22.2)。
通过提供用户定义的转换 ((T)x) 可以重载诸如 等强制转换运算。
注意:用户定义的转换不会影响
is或as运算符的行为。 尾注
元素访问(如 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.22 中各个运算符的说明指定运算符的预定义实现以及适用于每个运算符的任何其他规则。 说明中使用了术语一元运算符重载决策、二元运算符重载决策、数值提升,其提升运算符定义见以下子子句。
12.4.4 一元运算符重载解析
形式为 «op» x 或 x «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» 是可重载的二元运算符, x 是 X类型的表达式, y 是 Y类型的表达式,处理过程如下:
- 确定
X和Y为运算operator «op»(x, y)提供的候选用户定义运算符集。 该集由X提供的候选运算符和由Y提供的候选运算符的联合组成,每个运算符均使用 §12.4.6的规则确定。 对于合并集,候选者的合并方式如下所示:- 如果
X和Y是可转换的标识,或者如果X和Y派生自通用基类型,则共享候选运算符仅在组合集中发生一次。 - 如果
X与Y之间存在标识转换,由«op»Y提供的运算符Y与由«op»X提供的运算符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。 - 对于
operator «op»中所有T₀声明和所有此类运算符的提升形式,如果至少一个运算符对参数列表 是适用的(参见A),那么候选运算符集由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,其中b是byte,s是short,重载决策选择operator *(int, int)作为最佳运算符。 因此,效果是b和s转换为int,结果的类型int。 同样,对于操作i * d,其中i是int,d是double,overload解析选择operator *(double, double)作为最佳运算符。 结束示例
信息性文本的结尾。
12.4.7.2 一元数值提升
此子条款为供参考。
对于预定义的 +、-和 ~ 一元运算符的操作数,会进行一元数值提升。 一元数值提升只需将 sbyte、byte、short、ushort或 char 类型的操作数转换为 int 类型。 此外,对于一元运算符,一元数值推广将操作数类型 uint 转换为类型 long。
信息性文本的结尾。
12.4.7.3 二元数值提升
此子条款为供参考。
对于预定义的 +、-、*、/、%、&、|、^、==、!=、>、<、>= 和 <= 二元运算符的操作数,会进行二元数值提升。 二元数值提升会将两个操作数隐式转换为一个通用类型,且在非关系运算符的情况下,该通用类型也成为运算结果的类型。 二元数值提升包括按以下顺序应用以下规则:
- 如果任一操作数的类型为
decimal,则另一个操作数将转换为类型decimal;如果另一个操作数的类型为float或double,则会发生绑定时错误。 - 否则,如果任一操作数的类型为
double,则另一个操作数将转换为类型double。 - 否则,如果任一操作数的类型为
float,则另一个操作数将转换为类型float。 - 否则,如果任一操作数的类型为
ulong,则另一个操作数将转换为类型ulong;如果另一个操作数为type sbyte、short、int或long,则会发生绑定时错误。 - 否则,如果任一操作数的类型为
long,则另一个操作数将转换为类型long。 - 否则,如果任一操作数的类型为
uint,另一个操作数的类型为sbyte、short或int,则两个操作数将转换为类型long。 - 否则,如果任一操作数的类型为
uint,则另一个操作数将转换为类型uint。 - 否则,两个操作数将转换为
int类型。
注意:第一个规则禁止将
decimal类型与double和float类型混合的任何操作。 规则遵循以下事实:decimal类型和double和float类型之间没有隐式转换。 尾注
注意:另请注意,当另一个操作数是带符号的整型类型时,操作数不可能是
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类型的运算符和&运算符|,如bool?中所述),则提升运算符将生成一个值。 否则,提升运算符会解开操作数,应用基础运算符,并封装结果。 - 对于相等运算符
==和!=,当操作数类型都是不可为 null 的值类型,且结果类型是bool时,则存在运算符的提升形式。 通过在每个操作数类型中添加一个?修饰符,即可构造出提升形式。 提升运算符将两个null值视为相等,null值与任何非null值不相等。 如果两个操作数都是非null,则提升后的运算符会解包操作数,并使用基础运算符来生成bool结果。 - 对于关系运算符
<、>、<=和>=,当操作数类型都是不可为 null 的值类型,并且结果类型为bool时,会存在该运算符的提升形式。 通过在每个操作数类型中添加一个?修饰符,即可构造出提升形式。 如果一个或两个操作数都是false,则提升运算符会生成值null。 否则,提升运算符会解开操作数,应用基础运算符来生成bool结果。
12.5 成员查找
12.5.1 常规
成员查找是指确定类型上下文中名称的含义的过程。 在计算表达式中的 simple_name (§12.8.4) 或 member_access (§12.8.7) 时,可以进行成员查找。 如果 simple_name 或 member_access 作为 invocation_expression 的 primary_expression 出现 (§12.8.10.2),则该成员会显示为已调用。
如果成员是方法或事件,或者它是委托类型(§21)或类型 dynamic (§8.2.4)的常量、字段或属性,则表示该成员可 调用。
成员查找不仅考虑成员的名称,还考虑成员具有的类型参数数以及成员是否可访问。 出于成员查找的目的,泛型方法和嵌套泛型类型具有各自的声明中指示的类型参数数,所有其他成员都具有零类型参数。
类型 N 中带有 K 类型参数的名称 T 的成员查找处理过程如下:
- 首先,确定一组名为
N的可访问成员: - 接下来,如果
K为零,则删除其声明包括类型参数的所有嵌套类型。 如果K不为零,则删除具有不同类型参数数量的所有成员。 当K为零时,不会删除具有类型参数的方法,因为类型推理过程(§12.6.3)可能能够推断类型参数。 - 然后,如果调用该成员,将从集合中移除所有不可调用的成员。
- 然后,将从集合中删除被其他成员隐藏的成员。 对于集中的每个成员
S.M(其中S是声明成员M的类型),都要应用以下规则:- 如果
M是常量、字段、属性、事件或枚举成员,则会从集中删除在基类型S声明的所有成员。 - 如果
M是类型声明,则会从集中删除所有在基类型S中声明的非类型实体,并且移除在基类型M中声明且类型参数数量与S相同的所有类型声明。 - 如果
M是一种方法,则会从集中删除在基类型S中声明的所有非方法成员。
- 如果
- 接下来,被类成员隐藏的接口成员将从集合中删除。 仅当
T是类型参数,并且T具有除object以外的有效基类和非空有效接口集(§15.2.5)时,此步骤才有效。 对于集合中的每一个成员S.M,其中S是声明了成员M的类型,当S是一个非object的类声明时,应用以下规则:- 如果
M是常量、字段、属性、事件、枚举成员或类型声明,则会从集中删除在接口声明中声明的所有成员。 - 如果
M是一种方法,则从集中删除在接口声明中声明的所有非方法成员,并且从集中删除与接口声明中声明M相同的签名的所有方法。
- 如果
- 最后,移除隐藏成员后,查找结果被确定。
- 如果集由不是方法的单个成员组成,则此成员是查找的结果。
- 否则,如果集仅包含方法,则此组方法是查找的结果。
- 否则,查找会变得模棱两可,并且会发生绑定时间错误。
对于类型参数和接口以外的类型中的成员查找,以及严格的单一继承接口(继承链中的每个接口都有零个或一个直接的基接口)中的成员查找,查找规则的作用仅仅是派生成员隐藏具有相同名称或签名的基成员。 这种单一继承查找绝不会含糊不清。 §19.4.11 中介绍了多继承接口中的成员查找可能出现的歧义。
注意:此阶段仅说明一种歧义。 如果成员查找的结果是一个方法组,那么由于歧义,进一步使用该方法组可能会失败,例如,如 §12.6.4.1 和 §12.6.6.2中所述。 尾注
12.5.2 基类型
出于成员查找的目的,类型 T 被视为具有以下基类型:
- 如果
T是object或dynamic,那么T就没有基类型。 - 如果
T是 enum_type,则T的基类型是类类型System.Enum、System.ValueType和object。 - 如果
T是 struct_type,则T的基类型是类类型System.ValueType和object。注意:nullable_value_type 是一个 struct_type (§8.3.1)。 尾注
- 如果
T是 class_type,则T的基类型是T的基类,包括类类型object。 - 如果
T是 interface_type,则T的基类型是T的基本接口和类类型object。 - 如果
T是 array_type,则T的基类型是类类型System.Array和object。 - 如果
T是 delegate_type,则T的基类型是类类型System.Delegate和object。
12.6 函数成员
12.6.1 常规
函数成员是包含可执行语句的成员。 函数成员始终是类型的成员,不能是命名空间的成员。 C# 定义以下类别的函数成员:
- 方法
- 属性
- 事件
- 索引员
- 用户定义的运算符
- 实例构造函数
- 静态构造函数
- 终结器
除终结器和静态构造函数(无法显式调用)外,函数成员中包含的语句通过函数成员调用执行。 编写函数成员调用的实际语法取决于特定的函数成员类别。
函数成员调用的参数列表 (§12.6.2) 为函数成员的参数提供实际值或变量引用。
泛型方法的调用可能采用类型推理来确定要传递给该方法的类型参数集。 此过程在 §12.6.3中介绍。
方法、索引器、运算符和实例构造函数的调用采用重载解析来确定要调用的候选函数成员集。 此过程在 §12.6.4中介绍。
一旦在绑定时识别出特定的函数成员(可能通过重载解析),那么调用该函数成员的实际运行时过程如 §12.6.6中所述。
注释:下表汇总了在涉及可显式调用的六类函数成员的构造中发生的处理。 在表中,
e、x、y和value指示分类为变量或值的表达式,T指示分类为类型的表达式,F是方法的简单名称,P是属性的简单名称。
构造 例 描述 方法调用 F(x, y)重载解析用于在包含类或结构中选择最佳方法 F。 使用参数列表(x, y)调用该方法。 如果方法不是static,那么实例表达式是this。T.F(x, y)重载解析应用于在类或结构 F中选择最佳方法T。 如果该方法不是static,则会发生绑定时错误。 使用参数列表(x, y)调用该方法。e.F(x, y)重载解析用于在类、结构或接口中选择由 F类型给出的最佳方法e。 如果方法static,则会发生绑定时错误。 该方法使用实例表达式e和参数列表(x, y)调用。和 P会调用包含类或结构中属性 P的 get 访问器。 如果P是只读的,则会发生编译时错误。 如果P不是static,则实例表达式为this。P = value使用参数表 P调用包含类或结构中属性(value)的 set 访问器。 如果P为只读,则会发生编译时错误。 如果P不是static,则实例表达式为this。T.P类或结构 P中属性T的 get 访问器被调用。 如果P不是static或P是只写的,则会发生编译时错误。T.P = value使用参数表 P调用类或结构T中属性(value)的 set 访问器。 如果P不是static或P是只读,则会发生编译时错误。e.P使用实例表达式 P调用E类型给出的类、结构或接口中属性e的 get 访问器。 如果P为static或P为只写入,则会发生绑定时错误。e.P = value使用实例表达式 P和参数列表E调用e类型指定的类、结构或接口中属性(value)的 set 访问器。 如果P等于static或者P是只读的,则会发生绑定时错误。事件访问 E += value调用包含类或结构中事件 E的 add 访问器。 如果E不是static,则实例表达式为this。E -= value调用包含类或结构中事件 E的 remove 访问器。 如果E不是static,则实例表达式为this。T.E += value在类或结构 E中,事件T的 add 访问器被调用。 如果E不是static,则会发生绑定时错误。T.E -= value调用包含类或结构 E中事件T的 remove 访问器。 如果E不是static,则会发生绑定时错误。e.E += value使用实例表达式 E调用E类型给出的类、结构或接口中事件e的 add 访问器。 如果E是static,则会发生绑定时间错误。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重载解析用于在类或结构中选择由 x和y类型给出的最佳二进制运算符。 使用参数列表(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_name 的 argument 称为命名参数,而没有 argument_name 的 argument 是位置参数。
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 访问器声明的隐式值参数。
- 对于用户定义的一元运算符(包括转换),单个操作数对应于运算符声明的单个参数。
- 对于用户定义的二进制运算符,左操作数对应于第一个参数,右操作数对应于运算符声明的第二个参数。
- 当未命名参数位于已命名参数或与参数数组相对应的已命名参数之后时,该参数不对应任何参数。
注意:这可以防止
void M(bool a = true, bool b = true, bool c = true);调用M(c: false, valueB);。 第一个参数在位置外使用(参数在第一个位置使用,但名为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.4)。 如果参数数组没有元素,则传递一个空数组;未指定传递的引用是否指向新分配的空数组或现有的空数组。
示例:给定声明
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> } }通过类型推断,从方法的参数确定类型参数
int和string。结束示例
类型推定是方法调用的绑定时间处理的一部分 (§12.8.10.2),发生在调用的重载决策步骤之前。 当在方法调用中指定特定方法组,并且没有将类型参数指定为方法调用的一部分时,类型推理将应用于方法组中的每个泛型方法。 如果类型推理成功,则推断的类型参数用于确定后续重载解析的参数类型。 如果重载解析选择泛型方法作为要调用的方法,则推断的类型参数将用作调用的类型参数。 如果特定方法的类型推理失败,则该方法不参与重载解析。 类型推理失败本身不会导致绑定时错误。 然而,当重载决策无法找到任何适用的方法时,往往会导致绑定时错误。
如果每个提供的参数没有与方法中的一个参数(§12.6.2.2)准确对应,或者存在没有对应参数的非可选参数,则推理会立即失败。 否则,假定泛型方法具有以下签名:
Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)
调用形式为 M(E₁ ...Eₓ) 的方法时,类型推理的任务是查找每个类型参数 S₁...Sᵥ 的唯一类型实参 X₁...Xᵥ,使调用 M<S₁...Sᵥ>(E₁...Eₓ) 变为有效。
下面将类型推理的过程描述为算法。 如果符合性编译器在所有情况下都达到相同的结果,则可以使用替代方法实现符合性编译器。
在推定过程中,每个类型参数 Xᵢ 要么固定为特定类型 Sᵢ,要么不固定,并带有一组相关的边界。每个边界都是某种类型的 T。 最初时,每个类型变量 Xᵢ 是未固定状态,并具有一个空的边界集合。
类型推理分阶段进行。 每个阶段将尝试根据上一阶段的发现推断更多类型变量的类型参数。 第一阶段进行初步推断边界,而第二阶段将类型变量固定为特定类型并推导出更进一步的边界。 第二阶段可能需要重复多次。
注意:类型推理还用于其他上下文,包括用于方法组的转换(§12.6.3.15),并查找一组表达式的最佳常见类型(§12.6.3.16)。 尾注
12.6.3.2 第一阶段
对于每个方法参数 Eᵢ,输入类型推理(§12.6.3.7)从 Eᵢ 相应的参数类型 Tⱼ进行。
12.6.3.3 第二阶段
第二阶段按如下所示进行:
- 所有不依赖的
Xᵢ类型变量(§12.6.3.6)都是Xₑ固定的(§12.6.3.13)。 - 如果不存在这样的类型变量,那么所有非固定类型的变量
Xᵢ都会被固定,对于这些类型变量,以下所有内容都将被保留:- 至少有一个
Xₑ的类型变量Xᵢ -
Xᵢ具有一个非空的边界集
- 至少有一个
- 如果不存在此类类型变量,并且仍有 未修复 类型变量,则类型推理将失败。
- 否则,如果没有其他未固定类型变量存在,则类型推断会成功。
- 否则,对于具有相应参数类型的所有参数
EᵢTⱼ,其中输出类型(§12.6.3.5)包含Xₑ的类型变量,但输入类型(§12.6.3.4)则不Eᵢ进行输出类型推理(Tⱼ)。 然后重复第二个阶段。
12.6.3.4 输入类型
如果 E 是方法组或隐式类型的匿名函数,而 T 是委托类型或表达式树类型,那么 T 的所有参数类型都是类型为E的输入类型T。
12.6.3.5 输出类型
如果 E 是方法组或匿名函数,而 T 是委托类型或表达式树类型,那么 T 的返回类型就是类型为E的输出类型T。
12.6.3.6 依赖
如果对于某个类型为 Xᵢ 的参数 出现在类型为 的 Xₑ 的Eᵥ中,并且 Tᵥ 出现在类型为 Xₑ 的 Eᵥ中,则Tᵥ类型变量Xᵢ直接依赖于一个Eᵥ类型变量 Tᵥ。
如果 Xₑ直接依赖于Xᵢ,或者 Xₑ直接依赖于Xᵢ,Xᵢ依赖于Xᵥ,则 Xᵥ取决于Xₑ。 因此,“依赖于”是“直接依赖于”的可传递的闭包,但不是自反的闭包。
12.6.3.7 输入类型推理
从表达式E类型 的T按以下方式进行:
- 如果
E元组表达式(§12.8.6)具有 arityN和元素Eᵢ,并且T是具有相应元素类型的NarityTₑ的元组类型,或者T是一种具有相应元素类型的T0?元组类型T0N,则对于每个Tₑ元素类型Eᵢ,输入类型推理都是从Eᵢ到Tₑ的。 - 如果为
匿名函数,则 从 > 到 的显式参数类型推理 (§12.6.3.9 ) - 否则,如果
E具有类型U,并且相应的参数是值参数(§15.6.2.2),则从其进行U下限推理(T)。 - 否则,如果
E具有类型U,并且相应的参数是引用参数(§15.6.2.3.3),或输出参数(§15.6.2.3.4),则从其进行U确切推理(T)。 - 否则,如果
E具有类型U,并且相应的参数是输入参数(§15.6.2.3.2),并且E是输入参数,则从其进行U确切推理(T)。 - 否则,如果
E具有一个类型U,并且相应的参数是输入参数(§15.6.2.3.2),则从中生成U下限推理(T)。 - 否则,不会对此参数进行推定。
12.6.3.8 输出类型推理
输出类型推论是从表达式E到类型T 按以下方式进行的:
- 如果是
E具有 arityN和元素Eᵢ的元组表达式,并且T是具有相应元素类型的NarityTₑ的元组类型,或者T为可为 null 的值类型T0?T0,并且是具有相应元素类型的N元组类型Tₑ,则对于每个Eᵢ输出类型推理都是从Eᵢ到Tₑ的。 - 如果
E匿名函数具有推断的返回类型U(§12.6.3.14),并且T是具有返回类型的委托类型或表达式树类型Tₓ,则从中生成U下限推理(Tₓ)。 - 否则,如果
E是一个方法组,而T是一个委托类型或表达式树类型,其参数类型为T₁...Tᵥ而返回类型为Tₓ,并且使用类型E对T₁...Tᵥ进行重载决策会得到一个返回类型为U的单一方法,那么就会进行从U的Tₓ。 - 否则,如果
E是一个类型为U的表达式,则会进行从U的T。 - 否则,不进行推理。
12.6.3.9 显式参数类型推理
显式参数类型推定是从表达式 E到类型 T 的推定:
- 如果
E显式类型为具有参数类型的U₁...Uᵥ匿名函数,并且T是具有参数类型的V₁...Vᵥ委托类型或表达式树类型,则对于每个Uᵢ确切推理(§12.6.3.10),则从Uᵢ相应的函数Vᵢ。
12.6.3.10 精确推理
从类型 U类型 的V如下:
- 如果
是未固定的 之一,则 会被添加到 的确切边界集。 - 否则,集
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.11 下限推理
从类型U类型 的V如下:
- 如果
是未固定 的 之一,则将 添加到 的下限集。 - 否则,如果
V是类型V₁?,U是类型U₁?,则从U₁到V₁进行下限推理。 - 否则,集
U₁...Uₑ和V₁...Vₑ将通过检查是否存在以下情况来确定:-
V是数组类型V₁[...],U是相同排名的数组类型U₁[...] -
V是IEnumerable<V₁>、ICollection<V₁>、IReadOnlyList<V₁>>、IReadOnlyCollection<V₁>或IList<V₁>之一,U是单维数组类型U₁[] -
V是一个构造的class、struct、interface或delegate类型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₁可能是X或Y。
如果这些情况中的任何一种适用,则从每个Uᵢ到相应的Vᵢ进行推理,如下所示: - 如果不知道
Uᵢ是引用类型,则会进行确切推定 - 否则,如果
U是数组类型,则会进行下限推定 - 否则,如果
V是C<V₁...Vₑ>,那么推理依赖于i-th的C类型参数:- 如果是协变的,那么会进行 下限推理。
- 如果它是逆变类型的,则会进行上限推定。
- 如果它是固定的,则会进行确切推定。
-
- 否则,不进行推理。
12.6.3.12 上限推理
从类型U类型 的V如下:
- 如果
V是 未固定的Xᵢ之一,则将U添加到Xᵢ的上限集。 - 否则,集
V₁...Vₑ和U₁...Uₑ将通过检查是否存在以下情况来确定:-
U是数组类型U₁[...],V是相同排名的数组类型V₁[...] -
U是IEnumerable<Uₑ>、ICollection<Uₑ>、IReadOnlyList<Uₑ>、IReadOnlyCollection<Uₑ>或IList<Uₑ>之一,V是单维数组类型Vₑ[] -
U是类型U1?,V是类型V1? -
U是构建的类、结构、接口或委托类型C<U₁...Uₑ>,V是class, struct, interface的delegate或identical类型,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是数组类型,则会进行 上界推断。 - 否则,如果
U是C<U₁...Uₑ>,那么推理依赖于i-th的C类型参数:- 如果它是协变类型的,则会进行上限推定。
- 如果它是逆变类型的,则会进行下限推定。
- 如果它是固定的,则会进行确切推定。
-
- 否则,不进行推理。
12.6.3.13 修复
具有一组绑定的非固定类型变量 Xᵢ 的固定如下:
-
候选类型
Uₑ集最初是Xᵢ绑定集中所有类型的集合。 - 依次检查
Xᵢ的每个绑定:对于Xᵢ的每个确切绑定 U,所有与Uₑ不相同的类型U都会从候选集中删除。 对于U的每个下限Xᵢ,候选集中会删除所有类型Uₑ,它们不会从U进行隐式转换。 对于Xᵢ的每个上限 U,候选集中将删除所有Uₑ类型,它们不会隐式转换为U。 - 如果在剩余的候选类型
Uₑ中,有一个唯一的类型V可以从所有其他候选类型隐式转换到该类型,那么Xᵢ就被固定为V。 - 否则,类型推理失败。
12.6.3.14 推断返回类型
匿名函数的推断返回类型 F 用于类型推理和重载解析。 只有在已知所有参数类型的匿名函数中,才能确定推定的返回类型,这些参数类型可能是显式给出的,也可能是通过匿名函数转换提供的,还可能是在外层泛型方法调用的类型推定过程中推定出来的。
推断的有效返回类型 确定如下:
- 如果
F正文是具有类型的 表达式,则推断出的有效返回类型F是该表达式的类型。 - 如果块的主体
是 一个块 ,并且块语句中的表达式集具有最佳通用类型 ( §12.6.3.16),则推断的有效返回类型 为 < a2.6.3.16。 - 否则,无法推断
F的有效返回类型。
推断的返回类型 确定如下:
- 如果
F异步且主体F是分类为无内容(§12.2)的表达式或没有return表达式的块,则推断的返回类型为«TaskType»(§15.14.1)。 - 如果
F异步且具有推断的有效返回类型T,则推断的返回类型为«TaskType»<T>»(§15.14.1)。 - 如果
F非异步且具有推断的有效返回类型T,那么推断的返回类型为T。 - 否则,无法推断
F的返回类型。
示例:作为涉及匿名函数的类型推理的示例,请考虑在
Select类中声明的System.Linq.Enumerable扩展方法: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具有Name类型的string属性,Select方法可用于选择客户列表的名称:List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);在处理 的扩展方法调用 (
Select) 时,会将调用重写为静态方法调用:IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);由于未显式指定类型参数,因此类型推理用于推断类型参数。 首先,客户的参数与源参数相关,推断
TSource为Customer。 然后,使用上述匿名函数类型推理过程,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为string。 然后,第一个匿名函数的参数s给出推断的类型string,表达式TimeSpan.Parse(s)与f1的返回类型相关,推断YSystem.TimeSpan。 最后,第二个匿名函数(t)的参数得到推断的类型System.TimeSpan,表达式t.TotalHours与f2的返回类型相关,推断Zdouble。 因此,调用的结果的类型为double。结束示例
12.6.3.15 方法组转换的类型推理
与泛型方法的调用类似,当包含泛型方法的方法组 M 转换为给定委托类型 D(§10.8)时,还应应用类型推理。 给定一种方法
Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)
将方法组 M 分配给委托类型 D 后,类型推理的任务是查找类型参数 S₁...Sᵥ,以便表达式:
M<S₁...Sᵥ>
与 ..21.2D 兼容。
与泛型方法调用的类型推理算法不同,在这种情况下,只有参数 类型,没有参数 表达式。 具体而言,没有匿名函数,因此不需要多个推理阶段。
相反,所有 Xᵢ 都会被视为非固定,并从 的每个参数类型 UₑD的相应参数类型 Tₑ 进行M。 如果对于任何 Xᵢ 未找到边界,则类型推理将失败。 否则,所有 Xᵢ 都会固定到相应的 Sᵢ 中,这是类型推定的结果。
12.6.3.16 查找一组表达式的最佳常见类型
在某些情况下,需要为一组表达式推断通用类型。 具体而言,可以通过这种方式找到隐式类型数组的元素类型和具有 块 主体的匿名函数的返回类型。
一组表达式 E₁...Eᵥ 的最佳通用类型确定如下:
- 引入了一个新的 未固定 类型变量
X。 - 对于每个表达式,输出
Ei(§12.6.3.8)将从该表达式执行到X。 -
X是固定的(§12.6.3.13),如果可能,生成的类型是最好的常见类型。 - 否则推理失败。
注意:直观地说,这种推理相当于调用方法
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中的每个参数都对应于函数成员声明中的参数,如 §12.6.2.2中所述,最多一个参数对应于每个参数,任何参数都不对应的任何参数都是可选参数。 - 对于
A中的每个参数,参数的参数传递模式与相应参数的参数传递模式相同,并且
对于包含参数数组的函数成员,如果函数成员适用上述规则,则表示其 正常形式适用。 如果包含参数数组的函数成员在其普通形式中不适用,则函数成员可能改为适用于其 扩展形式:
- 扩展形式通过将函数成员声明中的参数数组替换为参数数组元素类型的零个或多个值参数来构造,使参数列表中的参数数量
A与参数总计数一致。 如果A的参数数少于函数成员声明中的固定参数数,则无法构造函数成员的扩展形式,因此不适用。 - 否则,如果
A中的每个参数满足以下其中一个条件,那么可以使用展开形式:
当从参数类型到输入参数的参数类型的隐式转换是动态隐式转换(§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_name 或 member_access 得到方法组时才适用。
- 只有当方法组产生于 simple_name、通过变量或值的 member_access 或 base_access 时,实例方法才适用。
- 如果方法组是从 simple_name中得来的,则仅当
this访问被允许时,实例方法才适用 §12.8.14。
- 如果方法组是从 simple_name中得来的,则仅当
- 当方法组源自于 member_access 时(如 §12.8.7.2 所述,可以通过实例或类型),实例方法和静态方法都适用。
- 一个泛型方法,其类型参数(显式指定或推断)并不完全满足其约束不适用。
- 在方法组转换的上下文中,应存在标识转换(§10.2.2)或隐式引用转换(§10.2.8)从方法返回类型到委托的返回类型。 否则,候选方法不适用。
12.6.4.3 更好的函数成员
为了确定更好的函数成员,构造一个精简后的参数列表 A,只包含参数表达式本身,并按照它们在原始参数列表中出现的顺序排布,同时排除任何 out 或 ref 参数。
按以下方式构造每个候选函数成员的参数列表:
- 如果函数成员仅适用于扩展形式,则使用扩展形式。
- 没有相应参数的可选参数将从参数列表中删除
- 从参数列表中删除引用和输出参数
- 参数重新排序,使其与参数列表中的相应参数位于同一位置。
给定参数列表 A,其中包含一组参数表达式 {E₁, E₂, ..., Eᵥ},以及具有参数类型 Mᵥ 和 Mₓ的两个适用函数成员 {P₁, P₂, ..., Pᵥ} 和 {Q₁, Q₂, ..., Qᵥ},如果 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ₓ的未经证实和未表达式的参数类型。 如果对于每个参数,Mᵥ不小于Mₓ,并且对于至少一个参数,Rx比Sx更具体,则Rx的参数类型比Sx更具体:- 类型参数不特定于非类型参数。
- 递归地,构造类型比另一个构造类型(具有相同数量的类型参数)更具体,条件是至少有一个类型参数更具体,并且没有类型参数比另一个的对应类型参数更不具体。
- 如果第一个数组类型比第二个数组类型在元素类型上更具体(且两者维度数相同),那么第一个数组类型更具体。
- 否则,如果一个成员是非提升运算符,而另一个成员是提升运算符,那么非提升运算符更好。
- 如果两个函数成员都找不到更好的参数,并且
Mᵥ的所有参数都具有相应的参数,而默认参数需要在Mₓ中替换至少一个可选参数,则Mᵥ优于Mₓ。 - 如果
Mᵥ中至少有一个参数使用了比 中相应参数更好的参数传递选择 (Mₓ),并且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 更好的从表达式转换
给定从表达式 C₁ 转换为类型 E 的隐式转换 T₁ 和从表达式 C₂ 转换为类型 E 的隐式转换 T₂,如果以下条件之一成立,则 C₁ 是比 :
-
E完全匹配T₁,E不完全匹配T₂(§12.6.4.6) -
E与T₁和T₂都完全匹配或都不匹配,并且T₁是一个比T₂(§12.6.4.7) 更好的转换目标 -
E是一个方法组(§12.2),T₁与方法组中用于转换的单个最佳方法兼容(§21.4),并且C₁与方法组中用于转换T₂的单个最佳方法不兼容C₂
12.6.4.6 完全匹配的表达式
如果给定表达式 E 和类型 T,满足以下任一条件,则 E完全匹配T:
-
E具有类型S,并且存在从S到T的标识转换 -
E是匿名函数,T是委托类型D或表达式树类型Expression<D>,并满足以下条件之一:- 参数列表
X(E)的上下文中存在D推断的返回类型,并且存在从X返回类型到返回类型的标识转换D -
E是一个没有返回值的asynclambda,而D的返回类型是非泛型的«TaskType»。 - 要么
E是非异步的,且D的返回类型为Y;要么E是异步的,且D的返回类型为«TaskType»<Y>(§15.14.1),需满足以下条件之一:-
E的主体是一个与Y完全匹配的表达式 -
E的主体是一个代码块,其中每个 return 语句返回的都是与Y完全匹配的表达式。
-
- 参数列表
12.6.4.7 更好的转换目标
给定两种类型 T₁ 和 T₂,如果满足以下条件之一,则 T₁是比 :
- 存在从
T₁到T₂的隐式转换,不存在从T₂到T₁的隐式转换 -
T₁是«TaskType»<S₁>(§15.14.1),T₂是«TaskType»<S₂>,而S₁是一个比S₂更好的转换目标。 -
T₁是«TaskType»<S₁>(§15.14.1),T₂是«TaskType»<S₂>,并且T₁比T₂更专业。 -
T₁是S₁或S₁?,其中S₁是有符号整数类型,T₂是S₂或S₂?,其中S₂是无符号整型类型。 具体说来:-
S₁是sbyte,S₂是byte、ushort、uint或ulong -
S₁是short,S₂是ushort、uint或ulong -
S₁是int,S₂是uint,或ulong -
S₁是long,S₂是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.4),接收器中的可访问索引器集在编译时已知。
在这些情况下,对已知函数成员集合中的每个成员进行有限的编译时检查,以确定它是否在运行时绝对不会被调用。 对于每个函数成员 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,并在M以下项中V声明或重写:-
E已计算。 如果此评估导致异常,则不会执行进一步的步骤。 对于实例构造函数而言,这一计算包括为新对象分配存储空间(通常来自执行堆栈)。 在这种情况下,E被归类为变量。 - 如果未
E分类为变量,或者V不是只读结构类型(§16.2.2),M并且不是只读函数成员(§16.4.12),则E为以下类型之一:- 输入参数 (§15.6.2.3.2),或
-
readonly字段 (§15.5.3),或 - 引用
readonly变量或返回 (§9.7),然后创建 's 类型的临时局部变量E,并将值E赋给该变量。 然后,E重新分类为对该临时局部变量的引用。 临时变量可以在this中以M的形式访问,但不能以任何其他方式访问。 因此,只有在可以编写E时,调用方才能观察M对this所做的更改。
- 参数列表的计算方式为 §12.6.2中所述。
-
M被调用。E引用的变量将成为由this引用的变量。
-
- 否则:
-
E已计算。 如果此评估导致异常,则不会执行进一步的步骤。 - 参数列表的计算方式为 §12.6.2中所述。
- 如果
E的类型是 value_type,则要执行装箱转换 (§10.2.9),将E转换为 class_type,并在以下步骤中将E视为该 class_type 的类型。 如果 value_type 是 enum_type,则 class_type 是System.Enum;,否则是System.ValueType。 - 检查
E的值是否有效。 如果E的值为 null,则会引发System.NullReferenceException,并且不再执行其他步骤。 - 确定要调用的函数成员实现:
- 如果
E的绑定时类型是接口,则要调用的函数成员是由M引用的实例的运行时类型提供的E的实现。 此函数成员通过应用接口映射规则(§19.6.5)来确定实例的运行时类型的M实现E。 - 否则,如果
M是虚函数成员,那么要调用的函数成员是由M所引用实例的运行时类型所提供的E实现。 此函数成员通过应用规则来确定与 引用的实例的运行时类型相关的M的最派生实现(E)。 - 否则,
M是一个非虚函数成员,要调用的正是函数成员M本身。
- 如果
- 在上述步骤中确定的函数成员实现被调用。
E引用的对象将成为由此引用的对象。
-
注意:§12.2 将属性访问分类为调用相应的函数成员,即
get访问器或set访问器。 执行上述过程以调用该访问器。 尾注
调用实例构造函数(§12.8.17.2)的结果是创建的值。 调用任何其他函数成员的结果都是从其主体返回 (§13.10.5) 的值(如有)。
12.6.6.2 装箱实例上的调用
在以下情况时,可以通过 value_type 的装箱实例来调用 value_type 中实现的函数成员:
- 当函数成员是从 class_type 类型继承的方法的重写,并通过 class_type 的实例表达式调用时。
注意:class_type 始终是
System.Object、System.ValueType或System.Enum之一。 尾注 - 当函数成员是接口成员函数的实现,并通过 interface_type 的实例表达式调用时。
- 当函数成员通过委托调用时。
在这种情况下,装箱实例会被视为包含 value_type 变量,该变量将成为函数成员调用中该变量所引用的变量。
注意:具体而言,这意味着在装箱实例上调用函数成员时,函数成员可以修改装箱实例中包含的值。 尾注
12.7 解构
解构是一个进程,其中表达式转换为单个表达式的元组。 当简单赋值的目标为元组表达式时,将使用析构来获取要分配给每个元组元素的值。
表达式 E 可以按以下方式解构为包含 n 元素的元组表达式:
- 如果
E是具有n元素的元组表达式,则解构的结果是表达式本身E。 - 否则,如果
E具有具有(T1, ..., Tn)元素的元组类型n,则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
: 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
| array_creation_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_expression、member_access、invocation_expression、element_access、post_increment_expression、post_decrement_expression、null_forgiving_expression、pointer_member_access和pointer_element_access)的一部分。 可以使用标准技术对语法进行转换,以消除互左递归。 此标准并不这样做,因为并非所有分析策略都需要它(例如 LALR 分析器不会),这样做会模糊化结构和说明。 尾注
pointer_member_access (§24.6.3)和 pointer_element_access (§24.6.4)仅在不安全的代码(§24)中可用。
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.IFormattable 或 System.FormattableString(§10.2.5),则内插字符串表达式具有该类型。 否则,它的类型为 string。
注意:内插字符串表达式的可能类型之间的区别可以从
System.String(§C.2) 和System.FormattableString(§C.3) 的文档来确定。 尾注
内插(包括 regular_interpolation 和 verbatim_interpolation 两种内插)的含义是将表达式的值格式化为 string,格式可以是 Regular_Interpolation_Format 或 Verbatim_Interpolation_Format 指定的格式,也可以是表达式类型的默认格式。 然后,格式化后的字符串会根据 interpolation_minimum_width 进行修改(如果有的话),以生成最终的 string 内插到 interpolated_string_expression 中。
注意:如何确定类型的默认格式,详见
System.String(§C.2) 和System.FormattableString(§C.3)。 标准格式的说明与 Regular_Interpolation_Format 和 Verbatim_Interpolation_Format相同,可以在System.IFormattable(§C.4)的文档以及标准库(§C)的其他类型中找到。 尾注
在 interpolation_minimum_width 中,constant_expression 应隐式转换为 int。 让字段宽度成为 constant_expression 的绝对值,让对齐方式成为 constant_expression 值的符号(正或负):
- 如果字段宽度的值小于或等于格式化字符串的长度,则不会修改格式化字符串。
- 否则,格式化字符串将填充空白字符,使其长度等于字段宽度:
- 如果对齐方式为正对齐,则格式化后的字符串将通过预填充进行右对齐,
- 否则会通过追加填充来进行左对齐。
interpolated_string_expression 的整体含义(包括上述内插的格式化和填充)是通过将表达式转换为方法调用来定义的:如果表达式的类型是 System.IFormattable 或 System.FormattableString,则该方法是 System.Runtime.CompilerServices.FormattableStringFactory.Create (§C.3),返回 System.FormattableString 类型的值;否则,类型应为 string,方法是 string.Format (§C.2),它会返回 string 类型的值。
在这两种情况下,调用的参数列表都由一个格式字符串字面量和格式规范组成,后者用于每个内插,而每个表达式都有一个与格式规范相对应的参数。
格式字符串字面量的构造如下,其中 N 是 interpolated_string_expression 中的内插次数。 格式字符串字面量依次包括:
- Interpolated_Regular_String_Start 或 Interpolated_Verbatim_String_Start 的字符
- Interpolated_Regular_String_Mid 或 Interpolated_Verbatim_String_Mid 的字符,如有
- 然后,如果
N ≥ 1从I到0的每个数字N-1:- 占位符规范:
- 一个左括号 (
{) 字符 -
I的十进制表示形式 - 然后,如果相应的 regular_interpolation 或 verbatim_interpolation 有一个 interpolation_minimum_width,则在逗号 (
,) 后面加上 constant_expression 值的十进制表示法 - 相应 regular_interpolation 或 verbatim_interpolation 的 Regular_Interpolation_Format 或 Verbatim_Interpolation_Format 的字符(如有)
- 一个右括号 (
}) 字符
- 一个左括号 (
- 紧随相应内插之后的 Interpolated_Regular_String_Mid 或 Interpolated_Verbatim_String_Mid 字符(如有)。
- 占位符规范:
- 最后,Interpolated_Regular_String_End 或 Interpolated_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 的形式可以是 I 或 I<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_declaration 的 attributes 中,并且如果该声明包含名称为I的类型参数,则 simple_name 将指向该类型参数。 - 否则,对于每个实例类型
T(§15.3.2),从紧接着的封闭类型声明的实例类型开始,然后从每个封闭类或结构声明的实例类型开始(如有):- 如果
e为零,并且T声明包含名称为I的类型参数,则 simple_name 引用该类型参数。 - 否则,如果 中
I的成员查找 (T) 与e类型参数产生了匹配项:- 如果
T是紧邻的封闭类或结构类型的实例类型,并且查找识别出一个或多个方法,则结果为具有关联实例表达式this的方法组。 如果指定了类型参数列表,则用于调用泛型方法(§12.8.10.2)。 - 否则,如果
T是立即封闭类或结构类型的实例类型, 如果查找标识实例成员,并且引用发生在实例构造函数、实例方法或实例访问器(§12.2.1)的 块 中,则结果与表单 的成员访问(this.I)相同。 这只能在e为零时发生。 - 否则,结果与 或
T.I形式的成员访问 (T.I<A₁, ..., Aₑ>) 相同。
- 如果
- 如果
- 否则,对于每个命名空间
N,从 simple_name 所在的命名空间开始,继续每个封闭的命名空间(如果有),直到全局命名空间,以下步骤将被执行,直到找到一个实体:- 如果
e为零,I是N中的命名空间的名称,则:- 如果 simple_name 所在的位置被
N的命名空间声明所括住,并且命名空间声明包含将 与命名空间或类型关联的 extern_alias_directive 或I,那么 simple_name 就会产生歧义,并出现编译时错误。 - 否则,simple_name 指的是
I中名为N的命名空间。
- 如果 simple_name 所在的位置被
- 否则,如果
N包含一个名称为I且具有e类型参数的可访问类型,则:- 如果
e为零并且 simple_name 所在的位置被N的命名空间声明所括住,并且命名空间声明包含将 与命名空间或类型关联的 extern_alias_directive 或I,那么 simple_name 就会产生歧义,并出现编译时错误。 - 否则,namespace_or_type_name 指代由给定类型参数构造的类型。
- 如果
- 否则,如果发生 simple_name 的位置由
N的命名空间声明括起来:- 如果
e为零,并且命名空间声明包含 extern_alias_directive 或 using_alias_directive 将名称I与导入的命名空间或类型关联,则 simple_name 指向该命名空间或类型。 - 否则,如果命名空间声明的 using_namespace_directive 所导入的命名空间正好包含一个名称为
I和e类型参数的类型,那么 simple_name 将指向使用给定类型参数构造的类型。 - 否则,如果命名空间声明的 using_namespace_directive 所导入的命名空间包含多个名称为
I和e类型参数的类型,那么 simple_name 就会产生歧义,并出现编译时错误。
- 如果
注意:此步骤与处理 namespace_or_type_name(§7.8)中的相应步骤完全并行。 尾注
- 如果
- 否则,如果
e为零且I为标识符_, 则simple_name 是 一个简单的放弃,它是声明表达式(§12.18)的形式。 - 否则,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.18)。 因此,deconstruction_expression 只能出现在简单赋值的左侧。
示例:以下代码声明三个变量:a、b 和 c。 每个都是一个整数,并从赋值语句右侧的元组中被赋值。
var (a, b, c) = (1, 2, 3); // a is 1, b is 2, and c is 3. var sum = a + b + c; // sum is 6.赋值的任何单个元素本身都可以是解构表达式。 例如,以下解构表达式将六个变量分配为
a到f。var (a, b, (c, d, (e, f))) = (1, 2, (3, 4, (5, 6)));在此示例中,请注意,嵌套元组的结构必须在分配的两侧匹配。
如果左侧的变量是隐式定义的,则相应的表达式必须具有类型:
(int a, string? b) = (42, null); //OK var (c, d) = (42, null); // Invalid as type of d cannot be inferred (int e, var f) = (42, null); // Invalid as type of f cannot be inferred结束示例
当且仅当元组表达式 Ei 的每个元素表达式都有类型 Ti 时,元组表达式才有类型。 类型应是与元组表达式具有相同迭代度的元组类型,其中每个元素由以下内容给出:
- 如果相应位置的元组元素具有名称
Ni,则元组类型元素应Ti Ni。 - 否则,如果
Ei是Ni、E.Ni或E?.Ni的形式,那么元组类型元素应为Ti Ni,除非以下任一条件成立:- 元组表达式的另一个元素名称为
Ni,或 - 另一个没有名称的元组元素具有形式为
Ni、E.Ni或E?.Ni的元组元素表达式。 -
Ni是ItemX的形式,其中X是一个非0开始的十进制数字序列,可以表示元组元素的位置,而X并不表示元素的位置。
- 元组表达式的另一个元素名称为
- 否则,元组类型元素应为
Ti。
元组表达式的求值方法是按从左到右的顺序求值每个元素表达式。
元组值可以通过将其转换为元组类型(§10.2.13)、将其重新分类为值(§12.2.2)或使其成为析构赋值(§12.22.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在此示例中,所有四个元组表达式都有效。 前两个,
t1和t2,不使用元组表达式的类型,而是应用隐式元组转换。 对于t2,隐式元组转换依赖于从2到long和从null到string的隐式转换。 第三个元组表达式的类型是(int i, string),因此可以重新分类为该类型的值。 而t4的声明则是一个错误:元组表达式没有类型,因为它的第二个元素没有类型。if ((x, y).Equals((1, 2))) { ... };此示例显示元组有时可能会导致多层括号,尤其是在元组表达式是方法调用的唯一参数时。
结束示例
12.8.7 成员访问
12.8.7.1 常规
member_access 由 primary_expression、predefined_type 或 qualified_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ₑ> 形式,其中 E 是 primary_expression、predefined_type 或 qualified_alias_member,I 是单个标识符,<A₁, ..., Aₑ> 是可选的 type_argument_list。 如果未指定任何 type_argument_list,请将 e 视为零。
具有类型 的 primary_expression 的 dynamic 会被动态绑定 (§12.3.3)。 在这种情况下,成员访问被归类为类型的 dynamic属性访问。 然后,在运行时使用运行时类型而不是 primary_expression 的编译时类型,应用下面的规则来确定 member_access 的含义。 如果运行时分类导致方法组,那么成员访问应是 invocation_expression 的 primary_expression。
对 member_access 的计算和分类如下:
- 如果
e为零,E为命名空间,并且E包含名称I的嵌套命名空间,则结果是该命名空间。 - 否则,如果
E是命名空间,并且E包含具有名称I和K类型参数的可访问类型,则结果是使用给定类型参数构造的类型。 - 如果
E被归类为类型、E不是类型参数、 中I的成员查找 (E) 与K类型参数产生匹配,则对E.I进行计算并归类如下:注意:当此类成员查找的结果为方法组且
K为零时,方法组可以包含具有类型参数的方法。 这允许将此类方法视为类型参数推理。 尾注- 如果
I标识类型,则结果是使用任何给定类型参数构造的类型。 - 如果
I标识一个或多个方法,则结果是没有关联的实例表达式的方法组。 - 如果
I标识静态属性,则结果是没有关联的实例表达式的属性访问。 - 如果
I标识静态字段:- 如果字段是只读的,并且引用发生在声明字段的类或结构的静态构造函数之外,则结果为一个值,即
I中静态字段E的值。 - 否则,结果是一个变量,即
I中的静态字段E。
- 如果字段是只读的,并且引用发生在声明字段的类或结构的静态构造函数之外,则结果为一个值,即
- 如果
I标识静态事件:- 如果引用发生在声明事件的类或结构体中,并且事件在声明时没有使用 event_accessor_declarations (§15.8.1),那么
E.I的处理方式就如同I是一个静态字段。 - 否则,结果将是一个没有相关实例表达式的事件访问。
- 如果引用发生在声明事件的类或结构体中,并且事件在声明时没有使用 event_accessor_declarations (§15.8.1),那么
- 如果
I标识常量,则结果为一个值,即该常量的值。 - 如果
I标识枚举成员,则结果为一个值,即该枚举成员的值。 - 否则,
E.I是无效的成员引用,并且会发生编译时错误。
- 如果
- 如果
E是属性访问、索引器访问、变量或值,并且其类型是T,且在具有 类型参数的I中对T的成员查找(K)产生匹配结果,那么就计算E.I,并按照如下方式进行分类:- 首先,如果
E是属性或索引器访问,则获取属性或索引器访问的值(§12.2.2),E 将重新分类为值。 - 如果
I标识一个或多个方法,则结果是具有关联实例表达式E的方法组。 - 如果
I标识了一个实例属性,那么结果就是一个属性访问,其关联实例表达式为E,关联类型为属性类型。 如果T是类类型,则在从T开始并搜索其基类时,从找到的第一个属性声明或重写中选择关联类型。 - 如果
T是 class_type,I标识该 class_type的实例字段:- 如果
E的值为null,则会引发System.NullReferenceException。 - 否则,如果字段是只读的,并且引用发生在声明字段的类的实例构造函数之外,则结果为一个值,即由
I引用的对象中的字段E的值。 - 否则,结果是一个变量,即
I引用的对象中的字段E。
- 如果
- 如果
T是 struct_type,并且I标识该 struct_type的实例字段:- 如果
E是一个值,或者字段是只读的,并且引用发生在声明字段的结构的实例构造函数外部,则结果为一个值,即由I给出的结构实例中字段E的值。 - 否则,结果是一个变量,即
I给出的结构实例中的字段E。
- 如果
- 如果
I标识实例事件:- 如果引用发生在声明事件的类或结构体中,并且事件声明时没有使用 event_accessor_declarations(§15.8.1),且引用未作为
a +=或-=运算符的左侧出现,那么E.I的处理方式完全等同于I是一个实例字段。 - 否则,结果是事件访问,相关的实例表达式为
E。
- 如果引用发生在声明事件的类或结构体中,并且事件声明时没有使用 event_accessor_declarations(§15.8.1),且引用未作为
- 首先,如果
- 否则,将尝试把
E.I作为扩展方法调用来处理 (§12.8.10.3)。 如果此操作失败,E.I是无效的成员引用,并且会发生绑定时错误。
12.8.7.2 相同的简单名称和类型名称
在表单 E.I的成员访问中,如果 E 是单个标识符,并且 E 作为 simple_name(§12.8.4)的含义是常量、字段、属性、局部变量或参数,其类型与 E 的含义相同(§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_access 是 member_access (§12.8.7) 的条件版本,如果结果类型是 void,则为绑定时错误。 有关其结果类型可能为 void 的 null 条件表达式,请参阅(§12.8.11)。
null_conditional_member_access包含一个primary_expression,后跟两个标记“?”和“”.,后跟一个具有可选type_argument_list的标识符,后跟零个或多个dependent_accesses,其中任一标记都可以在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?.A。
E 的含义如下:
如果
P的类型是可空值类型:让
T是P.Value.A的类型。如果
T是一个未知为引用类型或非 null 类型的类型参数,则会出现编译时错误。如果
T是不可为 null 的值类型,则E的类型T?,E的含义与以下含义相同:((object)P == null) ? (T?)null : P.Value.A不同的是
P只计算一次。否则,
E的类型是T,且E的含义与以下含义相同:((object)P == null) ? (T)null : P.Value.A不同的是
P只计算一次。
否则:
令
T为P.A表达式的类型。如果
T是一个未知为引用类型或非 null 类型的类型参数,则会出现编译时错误。如果
T是不可为 null 的值类型,则E的类型T?,E的含义与以下含义相同:((object)P == null) ? (T?)null : P.A不同的是
P只计算一次。否则,
E的类型是T,且E的含义与以下含义相同:((object)P == null) ? (T)null : P.A不同的是
P只计算一次。
注意:在一种形式的表达式中:
P?.A₀?.A₁那么如果
P的值为null,则A₀或A₁都不会被求值。 如果表达式是 null_conditional_member_access 或 null_conditional_element_access§12.8.13 运算的序列,则情况也是如此。尾注
null_conditional_projection_initializer 是 null_conditional_member_access 的一种限制,具有相同的语义。 它仅在匿名对象创建表达式中作为投影初始值设定项发生(§12.8.17.3)。
12.8.9 Null 包容表达式
12.8.9.1 常规
空值容忍表达式的值、类型、分类(§12.2)和安全上下文(§16.4.15)是其 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 包容运算符并不是错误。
12.8.9.2 重写“可能为 null”的判定
在某些情况下,编译器的静态 null 状态分析可能会确定某个表达式的 null 状态是 可能为 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将类型为s的变量string作为Assign的输出参数传递,编译器发出警告,因为s不是可为 null 的变量。 鉴于Assign的第二个参数不可能为 null,因此使用了“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 的含义。 如果 primary_expression 没有编译时类型 dynamic,那么方法调用将接受 §12.6.5 中描述的有限编译时检查。
invocation_expression 的 primary_expression 应是方法组或 delegate_type 的值。 如果 primary_expression 是方法组,则 invocation_expression 是方法调用(§12.8.10.2)。 如果 primary_expression 是 delegate_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.20)的上下文中才允许归类为无项的表达式。 否则,将发生绑定时错误。
- 否则,如果 invocation_expression 调用了按引用返回方法 (§15.6.1) 或按引用返回委托,结果将是一个变量,其关联类型与方法或委托的返回类型相同。 如果调用的是实例方法,而接收者属于类类型
T,则从T开始搜索基类时发现的第一个方法声明或重写中选择相关类型。 - 否则,invocation_expression 调用的是按值返回的方法 (§15.6.1) 或按值返回的委托,其结果是一个值,该值的类型与方法或委托的返回类型相一致。 如果调用的是实例方法,而接收者属于类类型
T,则从T开始搜索基类时发现的第一个方法声明或重写中选择相关类型。
12.8.10.2 方法调用
对于方法调用,invocation_expression 的 primary_expression 应是一个方法组。 方法组确定要调用的一种方法,或从重载方法集中选择要调用的特定方法。 在后一种情况下,要调用的特定方法的确定基于 argument_list中参数类型提供的上下文。
对于形式为 M(A) 的方法调用,其中 M 是一个方法组(可能包括 type_argument_list),而 A 是一个可选的 argument_list,其绑定时处理包括以下步骤:
- 方法调用的候选方法集已被构造。 对于与方法组
F相关联的每个方法M:- 如果
F是非泛型,则F在以下情况下为候选项:-
M没有类型参数列表,并且 -
F适用于A(§12.6.4.2)。
-
- 如果
F为泛型且M没有类型参数列表,则当以下情况下,F是候选项: - 如果
F是泛型的,并且M包含类型参数列表,则当以下情况下F是候选项:
- 如果
- 候选方法集被缩减为只包含来自最派生类型的方法:对于集合中的每个方法
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ₑ是可访问的,当作为静态方法应用于参数时适用,如上所示 - 从 expr 到
Mₑ第一个参数的类型之间存在隐式标识、引用或装箱转换。
搜索 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.G两者,优先于两者E.F。D.FC.F结束示例
12.8.10.4 委托调用
对于委托调用,invocation_expression 的 primary_expression 应是 delegate_type 的值。 此外,将 delegate_type 视为与 delegate_type相同的参数列表的函数成员,delegate_type 应适用于 invocation_expression的 argument_list(§12.6.4.2)。
对形式为 D(A) 的委托调用(其中 D 是 delegate_type 的 primary_expression,A 是可选的 argument_list)的运行时处理包括以下步骤:
-
D已计算。 如果此评估导致异常,则不会执行进一步的步骤。 - 会对参数列表
A进行计算。 如果此评估导致异常,则不会执行进一步的步骤。 - 检查
D的值是否有效。 如果D的值为null,则会引发System.NullReferenceException,并且不再执行其他步骤。 - 否则,
D就是对委托实例的引用。 函数成员调用 (§12.6.6) 在委托调用列表中的每个可调用实体上执行。 对于包含实例和实例方法的可调用实体,调用的实例是可调用实体中包含的实例。
有关没有参数的多个调用列表的详细信息,请参阅 §21.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.20.1)或method_body(§15.6.1)的上下文中。
与语法等效的 null_conditional_member_access 或 null_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_access 或 null_conditional_element_access 具有 delegate_type 时,才可以包含可选的 null_forgiving_operator。
null_conditional_invocation_expression 表达式 E 的形式为 P?A;其中 A 是语法等效的 null_conditional_member_access 或 null_conditional_element_access 的其余部分,因此 A 将以 . 或 [ 开头。 让我们 PA 表示串联 P 和 A。
当 E 作为 statement_expression 出现时,E 的含义与 statement 的含义相同:
if ((object)P != null) PA
不同的是 P 只计算一次。
当 E 作为 anonymous_function_body 或 method_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包括一个基本表达式,后跟一个“[”令牌,后跟一个参数列表,后跟一个“]”令牌。
argument_list 由一个或多个参数组成,用逗号分隔。
element_access
: primary_expression '[' argument_list ']'
;
primary_expression 如果element_access和pointer_element_access(§24.6.4)替代项都适用,则如果嵌入primary_expression是指针类型(§24.3),则选择后者。
element_access 的 primary_expression 不得为array_creation_expression,除非它包括array_initializer;也不得为stackalloc_expression,除非它包括stackalloc_initializer。
注意:存在此限制来禁止可能令人困惑的代码,例如:
var a = new int[3][1];否则会被解释为:
var a = (new int[3])[1];类似的限制适用于 null_conditional_element_access (§12.8.13)。 尾注
如果符合以下至少一项条件,则 element_access 是动态绑定的(§12.3.3)。
-
primary_expression 具有编译时类型
dynamic。 - argument_list至少有一个 表达式具有编译 时类型
dynamic。
在这种情况下,element_access的编译时类型取决于其primary_expression的编译时类型:如果它具有数组类型,则编译时类型为该数组类型的元素类型;否则,编译时类型为dynamicelement_access被归类为类型dynamic值。 运行时将应用以下规则以确定 element_access 的含义。此过程中使用的是运行时类型,而不是 primary_expression 的编译时类型,以及 argument_list 表达式中的那些具有编译时类型的类型 dynamic。 如果 primary_expression 没有编译时类型 dynamic,则元素访问将进行有限的编译时检查,如 §12.6.5 中所述。
示例:
var index = (dynamic)1; // index has compile-time type dynamic int[] a = {0, 1, 2}; var a_elem = a[index]; // dynamically bound, a_elem has compile-time type int string s = "012"; var s_elem = s[index]; // dynamcially bound, s_elem has compile-time type dynamic结束示例
如果element_access的primary_expression为:
- 数组类型的值, element_access 是数组访问(§12.8.12.2):
- 类型值
string, element_access 是字符串访问(§12.8.12.3): - 否则, primary_expression 应是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下 ,element_access 是索引器访问(§12.8.12.4)。
12.8.12.2 数组访问
对于数组访问, argument_list 不应包含命名参数或按引用参数(§15.6.2.3)。
argument_list中的表达式数应与array_type的排名相同,每个表达式应为:
- 类型
int、uint、long或ulong; 或 - 仅适用于单一排名数组访问,类型
Index或Range; 或 - 可隐式转换为上述一个或多个类型。
表单P[A]数组访问的运行时处理,其中Parray_type的primary_expression,是索引表达式A,包括以下步骤:
-
P已计算。 如果此评估导致异常,则不会执行进一步的步骤。 - 对于 argument_list 中的每个索引表达式,从左到右:
- 计算索引表达式,使结果值的类型为 T;
- 然后,此值将转换为类型中的第一个类型:
int、、uintlong、ulong或仅用于单一排名数组访问,Index或者Range;对于其中存在从 T 的隐式转换(§10.2)。 - 如果索引表达式或后续隐式转换的计算会导致异常,则不会计算进一步的索引表达式,也不会执行进一步的步骤。
- 检查
P的值是否有效。 如果P的值为null,则会引发System.NullReferenceException,并且不再执行其他步骤。 - 如果上述步骤生成了类型为单个索引值,
Range则:- 让 L 是引用的
P数组的长度。 -
A在 L (§18.3) 方面检查是否有效。 如果没有,则会引发 a,并且不执行进一System.ArgumentOutOfRangeException步的步骤。 - 相对于 L 的起始偏移量、S 和项
A,根据 (GetOffsetAndLength) 的说明确定。 - 数组访问的结果是包含从索引 S 开始的
P元素的浅表副本的数组。如果 N 为零,则数组具有零个元素。
- 让 L 是引用的
注意:S 和 N 可能均为零(24.3 美元)。 为空数组编制索引通常无效,但从零开始的空范围的索引有效,并返回空数组。 定义还允许 S 为 L,即过去结束索引 (§18.1),在这种情况下 ,N 将为零,返回一个空数组。 尾注
注意: 不能将数组的元素范围分配给使用数组访问。 这不同于索引器访问(§12.8.12.4),这些访问可能(但不需要)支持向值
Range指定的索引范围分配。 尾注
- 否则:
- 计算数组访问的结果是数组的元素类型的变量引用(§9.5)。
-
argument_list 中每个表达式的值都会与
P引用的数组实例每个维度的实际边界进行核对。 如果一个或多个值超出范围,则会引发System.IndexOutOfRangeException,并且不执行进一步的步骤。 - 计算索引表达式给定的数组元素的变量引用,这将成为数组访问的结果。
12.8.12.3 字符串访问
对于访问 argument_list element_access的字符串,应包含一个未命名的值参数(§15.6.2.2),该参数应为:
- 类型
int或Index; 或Range - 可隐式转换为上述一个或多个类型。
表单P[A]的字符串访问的运行时处理,其中类型P为primary_expressionstring,并且A是单个表达式,包括以下步骤:
-
P已计算。 如果此评估导致异常,则不会执行进一步的步骤。 - 计算索引表达式,使结果值的类型为 T;
- 然后,此值将转换为类型中的第一个类型:
int或 ;,Index其中Range存在从 T 的隐式转换(§10.2)。 - 如果索引表达式或后续隐式转换的计算会导致异常,则不会计算进一步的索引表达式,也不会执行进一步的步骤。
- 检查
P的值是否有效。 如果P的值为null,则会引发System.NullReferenceException,并且不再执行其他步骤。 - 如果前面的步骤生成了类型
Range为索引值,则:- 计算字符串访问的结果是类型的值
string。 - 让 L 成为由
P. 引用的字符串的长度。 -
A在 L (§18.3)方面检查是否有效,如果不是,则会引发 a,并且不执行进一System.ArgumentOutOfRangeException步的步骤。 - 相对于 L 的起始偏移量、S 和项
A,根据 (GetOffsetAndLength) 的说明确定。 - 字符串访问的结果是通过复制从 S 开始的
P个字符构成的字符串,如果 N 为零,则字符串为空。
- 计算字符串访问的结果是类型的值
注意:S 和 N 可能均为零(§18.3)。 为空字符串编制索引通常无效,但从零开始的空范围的索引有效,并返回空字符串。 定义还允许 S 为 L,即过去结束索引 (§18.1),在这种情况下 ,N 将为零,并返回一个空字符串。 尾注
- 否则:
- 计算字符串访问的结果是类型的值
char。 - 根据所
P引用的字符串实例的实际边界检查转换后的索引表达式的值。 如果该值超过范围,则会引发 a,并且不执行进一System.IndexOutOfRangeException步的步骤。 - 转换后的索引表达式与字符串
P偏移量处的字符值将成为字符串访问的结果。
- 计算字符串访问的结果是类型的值
12.8.12.4 索引器访问
对于索引器访问,element_access的primary_expression应为类、结构或接口类型的变量或值,并且此类型应实现一个或多个适用于element_access argument_list的索引器。
argument_list不得包含out或ref自变量。
对于形式为 P[A] 的索引访问,其中 P 为类、结构或接口类型 的 T 和 A 为 argument_list 的绑定时处理包括以下步骤:
- 构建由
T提供的索引器集。 该集包括在T或T的基类型中声明的所有索引器,这些索引器不是重写声明,并且在当前上下文中可以访问 (§7.5)。 - 该集合被简化为那些适用且未被其他索引器隐藏的索引器。 以下规则应用于集合中的每个索引器
S.I,其中S是声明索引器I的类型: - 如果生成的候选索引器集为空,则不存在适用的索引器,并发生绑定时错误。
- 候选索引器集的最佳索引器是使用 §12.6.4的重载解析规则标识的。 如果无法识别单个最佳索引器,则索引器访问不明确,并且会发生绑定时错误。
- 检查最佳索引器的访问器:
- 如果索引器访问是分配的目标,则索引器应具有一个集或 ref get 访问器,否则会发生绑定时错误;
- 否则,索引器应具有 get 或 ref get 访问器,否则会发生绑定时错误。
索引器访问的运行时处理包括以下步骤:
- 评估目标 primary_expression
P。 -
argument_list
A的索引表达式按从左到右的顺序计算。 - 使用在绑定时确定的最佳索引器:
12.8.13 Null 条件元素访问
null_conditional_element_access 由 primary_expression,后面依次包括“?”和“[”两个标记、一个 argument_list、一个“]”标记、零或多个 dependent_access 组成,其中任何一个标记前面都可以有一个 null_forgiving_operator。
null_conditional_element_access
: primary_expression '?' '[' argument_list ']'
(null_forgiving_operator? dependent_access)*
;
null_conditional_element_access的argument_list不应包含out或ref自变量。
null_conditional_element_access 的 primary_expression 不得为array_creation_expression,除非它包括array_initializer;也不得为stackalloc_expression,除非它包括stackalloc_initializer。
注意:存在此限制来禁止可能令人困惑的代码。 类似的限制适用于 element_access (§12.8.12),其中可以找到排除的内容的示例。 尾注
null_conditional_element_access 是 element_access (§12.8.12) 的条件版本,如果结果类型是 void,则为绑定时错误。 有关其结果类型可能为 void 的 null 条件表达式,请参阅(§12.8.11)。
null_conditional_element_access 表达式 E 的形式是 P?[A]B;其中 B 是 dependent_access(如有)。
E 的含义如下:
如果
P的类型是可空值类型:令
T为P.Value[A]B表达式的类型。如果
T是一个未知为引用类型或非 null 类型的类型参数,则会出现编译时错误。如果
T是不可为 null 的值类型,则E的类型T?,E的含义与以下含义相同:((object)P == null) ? (T?)null : P.Value[A]B不同的是
P只计算一次。否则,
E的类型是T,且E的含义与以下含义相同:((object)P == null) ? null : P.Value[A]B不同的是
P只计算一次。
否则:
令
T为P[A]B表达式的类型。如果
T是一个未知为引用类型或非 null 类型的类型参数,则会出现编译时错误。如果
T是不可为 null 的值类型,则E的类型T?,E的含义与以下含义相同:((object)P == null) ? (T?)null : P[A]B不同的是
P只计算一次。否则,
E的类型是T,且E的含义与以下含义相同:((object)P == null) ? null : P[A]B不同的是
P只计算一次。
注意:在一种形式的表达式中:
P?[A₀]?[A₁]如果
P的计算结果为null,那么A₀和A₁均不被计算。 如果表达式是 null_conditional_element_access 或 null_conditional_member_access§12.8.8 运算的序列,则情况也是如此。尾注
12.8.14 此访问权限
this_access 由关键字 this 组成。
this_access
: 'this'
;
只有在实例构造函数、实例方法、实例访问器 (§12.2.1) 或终结器的块中,才允许使用 this_access。 它具有以下含义之一:
- 在类的实例构造函数中的
this中使用 时,它将分类为值。 值的类型是发生用法的类的实例类型(§15.3.2),值为对所构造对象的引用。 - 在类的实例方法或实例访问器中的
this中使用 时,它被归类为值。 该值的类型是使用发生在其中的类的实例类型(§15.3.2),该值是对调用方法或访问器的对象的引用。 - 在结构的实例构造函数中的
this中使用 时,它被归类为变量。 变量的类型是在其中发生用法的结构的实例类型(§15.3.2),变量表示正在构造的结构。- 如果构造函数声明没有构造函数初始值设定项,则
this变量的行为与结构类型的输出参数完全相同。 具体而言,这意味着变量应在实例构造函数的每个执行路径中明确分配。 - 否则,
this变量的行为与结构类型的ref参数完全相同。 具体而言,这意味着该变量最初被视为已分配。
- 如果构造函数声明没有构造函数初始值设定项,则
- 在结构的实例方法或实例访问器中的
this中使用 时,它将分类为变量。 变量的类型是发生用法的结构的实例类型(§15.3.2)。
在 this 中使用 会导致编译时错误。 具体而言,不能在静态方法、静态属性访问器或字段声明的 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 时,I 应表示该类或结构的基类的成员。 同样,在类中发生 base[E] 时,基类中应存在适用的索引器。
在绑定时,形式为 和 base.I 的 base[E] 表达式的计算与写入 ((B)this).I 和 ((B)this)[E] 完全相同,其中 B 是出现该构造的类或结构的基类。 因此,base.I 和 base[E] 对应于 this.I 和 this[E],只是this被视为基类的一个实例。
当 base_access 引用虚拟函数成员(方法、属性或索引器)时,运行时调用哪个函数成员的决定 (§12.6.6) 将被更改。 要确定调用的函数成员,需要找到该函数成员相对于 的最派生实现 (B)(而不是相对于 this 的运行时类型,这在非基类访问中很常见)。 因此,在虚拟函数成员的重写中,可以使用 base_access 来调用函数成员的继承实现。 如果 base_access 引用的函数成员是抽象的,则会发生绑定时错误。
注释:与
this不同,base本身不是表达式。 该关键字仅在 base_access 或 constructor_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_expression 或 post_decrement_expression 具有编译时类型 dynamic。运行时,将使用 primary_expression的运行时类型应用以下规则。
如果后缀递增或递减操作的操作数是属性或索引器访问,则该属性或索引器应同时具有 get 和 set 访问器。 如果情况并非如此,则会发生绑定时错误。
一元运算符重载分辨率(§12.4.4)应用于选择特定的运算符实现。 以下类型存在预定义的 ++ 和 -- 运算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal和任意枚举类型。 预定义的 ++ 运算符返回通过向操作数添加 1 生成的值,预定义的 -- 运算符返回通过从操作数减去 1 生成的值。 在检查的上下文中,如果此加法或减法的结果超出结果类型的可取值范围,并且结果类型为整数类型或枚举类型,则会抛出 System.OverflowException。
应有从所选一元运算符的返回类型到 primary_expression类型的隐式转换,否则会发生编译时错误。
表单 x++ 或 x-- 的后缀递增或递减操作的运行时处理包括以下步骤:
- 如果
x被归类为变量:-
x会被求值以生成变量。 -
x的值会被保存。 - 保存的
x值将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。 - 运算符返回的值将转换为
x的类型,并存储在先前x计算给出的位置。 -
x的已保存值将成为操作的结果。
-
- 如果
x被归类为属性或索引器访问:- 如果
x不是static,则计算与x关联的实例表达式,如果x是索引器访问,则计算参数列表,从而在后续的 get 和 set 访问器调用中使用这些结果。 - 调用
x的 get 访问器,并保存返回值。 - 保存的
x值将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。 - 运算符返回的值会转换为
x的类型,并以该值作为值参数调用x的 set 访问器。 -
x的已保存值将成为操作的结果。
- 如果
和++--运算符还支持前缀表示法(§12.9.7)。 x 自身在操作后具有相同的值。
可以使用后缀或前缀符号调用运算符 ++ 或运算符 -- 的实现。 不能为这两个表示法使用单独的运算符实现。
12.8.17 new 运算符
12.8.17.1 常规
new 运算符用于创建新类型的实例。
有三种形式的新表达式:
- 对象创建表达式用于创建新的类类型和值类型的实例。
- 数组创建表达式用于创建新数组类型的实例。
- 委托创建表达式用于获取委托类型的实例。
new 运算符表示创建类型的实例,但不一定意味着内存分配。 具体而言,值类型的实例不需要超出它们所在的变量的附加内存,并且当使用 new 创建值类型的实例时,不会发生分配。
注意:委托创建表达式并不总是创建新实例。 当以与方法组转换(§10.8)或匿名函数转换(§10.7)相同的方式处理表达式时,可能会导致重用现有的委托实例。 尾注
12.8.17.2 对象创建表达式
12.8.17.2.1 常规
object_creation_expression 用于创建 class_type 或 value_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_expression 的 type 应为 class_type、value_type 或 type_parameter。 类型 不能是 元组类型,或者是抽象的或者静态的 类类型。
仅当 类型 为 class_type 或 struct_type时,才允许可选的 argument_list(§12.6.2)。
如果对象创建表达式包含对象初始值设定项或集合初始值设定项,则可以省略构造函数参数列表并括住括号。 省略构造函数参数列表和封闭括号等效于指定空参数列表。
处理包含对象初始值设定项或集合初始值设定项的对象创建表达式包括先处理实例构造函数,然后处理对象初始值设定项指定的成员或元素初始化(§12.8.17.2.2) 或集合初始值设定项(§12.8.17.2.3)。
如果可选 argument_list 中的任何参数具有编译时类型 dynamic,则 object_creation_expression 将动态绑定(§12.3.3),并且以下规则将在运行时,根据具有编译时类型 的 dynamic 中参数的运行时类型来应用。 但是,创建对象时会进行有限的编译时检查,如 §12.6.5中所述。
对形式为 的 new T(A)(其中 T 是 class_type 或 value_type,而 A 是可选的 argument_list)的绑定时处理包括以下步骤:
- 如果
T是 value_type,并且不存在A: - 否则,如果
T为 type_parameter 且不存在A: - 否则,如果
T是 class_type 或 struct_type:
即使 object_creation_expression 是动态绑定的,编译时的类型仍然是 T。
对形式为新 的 T(A)(其中 T 是 class_type 或 struct_type,而 A 是可选的 argument_list)的运行时处理包括以下步骤:
- 如果
T为 class_type: - 如果
T为 struct_type:-
T类型的实例是通过分配临时局部变量创建的。 由于 struct_type 的实例构造函数必须明确为要创建的实例的每个字段赋值,因此不需要初始化临时变量。 - 实例构造函数根据函数成员调用规则(§12.6.6)调用。 对新分配的实例的引用会自动传递给实例构造函数,可以从该构造函数中访问该实例,如下所示。
-
12.8.17.2.2 对象初始值设定项
对象初始值设定项 指定对象零个或多个字段、属性或索引元素的值。
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 后面都有一个等号和一个表达式、一个对象初始值设定项或一个集合初始值设定项。 在对象初始化器中,表达式不能引用正在初始化的那个新创建的对象。
在initializer_target的argument_list中,对类型Index (§18.4.2)或Range(§18.4.3)的参数没有隐式支持。
一个成员初始值设定项,该初始值设定项指定在等号之后的处理方式与目标赋值 (§12.22.2) 相同。
在等号后指定对象初始化器的成员初始化器是 嵌套对象初始化器,即嵌入对象的初始化。 嵌套对象初始值设定项中的赋值被视为对字段或属性成员的赋值,而不是为字段或属性分配新值。 嵌套对象初始化程序不能应用于具有值类型的属性或具有值类型的只读字段。
在等号后指定集合初始化器的成员初始化器是对嵌入式集合的初始化。 不同于将新集合分配给目标字段、属性或索引器,初始值设定项中指定的元素被添加到目标所引用的集合中。 目标应为满足 §12.8.17.2.3中指定的要求的集合类型。
当初始值设定项目标引用索引器时,索引器的参数应当始终精确计算一次。 因此,即使参数最终从未被使用(例如,由于嵌套初始值设定项为空),也会对其副作用进行评估。
示例:以下类表示具有两个坐标的点:
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.2.3 集合初始值设定项
集合初始化器指定集合的元素。
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)*
;
集合初始值设定项由元素初始值设定项序列组成,由 { 和 } 标记括起来,用逗号分隔。 每个元素初始值设定项指定要添加到要初始化的集合对象的元素,由 { 和 } 标记和逗号分隔的表达式列表组成。 可以在不使用大括号的情况下编写单表达式元素初始化器,但它不能是赋值表达式,以避免与成员初始化器混淆。
non_assignment_expression生产在 §12.23 中定义。
示例:下面是包含集合初始值设定项的对象创建表达式的示例:
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.3 匿名对象创建表达式
匿名对象创建表达式 用于创建匿名类型的对象。
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₂,
光伏=电动汽车}
声明了一个匿名类型,其形式为
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;最后一行的赋值是允许的,因为
p1和p2属于相同的匿名类型。结束示例
匿名类型的 Equals 和 GetHashcode 方法将替代从 object继承的方法,并在属性的 Equals 和 GetHashcode 方面定义,以便仅当其所有属性相等时,同一匿名类型的两个实例才相等。
成员声明符可以缩写为简单名称(§12.8.4)、成员访问(§12.8.7)、null 条件投影初始化器 §12.8.8 或基础访问(§12.8.15)。 这称为投影初始值设定项,是同名属性声明和赋值的简称。 具体来说,以下形式的成员声明符
«identifier»、«expr» . «identifier» 和 «expr» ? . «identifier»
分别精确等效于:
«identifier» = «identifier»、«identifier» = «expr» . «identifier» 和 «identifier» = «expr» ? . «identifier»
因此,在投影初始值设定项中,标识符既选择值,也选择赋值的字段或属性。 直观地说,投影初始值设定项不仅投影值,还投影值的名称。
12.8.17.4 数组创建表达式
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[][,]类型的数组实例。 结束示例
表达式列表中的每个表达式的类型应为 int、uint、long或 ulong,或者隐式转换为其中一个或多个类型。 每个表达式的值确定新分配的数组实例中相应维度的长度。 由于数组维度的长度应为非负值,因此在表达式列表中具有具有负值的常量表达式是编译时错误。
除了不安全的上下文(§24.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.16)。 对于多维数组,即 rank_specifier 中至少包含一个逗号的数组,该集合包括嵌套 array_initializer 中的所有 expression。
数组初始值设定项将在 §17.7 中进一步介绍。
计算数组创建表达式的结果被分类为一个值,即对新分配的数组实例的引用。 数组创建表达式的运行时处理包括以下步骤:
-
expression_list 中的维度长度表达式从左到右依次求值。 对每个表达式进行计算后,将隐式转换(§10.2)转换为下列类型之一:
int、uint、long、ulong。 在此列表中的类型中,选择存在隐式转换的第一个类型。 如果表达式的计算或后续隐式转换导致异常,则不会计算进一步的表达式,也不会执行进一步的步骤。 - 维度长度的计算值将进行如下验证:如果一个或多个值小于零,则会引发
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最后一个表达式会导致编译时错误,因为
int和string都无法隐式转换为另一个表达式,因此没有最佳通用类型。 在这种情况下,必须使用显式指定类型的数组创建表达式,例如将类型指定为object[]。 或者,可以将其中一个元素转换为一个通用基类型,该类型随后将成为推断出来的元素类型。结束示例
隐式类型数组创建表达式可以与匿名对象初始值设定项(§12.8.17.3)结合使用来创建匿名类型化的数据结构。
示例:
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.5 委托创建表达式
delegate_creation_expression 用于获取 delegate_type 的实例。
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
委托创建表达式的参数应为方法组、匿名函数或编译时类型 dynamic 或 delegate_type的值。 如果参数是一个方法组,它将标识该方法,对于实例方法,还将标识要为其创建委托的对象。 如果参数是匿名函数,则直接定义委托目标的参数和方法主体。 如果参数是一个值,则标识要创建副本的委托实例。
如果 表达式 具有编译时类型 dynamic,则 delegate_creation_expression 将动态绑定(§12.8.17.5),并且以下规则在运行时使用 表达式的运行时类型应用。 否则,这些规则将在编译时应用。
形式为新 的 D(E) 的绑定时处理过程(其中 D 是 delegate_type,E 是 expression)包括以下步骤:
如果
E是方法组,则委托创建表达式的处理方式与方法组转换(§10.8)从E到D相同。如果
E为值,应与E兼容(§21.2),D结果是对新创建的委托的引用,其中包含调用E的单项调用列表。
形式为新 的 D(E) 的运行时处理过程(其中 D 是 delegate_type,E 是 expression)包括以下步骤:
- 如果
E是方法组,则委托创建表达式将被计算为从 到E的方法组转换(D)。 - 如果
E是匿名函数,则委托创建将作为从E到D(§10.7) 的匿名函数转换进行求值。 - 如果
E是 delegate_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; }
A.f字段初始化了一个委托,该委托指向第二个Square方法,因为该方法与DoubleFunc的参数列表和返回类型完全匹配。 如果没有第二个Square方法,则会发生编译时错误。结束示例
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 generic_dimension_specifier?)*
| unbound_qualified_alias_member ('.' identifier generic_dimension_specifier?)*
;
unbound_qualified_alias_member
: identifier '::' identifier generic_dimension_specifier?
;
generic_dimension_specifier
: '<' comma* '>'
;
comma
: ','
;
typeof_expression 的第一种形式由 typeof 关键字和括号内的类型组成。 这种形式的表达式的结果是所指示类型的 System.Type 对象。 对于任何给定类型,只有一个 System.Type 对象。 这意味着,对于 T 类型,typeof(T) == typeof(T) 始终为 true。 该类型不能是 dynamic。
typeof_expression 的第二种形式由 typeof 关键字和括号中的 unbound_type_name 组成。
注意:unbound_type_name 和 unbound_qualified_alias_member 的语法遵循 type_name(§7.8)和 qualified_alias_member(§14.8.1)的语法,只不过 generic_dimension_specifier 被替换为 type_argument_list。 尾注
在识别typeof_expression的操作数时,如果unbound_type_name和type_name都适用,即不包含generic_dimension_specifier或type_argument_list,那么应选择type_name。
注意:由于 typeof_expression的替代项的排序,ANTLR 会自动做出指定的选择。 尾注
unbound_type_name的含义确定为:
- 通过将每个 generic_dimension_specifier 替换为 type_argument_list,其中每个 type_argument 具有相同数量的逗号和关键字
object,以便将标记序列转换为 type_name。 - 生成的 type_name 解析为构造类型(§7.8)。
- 然后 unbound_type_name 是与已解析的构造类型(§8.4)关联的未绑定泛型类型。
注意:实现不需要转换令牌序列,也不需要生成中间构造类型,只需确定的未绑定泛型类型“好像”遵循了这个过程一样。 尾注
如果类型名称是可 null 引用类型,则属于错误。
typeof_expression 的结果是生成的非绑定泛型的 System.Type 对象。
typeof_expression 的第三种形式由 typeof 关键字和括号内的 void 关键字组成。 这种形式的表达式结果是一个代表没有类型的 System.Type 对象。
typeof(void) 返回的类型对象不同于任何类型返回的类型对象。
注释:这种特殊的
System.Type对象在允许对语言中的方法进行反射的类库中非常有用,这些方法希望通过void实例来表示任何方法(包括System.Type方法)的返回类型。 尾注
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]请注意,
int和System.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 在 §24.6.9 中指定。
12.8.20 checked 和 unchecked 运算符
checked 和 unchecked 运算符用于控制整型算术运算和转换的溢出检查上下文。
checked_expression
: 'checked' '(' expression ')'
;
unchecked_expression
: 'unchecked' '(' expression ')'
;
checked 运算符在选中上下文中计算其包含的表达式,而 unchecked 运算符在未选中上下文中计算其包含的表达式。
checked_expression 或 unchecked_expression 与 parenthesized_expression (§12.8.5) 完全对应,只是所包含的表达式在给定的溢出检查上下文中进行求值。
溢出检查上下文也可以通过 checked 和 unchecked 语句(§13.12)来控制。
下列操作会受到由 checked 和 unchecked 运算符和语句建立的溢出检查上下文的影响:
- 当作数为整型或枚举类型时,预定义
++运算符和--运算符(§12.8.16 和 §12.9.7)。 - 预定义的
-一元运算符(§12.9.3),当操作数为整型时。 - 当两个作数均为整型或枚举类型时,预定义
+运算符、-*二/进制运算符(§12.11)。 - 显式数值转换(§10.3.2)从一个整型或枚举类型转换为另一个整型或枚举类型,或者从
float或double转换为整型或枚举类型。
当上述操作之一生成结果过大而无法用目标类型表示时,执行该操作的上下文将控制生成的行为。
-
checked在上下文中,如果作是常量表达式(§12.24),则会发生编译时错误。 否则,当在运行时执行此运算时,则会引发System.OverflowException。 - 通过丢弃不适合目标类型的高序位可在
unchecked上下文中将结果截断。
对于非常量表达式(§12.24)(在运行时计算的表达式)未由任何 checked 运算符 unchecked 或语句括起来,除非外部因素(如编译器开关和执行环境配置)调用检查已检查的计算,否则默认溢出检查上下文将被取消选中。
对于常量表达式(§12.24)(可在编译时完全计算的表达式),始终检查默认溢出检查上下文。 除非在 unchecked 上下文中显式放置常量表达式,否则在编译时间计算表达式过程中出现的溢出将始终导致编译时错误。
匿名函数的主体不受其出现的 checked 或 unchecked 上下文的影响。
示例:在以下代码中
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.OverflowException,G方法返回 -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 }在计算
F和H中的常量表达式时发生的溢出会导致报告编译时错误,因为这些表达式是在checked上下文中计算的。 在G中计算常量表达式时也会发生溢出,但由于计算发生在unchecked上下文中,因此不会报告溢出。结束示例
checked 和 unchecked 运算符只会影响文本包含在“(”和“)”标记中的运算的溢出检查上下文。 运算符对在计算包含的表达式时调用的函数成员没有影响。
示例:在以下代码中
class Test { static int Multiply(int x, int y) => x * y; static int F() => checked(Multiply(1000000, 1000000)); }在 F 中使用
checked不会影响x * y中Multiply的求值,因此x * y将在默认的溢出检查上下文中求值。结束示例
在十六进制表示法中编写带符号整型类型的常量时,unchecked 运算符很方便。
示例:
class Test { public const int AllBits = unchecked((int)0xFFFFFFFF); public const int HighBit = unchecked((int)0x80000000); }上述两个十六进制常量都属于类型
uint。 由于常量在int范围之外,没有unchecked运算符,转换为int时将产生编译时错误。结束示例
注释:
checked和unchecked运算符和语句允许程序员控制某些数值计算的某些方面。 但是,某些数值运算符的行为取决于其操作数的数据类型。 例如,两个小数相乘始终会导致溢出异常,即使在明确未检测的结构中也是如此。 同样,即使在显式检查的结构中,两个浮点数相乘也不会出现溢出异常。 此外,其他运算符永远不会受到检查模式的影响,无论是默认还是显式。 尾注
12.8.21 默认值表达式
默认值表达式用于获取类型的默认值(§9.3)。
default_value_expression
: explicitly_typed_default
| default_literal
;
explicitly_typed_default
: 'default' '(' type ')'
;
default_literal
: 'default'
;
default_literal 表示默认值 (§9.3)。 它没有类型,但可以通过默认文本转换(§10.2.16)转换为任何类型。
default_value_expression的结果是explicitly_typed_default中显式类型的默认值(§9.3),或default_value_expression转换的目标类型。
如果类型为下列值之一, 则default_value_expression 是常量表达式(§12.24):
- 一个引用类型
- 已知为引用类型的类型参数 (§8.2);
- 以下值类型之一:
sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool,;或 - 任何枚举类型。
12.8.22 堆栈分配
堆栈分配表达式从执行堆栈分配内存块。 执行堆栈 是存储局部变量的内存区域。 执行堆栈不是托管堆的一部分。 当当前函数返回时,将自动恢复用于本地变量存储的内存。
堆栈分配表达式的安全上下文规则在 §16.4.15.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)指示要存储在新分配的内存位置的元素的类型,而表达式 指示这些元素的数量。 这些要素共同决定所需的分配大小。
表达式 的类型应隐式转换为类型 int。
由于堆栈分配的大小不能为负值,因此将项目数指定为求值为负值的 constant_expression 是一个编译时错误。
在运行时,如果要分配的项数为负值,则行为是未定义的。 如果为零,则不进行分配,返回的值是实现定义的。 如果没有足够的内存可用于分配项目,则会抛出 System.StackOverflowException。
当存在 stackalloc_initializer 时:
- 如果省略unmanaged_type,则会根据stackalloc_element_initializer集的最佳通用类型(§12.6.3.16)规则推断。
- 如果省略 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 是指针类型(§24.3)或推断(var),则 stackalloc_expression 的结果是类型 T* (§24.9)的指针。 在这种情况下,stackalloc_expression 必须出现在不安全的代码中。 否则,stackalloc_expression 的结果是 Span<T> 类型的实例,其中 T 是 unmanaged_type 类型:
-
Span<T>(§C.3) 是一个 ref 结构类型 (§16.2.3),它将一个内存块(这里是由 stackalloc_expression 分配的内存块)显示为类型 (T) 项目的可索引集合。 - 结果的
Length属性返回分配的项数。 - 结果的索引器 (§15.9) 会返回一个 variable_reference (§9.5),指向已分配块中的一个项目,并进行范围检查。
catch 或 finally 块中不允许使用堆栈分配初始化器(§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) 成功,则表达式将被视为 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_expression 是 string类型的常量表达式,在运行时不起作用。 具体来说,它的 named_entity 不会被求值,在进行定赋值分析 (§9.4.4.22) 时也会被忽略。 其值是在可选的最终 type_argument_list 之前的 named_entity 的最后一个标识符,转换方式如下:
- 前缀“
@”(如果使用)将被删除。 - 每个 unicode_escape_sequence 都会被转换成相应的 Unicode 字符。
- 任何 formatting_characters 都会被删除。
在测试标识符之间的相等性时,§6.4.3 中应用了这些转换。
示例:在假设
nameof命名空间中声明了泛型类型List<T>的情况下,下面说明了各种System.Collections.Generic表达式的结果: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.20 中进一步介绍。
12.9 一元运算符
12.9.1 概述
仅限逻辑求反 +)、-、、!、、~强制转换和^运算符称为一元运算符。++--await
注意:后缀 null 宽容运算符 (§12.8.9)
!,由于其编译时不可重载的性质,未包含在上述列表中。 尾注
unary_expression
: primary_expression
| '+' unary_expression
| '-' unary_expression
| logical_negation_operator unary_expression
| '~' 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 (§24.6.2)和 addressof_expression (§24.6.5)仅在不安全的代码(§24)中可用。
如果 unary_expression 的操作数具有编译时类型 dynamic,它将被动态绑定 (§12.3.3)。 在这种情况下:
-
unary_expression的编译时类型为:
-
Index^用于 hat 运算符 (§12.9.6) -
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值。 如果x是NaN,则结果也是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 是具有基础类型 E的枚举类型 U 的表达式,与计算 (E)(~(U)x)完全相同,但对 E 的转换始终像在 unchecked 上下文中一样执行(§12.8.20)。
还预定义了上面定义的未提升预定义按位求补运算符的提升 (§12.4.8) 形式。
12.9.6 Hat 运算符
一元 ^ 运算符称为 hat 运算符。 hat 运算符不可重载(§12.4.3),并且有一个预定义的 hat 运算符:
Index operator ^(int x);
窗体 ^x 作的结果是一个与表达式结果等效的从端 Index 值 (§18.2) 值:
new Index(x, true)
与其他 unary_expression一样,作数可能具有编译时类型 dynamic (§12.9.1),并动态绑定(§12.3.3)。 结果的编译时类型始终 Index为 。
帽子操作员的提升形式(§12.4.8)也是预定义的。
12.9.7 前缀递增和递减运算符
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
前缀递增或递减操作的操作数应被归类为变量、属性访问或索引器访问的表达式。 操作的结果是与操作数类型相同的值。
如果前缀递增或递减操作的操作数是属性或索引器访问,则属性或索引器应同时具有 get 和 set 访问器。 如果情况并非如此,则会发生绑定时错误。
一元运算符重载分辨率(§12.4.4)应用于选择特定的运算符实现。 以下类型存在预定义的 ++ 和 -- 运算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal和任意枚举类型。 预定义的 ++ 运算符返回通过向操作数添加 1 生成的值,预定义的 -- 运算符返回通过从操作数减去 1 生成的值。 在 checked 情况下,如果此加法或减法的结果不在结果类型的范围内,并且结果类型是整型或枚举类型,则会抛出 System.OverflowException。
应有从所选一元运算符的返回类型到 unary_expression类型的隐式转换,否则会发生编译时错误。
++x 或 --x 形式的前缀递增或递减操作的运行时处理包括以下步骤:
- 如果
x被归类为变量:-
x会被求值以生成变量。 -
x的值转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。 - 运算符返回的值将转换为
x的类型。 生成的值存储在通过评估x所得到的位置,成为操作的结果。
-
- 如果
x被归类为属性或索引器访问:- 如果
x不是static,则计算与x关联的实例表达式,如果x是索引器访问,则计算参数列表,从而在后续的 get 和 set 访问器调用中使用这些结果。 -
x的 get 访问器会被调用。 - get 访问器返回的值将转换为所选运算符的操作数类型,并使用此值作为参数来调用运算符。
- 运算符返回的值将转换为
x的类型。 调用x的 set 访问器时,该值将作为其值参数。 - 此值也将成为操作的结果。
- 如果
++ 和 -- 运算符还支持后缀表示法(§12.8.16)。
x++ 或 x-- 的结果是操作前 x 的值,而 ++x 或 --x 的结果是操作后 x 的值。 在任一情况下,x 自身在操作后具有相同的值。
可以使用后缀或前缀符号调用运算符 ++ 或运算符 -- 的实现。 不能为这两个表示法使用单独的运算符实现。
还预定义了上面定义的未提升预定义前缀递增和递减运算符的提升 (§12.4.8) 形式。
12.9.8 强制转换表达式
cast_expression 用于将一个表达式显式转换为给定类型。
cast_expression
: '(' type ')' unary_expression
;
形式为 的 (T)E 会将 T 的值显式转换 (E) 为类型 ,其中 是一个类型,E 是一个 T。 如果不存在从 E 到 T的显式转换,则会发生绑定时错误。 否则,结果是由显式转换生成的值。 即使 E 表示变量,结果也始终被分类为值。
cast_expression 的语法会导致某些语法歧义。
示例:表达式
(x)–y既可以解释为 cast_expression(将–y强制转换为类型x),也可以解释为 additive_expression 与 parenthesized_expression 结合(计算值x – y)。 结束示例
若要解决 cast_expression 歧义,存在以下规则:仅当以下至少一个为 true 时,括在括号中的一个或多个标记(§6.4)序列被视为 cast_expression 的开始:
- 标记序列是类型的正确语法,但不适用于表达式。
- 标记序列是类型的正确语法,右括号后面的标记是标记“
~”, 标记“!”,标记“(”,标识符(§6.4.3)、文本(§6.4.5),或任何关键字(§6.4.4)(as和is除外)。
上述“正确语法”一词仅指标记符的序列应符合特定的语法规则。 它特别不考虑任何构成标识符的实际含义。
示例:如果
x和y是标识符,则即使x.y实际上没有表示类型,x.y也是正确的语法。 结束示例
注意:根据消歧义规则,如果
x和y是标识符,(x)y、(x)(y)和(x)(-y)就是 cast_expression,但(x)-y不是,即使x标识了一个类型。 但是,如果x是标识预定义类型的关键字(如int),则所有四种形式都是 cast_expression(因为此类关键字本身不可能是表达式)。 尾注
12.9.9 Await 表达式
12.9.9.1 常规
await 运算符用于暂停对封闭异步函数的求值,直到操作数所代表的异步操作完成为止。
await_expression
: 'await' unary_expression
;
只允许在异步函数(§15.14)的正文中使用await_expression。 在最近的封闭异步函数中,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.9.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.9.3 await 表达式分类
表达式 await t 的分类方式与表达式 (t).GetAwaiter().GetResult()相同。 因此,如果 GetResult 的返回类型是 void,那么 await_expression 被分类为无。 如果它具有非 void 的返回类型 T,则 await_expression 会被归类为 T 类型的值。
12.9.9.4 await 表达式的运行时计算
在运行时,表达式 await t 的计算方式如下:
- 通过对表达式
a进行求值,可以获得 awaiter(t).GetAwaiter()。 -
boolb是通过对表达式(a).IsCompleted求值来获取。 - 如果
b是false,则评估取决于a是否实现接口System.Runtime.CompilerServices.ICriticalNotifyCompletion(以下简称ICriticalNotifyCompletion)。 此检查是在绑定时完成的;也就是说,如果a在运行时具有编译时类型dynamic,则在运行时进行检查,否则在编译时进行检查。 让r表示恢复委托 (§15.14):- 如果
a没有实现ICriticalNotifyCompletion,则对表达式((a) as INotifyCompletion).OnCompleted(r)进行求值。 - 如果
a实现了ICriticalNotifyCompletion,则对表达式((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)进行求值。 - 然后暂停评估,并将控件返回到异步函数的当前调用方。
- 如果
- 紧接着(如果
b是true)或稍后调用恢复委托(如果b是false)时,表达式(a).GetResult()将被求值。 如果返回一个值,则该值是 await_expression的结果。 否则,结果为“无”。
awaiter 对接口方法 INotifyCompletion.OnCompleted 和 ICriticalNotifyCompletion.UnsafeOnCompleted 的实现应导致委托 r 最多被调用一次。 否则,未定义封闭异步函数的行为。
12.10 Range 运算符
运算符 .. 称为 范围 运算符。
range_expression
: unary_expression
| unary_expression? '..' unary_expression?
;
预定义的范围运算符为:
Range operator ..(Index x, Index y);
范围运算符不可重载(§12.4.3)。
所有范围表达式都被视为具有窗体 x..y,其中:
-
x如果存在,则为左侧作数;否则为表达式0; -
y如果存在,则为右作数,否则为表达式^0。
作的结果是等效 Range 于表达式结果的 (§18.3) 值:
new Range(x, y)
如果范围表达式中的任一作数或两个作数都具有编译时类型 dynamic,则表达式将动态绑定 (§12.3.3.3)。 结果的编译时类型始终 Range为 。
范围运算符的提升形式(§12.4.8)也是预定义的。
范围运算符是非关联运算符(§12.4.2)。
12.11 算术运算符
12.11.1 常规
*、/、%、+和 - 运算符称为算术运算符。
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
如果算术运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型为dynamic,下面所述的解决将在运行时通过具有编译时类型dynamic的操作数的运行时类型来进行。
12.11.2 乘法运算符
对于表单 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 的所有可能组合的结果。 在表中,
x和y是正有限值。z是x * y的结果,并四舍五入为最接近的可表示值。 如果结果的大小对于目标类型太大,则z为无穷大。 由于舍入,即使z和x都不为零,y也可能为零。+y-y+0-0+∞-∞NaN+x+z-z+0-0+∞-∞NaN-x-z+z-0+0-∞+∞NaN+0+0-0+0-0NaNNaNNaN-0-0+0-0+0NaNNaNNaN+∞+∞-∞NaNNaN+∞-∞NaN-∞-∞+∞NaNNaN-∞+∞NaNNaNNaNNaNNaNNaNNaNNaNNaN(除非另有说明,但在 §12.11.2–§12.11.6 的浮点表中,使用“”
+表示值为正;使用“-”表示值为负;缺少符号意味着该值可能是正值或负数或没有符号(NaN)。十进制乘法:
decimal operator *(decimal x, decimal y);如果生成的值的大小太大而无法以小数格式表示,则会引发
System.OverflowException。 由于舍入,即使两个操作数都不为零,结果也可能为零。 在任何四舍五入之前,结果的范围是两个操作数范围之总和。 十进制乘法等效于使用System.Decimal类型的乘法运算符。
还预定义了上面定义的未提升预定义乘法运算符的提升 (§12.4.8) 形式。
12.11.3 除法运算符
对于表单 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);如果右操作数的值为零,则会引发
System.DivideByZeroException。除法将结果舍入为零。 因此,结果的绝对值是小于或等于两个操作数商的绝对值的可能最大整数。 当两个操作数具有相同的符号,当两个操作数具有相反的符号时,结果为零或正。
如果左操作数是最小可表示的
int或long值,并且右操作数是–1,则会发生溢出。 在checked上下文中,这会引发System.ArithmeticException(或其子类)。 在unchecked上下文中,是引发System.ArithmeticException(或其子类),还是不报告溢出,而是将结果值作为左操作数的值,这取决于具体的实现定义。浮点除法:
float operator /(float x, float y); double operator /(double x, double y);商是根据 IEC 60559 算术规则计算得出的。 下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,
x和y是正有限值。z是x / y的结果,并四舍五入为最接近的可表示值。+y-y+0-0+∞-∞NaN+x+z-z+∞-∞+0-0NaN-x-z+z-∞+∞-0+0NaN+0+0-0NaNNaN+0-0NaN-0-0+0NaNNaN-0+0NaN+∞+∞-∞+∞-∞NaNNaNNaN-∞-∞+∞-∞+∞NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN小数除法:
decimal operator /(decimal x, decimal y);如果右操作数的值为零,则会引发
System.DivideByZeroException。 如果生成的值的大小太大而无法以小数格式表示,则会引发System.OverflowException。 由于舍入,即使第一个操作数不是零,结果也可能为零。 在四舍五入之前,结果的精度最接近于首选精度,从而保留与精确结果相等的结果。 首选精度等于x精度减去y精度。十进制除法等效于使用类型为
System.Decimal的除法运算符。
还预定义了上面定义的未提升预定义除法运算符的提升 (§12.4.8) 形式。
12.11.4 余数运算符
对于表单 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);x % y的结果是由x – (x / y) * y生成的值。 如果y为零,则会抛出System.DivideByZeroException。如果左侧操作数是最小的
int或long值,而右侧操作数是–1,那么只有当System.OverflowException引发异常时,才会引发x / y。浮点余数:
float operator %(float x, float y); double operator %(double x, double y);下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,
x和y是正有限值。z是x % y的结果,计算为x – n * y,其中 n 是小于或等于x / y的最大可能整数。 计算余数的方法类似于用于整数操作数的方法,但不同于 IEC 60559 定义(其中n是最接近x / y的整数)。+y-y+0-0+∞-∞NaN+x+z+zNaNNaN+x+xNaN-x-z-zNaNNaN-x-xNaN+0+0+0NaNNaN+0+0NaN-0-0-0NaNNaN-0-0NaN+∞NaNNaNNaNNaNNaNNaNNaN-∞NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN十进制余数:
decimal operator %(decimal x, decimal y);如果右操作数的值为零,则会引发
System.DivideByZeroException。 当System.ArithmeticException(或其子类)被引发时,它是由实现定义的。 如果x % y不引发异常,则一致实现不应引发x / y的异常。 在四舍五入之前,结果的精度是两个操作数精度中较大的一个,如果结果的符号不为零,则与x的符号相同。十进制余数等效于使用类型为
System.Decimal的余数运算符。注意:这些规则确保所有类型的结果永远不会与左操作数的符号相反。 尾注
还预定义了上面定义的未提升预定义余数运算符的提升 (§12.4.8) 形式。
12.11.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 的所有可能组合的结果。 在表中,
x和y是非零有限值,z是x + y的结果。 如果x和y具有相同的震级但相反的迹象,则z为正零。 如果x + y太大,无法在目标类型中表示,则z为无穷大且符号与x + y相同。y+0-0+∞-∞NaNxzxx+∞-∞NaN+0y+0+0+∞–∞NaN-0y+0-0+∞-∞NaN+∞+∞+∞+∞+∞NaNNaN-∞-∞-∞-∞NaN-∞NaNNaNNaNNaNNaNNaNNaNNaN小数加法:
decimal operator +(decimal x, decimal y);如果生成的值的大小太大而无法以小数格式表示,则会引发
System.OverflowException。 在四舍五入之前,结果的精度是两个操作数的精度较大者。小数加法等效于使用类型为
System.Decimal的加法运算符。枚举加法。 每个枚举类型都隐式提供以下预定义运算符,其中
E为枚举类型,U是E的基础类型: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操作数都会通过调用从类型ToString继承的虚拟object方法转换为其字符串表示形式。 如果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.11.6 和 §21.6。 由于
System.Delegate不是委托类型,因此未为其定义运算符 +。尾注
还预定义了上面定义的未提升预定义加法运算符的提升 (§12.4.8) 形式。
12.11.6 减法运算符
对于表单 x – y的操作,将应用二进制运算符重载分辨率(§12.4.5)来选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义减法运算符。 运算符都是从 y 中减去 x。
整数减法:
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 的所有可能组合的结果。 在表中,
x和y是非零有限值,z是x – y的结果。 如果x和y相等,则z为正零。 如果x – y太大,无法在目标类型中表示,则z为无穷大且符号与x – y相同。y+0-0+∞-∞NaNxzxx-∞+∞NaN+0-y+0+0-∞+∞NaN-0-y-0+0-∞+∞NaN+∞+∞+∞+∞NaN+∞NaN-∞-∞-∞-∞-∞NaNNaNNaNNaNNaNNaNNaNNaNNaN(在上表中,
-y项表示 的y,而并非值为负。)十进制减法:
decimal operator –(decimal x, decimal y);如果生成的值的大小太大而无法以小数格式表示,则会引发
System.OverflowException。 在四舍五入之前,结果的精度是两个操作数的精度较大者。小数减法等效于使用
System.Decimal类型的减法运算符。枚举减法。 每个枚举类型都隐式提供以下预定义运算符,其中
E为枚举类型,U是E的基础类型:U operator –(E x, E y);此运算符的计算方式与
(U)((U)x – (U)y)完全相同。 换句话说,运算符计算x和y的序号值之间的差异,结果的类型是枚举的基础类型。E operator –(E x, U y);此运算符的计算方式与
(E)((U)x – y)完全相同。 换句话说,运算符从枚举的基础类型中减去一个值,从而生成枚举的值。委托删除。 每个委托类型都隐式提供以下预定义运算符,其中
D是委托类型:D operator –(D x, D y);语义如下:
- 如果第一个操作数是
null,则操作的结果是null。 - 否则,如果第二个操作数
null,则该操作的结果为第一个操作数的值。 - 否则,这两个作数都表示非空调用列表(§21.2)。
- 如果列表比较相等,由委托相等运算符(§12.13.9)确定,则运算的结果为
null。 - 否则,该操作的结果是一个新的调用列表,其中包含第一个操作数的列表,其中删除了第二个操作数的条目,前提是第二个操作数的列表是第一个操作数的子列表。 (要确定子列表是否相等,需要像委托相等运算符一样比较相应的条目。)如果第二个操作数的列表与第一个操作数列表中多个连续条目的子列表匹配,则删除最后一个匹配的连续条目子列表。
- 否则,操作结果就是左操作数的值。
- 如果列表比较相等,由委托相等运算符(§12.13.9)确定,则运算的结果为
在此过程中,操作数的列表(如有)都不会改变。
示例:
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.12 Shift 运算符
<< 和 >> 运算符用于执行位移运算。
shift_expression
: additive_expression
| shift_expression '<<' additive_expression
| shift_expression right_shift additive_expression
;
如果 shift_expression 的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型为dynamic,下面所述的解决将在运行时通过具有编译时类型dynamic的操作数的运行时类型来进行。
对于表单 x << count 或 x >> 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的类型为int或long时,将丢弃x的低序位,其余位向右移动,如果x为非负位,则高阶空位位置设置为零,如果x为负位,则设置为 1。当
x的类型为uint或ulong时,x的低阶位会被丢弃,其余位被右移,高阶空位位置被置零。
对于预定义的运算符,要移动的位数计算如下:
- 当
x的类型为int或uint时,移位计数由count的低序五位决定。 换句话说,移位计数是从count & 0x1F计算得出的。 - 当
x的类型为long或ulong时,移位计数由count的低序六位决定。 换句话说,移位计数是从count & 0x3F计算得出的。
如果结果的移位计数为零,则移位运算符只需返回 x的值。
移位运算绝对不会导致溢出并在选中和未选中上下文中产生相同结果。
当 >> 运算符的左操作数是带符号整型时,运算符会执行算术右移,其中操作数最有效位(符号位)的值会传播到高阶空位位置。 当 >> 运算符的左操作数为无符号整型时,该运算符将执行逻辑右移,其中高阶空位位置始终置零。 要执行与操作数类型相反的操作,可以使用显式强制转换。
示例:如果
x是int类型的变量,则操作unchecked ((int)((uint)x >> y))执行x的逻辑移位。 结束示例
还预定义了上面定义的未提升预定义移位运算符的提升 (§12.4.8) 形式。
12.13 关系运算符和类型测试运算符
12.13.1 常规
==、!=、<、>、<=、>=、is和 as 运算符称为关系运算符和类型测试运算符。
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.13.12 中介绍,as运算符在 §12.13.13 中介绍。
==、!=、<、>、<= 和 >= 运算符是比较运算符。
如果将 default_literal(§12.8.21)用作 <、>、<=或 >= 运算符的操作数,则会发生编译时错误。
如果 default_literal 被用作 == 或 != 运算符的两个操作数,则会出现编译时错误。 如果将 default_literal 用作 is 或 as 运算符的左操作数,则会发生编译时错误。
如果比较运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型是 dynamic,下面所述的解决方案将在运行时通过那些具有编译时类型 dynamic的操作数的运行时类型来进行。
对于形式为 x «op» y的操作,其中 «op» 是比较运算符,重载决策(§12.4.5)将被应用以便选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 如果 equality_expression 的两个操作数都是 null 字面量,则不执行重载决策,并且根据运算符是 true 还是 false,表达式的结果会是 == 或 != 的常量值。
以下子项描述了预定义的比较运算符。 所有预定义的比较运算符都返回布尔类型的结果,如下表所述。
| 运算 | 结果 |
|---|---|
x == y |
如果 true 等于 x,则为 y,否则 false |
x != y |
如果 true 不等于 x,那么 y,否则 false |
x < y |
如果 true 小于 x,输出 y,否则输出 false |
x > y |
如果 true 大于 x,则 y 否则为 false |
x <= y |
如果 true 小于或等于 x, 则使用 y,否则使用 false |
x >= y |
如果 true 大于或等于 x,则输出 y,否则输出 false |
12.13.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.13.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外的所有运算符,结果为 !=,而对于 false,结果为 。 对于任何两个操作数,x != y 始终生成与 !(x == y)相同的结果。 但是,当一个操作数或两个操作数都是 NaN 时,<、>、<=和 >= 运算符不会生成与相反运算符的逻辑否定相同的结果。
示例:如果任一
x和y为 NaN,则x < yfalse,但!(x >= y)true。 结束示例
当两个操作数都不为 NaN 时,运算符将两个浮点操作数的值与排序进行比较
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
其中,min 和 max 是可以采用给定浮点格式表示的最小和最大的正有限值。 此排序的显著影响包括:
- 负零和正零被视为相等。
- 负无穷大被视为小于所有其他值,但等于另一个负无穷大。
- 正无穷大被视为大于所有其他值,但等于另一个正无穷大。
还预定义了上面定义的未提升预定义浮点比较运算符的提升 (§12.4.8) 形式。
12.13.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.13.5 布尔相等运算符
预定义的布尔相等运算符为:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
如果 == 和 true 都是 x,或者 y 和 true 都是 x,则 y 的结果是 false。 否则,结果为 false。
如果 != 和 false 都是 x,或者 y 和 true 都是 x,则 y 的结果是 false。 否则,结果为 true。 当操作数的类型为 bool时,!= 运算符将生成与 ^ 运算符相同的结果。
还预定义了上面定义的未提升预定义布尔值相等运算符的提升 (§12.4.8) 形式。
12.13.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 是枚举类型 E(其基础类型为 U)的表达式,且 «op» 是比较运算符之一,这与计算 ((U)x) «op» ((U)y)的结果完全一致。 换句话说,枚举类型比较运算符只是比较两个操作数的基础整数值。
还预定义了上面定义的未提升预定义枚举比较运算符的提升 (§12.4.8) 形式。
12.13.7 引用类型相等运算符
每个类类型 C 隐式提供以下预定义的引用类型相等运算符:
bool operator ==(C x, C y);
bool operator !=(C x, C y);
除非 C 存在预定义的相等运算符(例如,当 C 是 string 或 System.Delegate时)。
运算符返回比较两个引用的相等性或非相等性的结果。
operator == 返回 true 的条件是 x 和 y 引用同一实例或两者都是 null;而 operator != 返回 true 的条件是使用相同操作数的 operator == 返回 false。
除了正常的适用性规则(§12.6.4.2),预定义的引用类型相等运算符还需要下列之一才能适用:
- 两个操作数都是已知类型为 reference_type 或字面量
null的值。 此外,从任一操作数到另一操作数的类型之间存在着标识转换或显式引用转换 (§10.3.5)。 - 一个操作数是文本
null,另一个操作数是类型T的值,其中T是一个未知值类型的 type_parameter,并且没有值类型约束。- 如果运行时
T为不可为 null 的值类型,则==的结果是false,并且!=的结果是true。 - 如果运行时
T是可为 null 的值类型,则结果根据HasValue作数的属性计算,如 (§12.13.10) 中所述。 - 如果在运行时
T是引用类型,那么如果操作数是true,结果为null,否则为false。
- 如果运行时
除非其中一个条件为 true,否则会出现绑定时错误。
注意:这些规则的显著影响包括:
- 在绑定阶段,如果使用预定义的引用类型相等运算符来比较两个在绑定时已知不同的引用,这是一个错误。 例如,如果操作数的绑定时类型是两个类类型,如果两个操作数都不派生自另一个类类型,则两个操作数不可能引用同一对象。 因此,该操作被视为绑定时错误。
- 预定义的引用类型相等运算符不允许对值类型操作数进行比较(除非在某些特殊情况下类型参数与
null进行比较时)。- 预定义引用类型相等运算符的操作数从不会装箱。 执行这种装箱操作毫无意义,因为对新分配的装箱实例的引用必然与所有其他引用不同。
对于形式为
x == y或x != y的操作,如果存在任何适用的用户定义operator ==或operator !=,运算符重载解析规则(§12.4.5)将选择该运算符,而不是预定义的引用类型相等运算符。 通过将一个或两个操作数显式强制转换为object类型,始终可以选择预定义的引用类型相等运算符。尾注
示例:以下示例检查无约束类型参数的参数类型是否为
null。class C<T> { void F(T x) { if (x == null) { throw new ArgumentNullException(); } ... } }即使
x == null可以表示不可为 null 的值类型,也允许使用T结构,而且当false是不可为 null 的值类型时,结果会被直接定义为T。结束示例
对于格式为 x == y 或 x != 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
s和t变量引用两个包含相同字符的不同字符串实例。 第一个比较输出True,因为当两个作数的类型均为类型时,会选择预定义的字符串相等运算符(string)。 其余的比较均输出False,因为当操作数的绑定时间类型为operator ==时,string类型中object的重载不适用。请注意,上述技术对值类型没有意义。 示例
class Test { static void Main() { int i = 123; int j = 123; Console.WriteLine((object)i == (object)j); } }输出
False,因为强制转换创建了对装箱int值的两个单独实例的引用。结束示例
12.13.8 字符串相等运算符
预定义的字符串相等运算符为:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
当下列值之一为 true 时,两个 string 值被视为相等:
- 两个值都是
null。 - 这两个值都是非
null的引用,指向具有相同长度且每个字符位置上字符都相同的字符串实例。
字符串相等运算符比较字符串值,而不是字符串引用。 当两个单独的字符串实例包含完全相同的字符序列时,字符串的值相等,但引用不同。
注意:如 §12.13.7 中所述,引用类型相等运算符可用于比较字符串引用而不是字符串值。 尾注
12.13.9 委托相等运算符
预定义的委托相等运算符包括:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
两个委托实例被视为相等,如下所示:
- 如果委托实例中的任何一个是
null,那么当且仅当它们都是null时,它们才相等。 - 如果委托具有不同的运行时类型,则它们永远不会相等。
- 如果两个委托实例都有调用列表(§21.2),则这些实例仅在调用列表的长度相同时才相等,并且其中一个调用列表中的每个条目与相应条目相等(如下所示),按顺序在其他调用列表中。
以下规则控制调用列表条目的相等性:
- 如果两个调用列表条目都引用相同的静态方法,则条目相等。
- 如果两个调用列表项都引用同一目标对象(由引用相等运算符定义)上的同一非静态方法,则这些条目是相等的。
- 允许使用相同(可能为空)的外部变量实例集(但不需要)计算语义上相同的匿名函数(§12.20)生成的调用列表条目。
如果运算符重载解析为任一委托相等运算符,并且两个作数的绑定时间类型都是委托类型,而不是 §21System.Delegate中所述,并且绑定类型作数类型之间没有标识转换,则会发生绑定时错误。
注意:此规则可防止比较中由于引用了不同类型委托的实例而永远无法将非
null值视为相等。 尾注
12.13.10 可以为 null 的值类型和 null 文本之间的相等运算符
== 和 != 运算符允许一个操作数是可为 null 的值类型,另一个操作数是 null 字面量,即使该操作没有预定义或用户定义的运算符(未提升或已提升的形式)。
对于其中一种形式的运算
x == null null == x x != null null != x
如果 x 是可为 null 的值类型的表达式,并且运算符重载分辨率(§12.4.5)找不到适用的运算符,则结果将从 HasValue的 x 属性中计算。 具体而言,前两种形式将转换为 !x.HasValue,最后两种形式将转换为 x.HasValue。
12.13.11 元组相等运算符
元组相等运算符按词法顺序成对应用于元组操作数的元素。
如果运算符 x 或 y 的每个操作数 == 和 != 被分类为元组或元组类型的值(§8.3.11),则该运算符是 元组相等运算符。
如果操作数 e 分类为元组,则 e1...en 的元素应是计算元组表达式的元素表达式的结果。 否则,如果 e 是元组类型的值,则元素应为 t.Item1...t.Itemn,其中 t 是对 e 求值的结果。
元组相等运算符的操作数 x 和 y 应具有相同的 arity,否则会出现编译时错误。 对于每一对元素 xi 和 yi,应使用相同的相等运算符,并产生 bool、dynamic 类型的结果,或隐式转换为 bool 的类型,或定义了 true 和 false 运算符的类型。
元组相等运算符 x == y 的计算方式如下:
- 对左侧操作数
x进行求值。 - 对右侧操作数
y进行求值。 - 对于每对元素
xi和yi按词法顺序排列:- 运算符
xi == yi被求值后,会以如下方式得到bool类型的结果:- 如果比较得到了
bool,那么这就是结果。 - 否则,如果比较结果是
dynamic,则会动态调用运算符false,然后用逻辑非运算符(bool)对生成的!值进行取反。 - 否则,如果比较类型可以隐式转换为
bool,则应用该隐式转换。 - 否则,如果比较的类型具有运算符
false,则调用该运算符,并用逻辑求反运算符(bool)对生成的!值进行逻辑求反。
- 如果比较得到了
- 如果结果
bool是false,则不再进行进一步的计算,元组相等运算符的结果是false。
- 运算符
- 如果所有元素比较的结果为
true,则元组相等运算符的结果是true。
元组相等运算符 x != y 的计算方式如下:
- 对左侧操作数
x进行求值。 - 对右侧操作数
y进行求值。 - 对于每对元素
xi和yi按词法顺序排列:- 运算符
xi != yi被求值后,会以如下方式得到bool类型的结果:- 如果比较得到了
bool,那么这就是结果。 - 否则,如果比较产生了
dynamic,则会动态调用运算符true,生成的bool值为结果。 - 否则,如果比较类型可以隐式转换为
bool,则应用该隐式转换。 - 否则,如果比较的类型具有运算符
true,则会调用该运算符,并且生成的bool值为结果。
- 如果比较得到了
- 如果结果
bool是true,则不再进行进一步的计算,元组相等运算符的结果是true。
- 运算符
- 如果所有元素比较的结果为
false,则元组相等运算符的结果是false。
12.13.12 is 运算符
is 运算符有两种形式。 一种是 is-type 运算符,它的右侧有一个类型。 另一种是 is-pattern 运算符,它的右侧有一个模式。
12.13.12.1 is-type 运算符
is-type 运算符 用于检查对象的运行时类型是否与给定类型兼容。 检查在运行时进行。 操作 E is T的结果,其中 E 是表达式且 T 是类型而非 dynamic,是一个布尔值。该布尔值指示 E 是否为非 null,并且能否通过引用转换、装箱转换、拆箱转换、包装转换或拆包转换成功转换为类型 T。
此运算的计算方式如下:
- 如果
E是匿名函数或方法组,则会发生编译时错误。 - 如果
E是null字面量,或者E的值是null,则结果为false。 - 否则:
- 让
R是E的运行时类型。 - 让
D派生自R,如下所示: - 如果
R为可以为 null 的值类型,则D是R的基础类型。 - 否则,
D是R。 - 结果取决于
D和T,如下所示: - 如果
T是引用类型,则结果为true,但前提是:-
D和T之间存在标识转换, -
D是引用类型,并且存在从D到T的隐式引用转换,或者 - 任一:
D是一种值类型,并且存在从D到T的装箱转换。
或者:D是一种值类型,T是由D实现的接口类型。
-
- 如果
T是可为 null 的值类型,并且如果true是D的基础类型,那么结果是T。 - 如果
T是不可为 null 的值类型,则如果true且D的类型相同,则结果T。 - 否则,结果为
false。
is 运算符不考虑用户定义的转换。
注意:由于
is运算符是在运行时进行评估的,所有类型参数都已被替换,不存在需要考虑的开放类型(§8.4.3)。 尾注
注意:
is运算符在编译时类型和转换方面可以理解,如下所示,其中C是E的编译时类型:
- 如果
e的编译时类型与T相同,或者存在从 的编译时类型到 的隐式引用转换(§10.2.8)、装箱转换(§10.2.9)、包装转换(E)或显式解包转换(T):
- 如果
C是非空值类型,则该操作的结果是true。- 否则,该操作的结果等效于计算
E != null。- 否则,如果显式引用转换(§10.3.5)或取消装箱转换(§10.3.7)存在
C,T或者如果C或T为开放类型(§8.4.3),则应执行上述运行时检查。- 否则,无法将
E引用、装箱、包装或解包转换为类型T,操作结果为false。 编译器可以根据编译时类型进行优化。尾注
12.13.12.2 is-pattern 运算符
is-pattern 运算符 用于检查表达式 计算的值是否与给定模式 匹配(§11)。 检查在运行时进行。 如果值与模式匹配,则 is-pattern 运算符的结果为 true;否则为 false。
对于形如 E is P的表达式,其中 E 是 T 类型的关系表达式,P 是一个模式,如果存在以下任一情况,则为编译时错误:
12.13.13 作为运算符
as 运算符用于将某个值显式转换为给定的引用类型或可空值类型。 与强制转换表达式(§12.9.8)不同, as 运算符永远不会引发异常。 如果无法进行指明的转换,则结果值为 null。
在表单 E as T的操作中,E 应为表达式,T 应为引用类型、已知为引用类型的类型参数或可为 null 的值类型。 此外,至少应为下列其中一个,否则会发生编译时错误:
- 从 到 存在标识 (§10.2.2)、隐式可为 null (§10.2.6)、隐式引用 (§10.2.8)、装箱 (§10.2.9)、显式可为 null (§10.3.4)、显式引用 (
E),或封装 (T) 转换。 -
E或T的类型是一种开放类型。 -
E是null字面量。
如果 E 的编译时类型不是 dynamic,操作 E as T 生成的结果与...相同。
E is T ? (T)(E) : (T)null
不同的是 E 只计算一次。 预计编译器将对 E as T 进行优化,以便最多执行一次运行时类型检查,而不是上述扩展所隐含的两次运行时类型检查。
如果 E 的编译时类型是 dynamic,与强制转换运算符不同,as 运算符不会动态绑定 (§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 } }已知
T的类型参数G是引用类型,因为它具有类约束。 但是,U的类型参数H不是;因此不允许在as中使用H运算符。结束示例
12.14 逻辑运算符
12.14.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.14.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.14.3 枚举逻辑运算符
每个枚举类型 E 隐式提供以下预定义的逻辑运算符:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
计算 x «op» y的结果,其中 x 和 y 是枚举类型 E(具有基础类型 U)的表达式,并且 «op» 是其中一个逻辑运算符,其结果与计算 (E)((U)x «op» (U)y)的结果完全相同。 换句话说,枚举类型逻辑运算符只需对两个操作数的基础类型执行逻辑操作。
还预定义了上面定义的未提升预定义枚举逻辑运算符的提升 (§12.4.8) 形式。
12.14.4 布尔逻辑运算符
预定义的布尔逻辑运算符为:
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
如果 x & y 和 true 都为 x,则 y 的结果为 true。 否则,结果为 false。
如果 x | y 或 true 是 x,则 y 的结果是 true。 否则,结果为 false。
如果 x ^ y 是 true 且 x 是 true,或者 y 是 false 且 x 是 false,则 y 的结果是 true。 否则,结果为 false。 当操作数的类型为 bool时,^ 运算符将计算与 != 运算符相同的结果。
12.14.5 可为 Null 的布尔值和 |运营商
可以为 null 的布尔类型 bool? 可以表示三个值,true、false和 null。
与其他二进制运算符一样,逻辑运算符 & 的提升形式和 | (§12.14.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.15 条件逻辑运算符
12.15.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返回false且operator false返回false的状态。 在这些情况下,&&和||都不会短路。 尾注
如果条件逻辑运算符的操作数具有编译时类型 dynamic,则表达式将动态绑定(§12.3.3)。 在这种情况下,表达式的编译时类型是 dynamic,下面所述的解决方案将在运行时通过那些具有编译时类型 dynamic的操作数的运行时类型来进行。
在处理形式为 x && y 或 x || y 的运行时,将应用重载决策 (§12.4.5),就好像运算被写成 x & y 或 x | y。 那么:
- 如果重载解析未能找到单个最佳运算符,或者如果重载解析选择预定义的整数逻辑运算符之一或可为 null 的布尔逻辑运算符(§12.14.5),则会发生绑定时错误。
- 否则,如果所选运算符是预定义的布尔逻辑运算符之一(§12.14.4),则按照 §12.15.2 中所述处理该作。
- 否则,所选运算符是用户定义的运算符,并且按照 §12.15.3 中所述处理该作。
无法直接重载条件逻辑运算符。 但是,由于条件逻辑运算符根据常规逻辑运算符进行评估,因此常规逻辑运算符的重载具有某些限制,也被视为条件逻辑运算符的重载。 这在 §12.15.3 中进一步介绍。
12.15.2 布尔条件逻辑运算符
如果 && 或 || 的操作数类型为 bool,或者操作数类型未定义适用的 operator & 或 operator |,但确实定义了到 bool的隐式转换,则按以下方式处理操作:
- 运算
x && y的计算结果为x ? y : false。 换句话说,首先计算x并将其转换为类型bool。 然后,如果x是true,y将被计算并转换为类型bool,这将成为操作的结果。 否则,操作的结果为false。 - 运算
x || y的计算结果为x ? true : y。 换句话说,首先计算x并将其转换为类型bool。 然后,如果x是true,则操作的结果为true。 否则,y将被求值并转换为类型bool,而它将成为运算的结果。
12.15.3 用户定义的条件逻辑运算符
当 && 或 || 的操作数属于声明了适用的用户定义 operator & 或 operator | 的类型时,以下两项均应为 true,其中 T 是声明所选操作数的类型:
- 所选运算符的返回类型和每个参数的类型应为
T。 换言之,运算符应计算T类型的两个操作数的逻辑 AND 或逻辑 OR,并返回T类型的结果。 -
T应包含operator true和operator false的声明。
如果未满足上述任一要求,则会发生绑定时错误。 否则,计算 && 或 || 操作时,将用户定义的 operator true 或 operator false 与所选的用户定义运算符组合在一起。
- 操作
x && y计算为T.false(x) ? x : T.&(x, y),其中T.false(x)调用在operator false中声明的T,T.&(x, y)调用所选operator &。 换言之,首先对x进行求值,然后在结果上调用operator false以确定x是否肯定为 false。 然后,如果x绝对为 false,则操作的结果是之前为x计算的值。 否则,将对y进行评估,并在已经为operator &计算出的值和为x计算的新值上调用选定的y,以生成该操作的结果。 - 操作
x || y计算为T.true(x) ? x : T.|(x, y),其中T.true(x)调用在operator true中声明的T,T.|(x, y)调用所选operator |。 换句话说,首先评估x,然后对结果调用operator true,以确定x是否为真。 然后,如果x绝对正确,则操作的结果是之前为x计算的值。 否则,将对y进行评估,并在已经为operator |计算出的值和为x计算的新值上调用选定的y,以生成该操作的结果。
在这两种操作中,x 所给出的表达式只被求值一次,而 y 所给出的表达式要么不被求值,要么正好被求值一次。
12.16 Null 合并运算符
?? 运算符称为 null 合并运算符。
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
| throw_expression
;
在表单 a ?? b的 null 合并表达式中,如果 a 为非null,则结果为 a;否则,结果为 b。 只有当 b 是 a 时,运算才会对 null 求值。
null 合并运算符是右关联运算符,这意味着运算是从右向左分组的。
示例:形式为
a ?? b ?? c的表达式会被作为a ?? (b ?? c)进行求值。 一般情况下,E1 ?? E2 ?? ... ?? EN形式的表达式返回非null操作数的第一个操作数,如果所有操作数都null,则返回null。 结束示例
表达式的类型 a ?? b 取决于操作数上可用的隐式转换。 根据首选项,a ?? b 的类型为 A₀、A或 B,其中 A 是 a 类型(前提是 a 具有类型),B 是 b的类型(前提是 b 具有类型),A₀ 是 A 的基础类型(如果 A 为可为 null 的值类型,则为 A。 具体来说,a ?? b 的处理过程如下:
- 如果
A存在并且是非托管类型(§8.8)或已知为不可为 null 的值类型,则会发生编译时错误。 - 否则,如果
A存在并且b是动态表达式,则结果类型dynamic。 在运行时,首先对a进行求值。 如果a不是null,那么a转换为dynamic,并且这将成为结果。 否则,将对b进行求值,并将其作为结果。 - 否则,如果
A存在并且是可为 null 的值类型,并且存在从b到A₀的隐式转换,则结果类型A₀。 在运行时,首先对a进行求值。 如果a不是null,则a被解包为类型A₀,最终结果即为此。 否则,b将被求值并转换为类型A₀,并将其作为结果。 - 否则,如果存在
A,并且存在从b到A的隐式转换,则结果类型为A。 在运行时,首先对a进行求值。 如果a不等于null,则a成为结果。 否则,b将被求值并转换为类型A,并将其作为结果。 - 否则,如果
A存在并且是可为 null 的值类型,则b具有类型B,并且存在从A₀到B的隐式转换,则结果类型B。 在运行时,首先对a进行求值。 如果a不是null,那么将a解包为类型A₀并转换为类型B,结果即为如此。 否则,将对b求值,并将其作为结果。 - 否则,如果
b具有类型B,并且存在从a到B的隐式转换,则结果类型为B。 在运行时,首先对a进行求值。 如果a不是null,a转换为类型B,并成为结果。 否则,将对b求值,并将其作为结果。 - 否则,
a和b不兼容,并且会发生编译时错误。
示例:
T M<T>(T a, T b) => a ?? b; string s = M(null, "text"); int i = M(19, 23);方法
T的类型参数M不受约束。 因此,类型参数可以是引用类型,也可以是可为 null 的值类型,如第一次调用M所示。 类型参数还可以是不可为 null 的值类型,如第二次调用M中所示。 当类型参数为不可为 null 的值类型时,表达式a ?? b的值为a。结束示例
12.17 引发表达式运算符
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 转换来转换为各种类型。
throw expression 只能出现在以下语法上下文中:
- 作为三元条件运算符 (
?:) 的第二个或第三个操作数。 - 作为 null 合并运算符 (
??) 的第二个操作数。 - 作为表达式的 lambda 或成员的主体。
12.18 声明表达式
声明表达式声明局部变量。
declaration_expression
: local_variable_type identifier
;
local_variable_type
: type
| 'var'
;
如果简单名称查找找不到关联的声明(§12.8.4),则 _ 也被视为声明表达式。 作为声明表达式使用时,_ 被称为 simple discard。 它在语义上等效于 var _,但允许在更多地方使用。
声明表达式应仅在以下语法上下文中发生:
- 作为
out中的 argument_value。 - 作为一个简单的放弃
_,由简单赋值(§12.22.2.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能够访问i1和b1,它们已被引入到封闭范围中。
s2的声明显示了在嵌套调用i2时使用M的尝试,这是不允许的,因为引用发生在声明i2的参数列表中。 另一方面,允许在最终参数中引用b2,因为它出现在声明b2的嵌套参数列表结束之后。
s3的声明显示,隐式和显式类型的声明表达式都会被弃用。 由于丢弃不声明命名变量,因此允许_标识符的多次出现。(int i1, int _, (var i2, var _), _) = (1, 2, (3, 4), 5);本例展示了在解构赋值中对变量和弃码使用隐式和显式类型的声明表达式。 未找到 声明时,
_var _等效于_。void M1(out int i) { ... } void M2(string _) { M1(out _); // Error: `_` is a string M1(out var _); }本示例展示了当
var _不可用时,使用_提供隐式类型的丢弃,因为它指定了封闭范围中的一个变量。结束示例
12.19 条件运算符
?: 运算符称为条件运算符。 有时也称为三元运算符。
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
| null_coalescing_expression '?' 'ref' variable_reference ':'
'ref' variable_reference
;
形式为 b ? x : y 的条件表达式首先计算条件 b。 然后,如果 b 等于 true,则计算 x 并作为操作的结果。 否则,y 将被求值并成为运算结果。 条件表达式不会同时计算 x 和 y。
条件运算符是右关联运算符,这意味着运算是从右向左分组的。
示例:形式为
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,则 x 运算符的第二个和第三个操作数(y 和 ?:)控制条件表达式的类型:
- 如果
x具有类型X,并且y具有类型Y,- 如果存在标识转换,
XY则结果是一组表达式(§12.6.3.16)的最佳常见类型。 如果任一类型为dynamic,则类型推理更喜欢dynamic(§8.7)。 如果任一类型是元组类型(§8.3.11),则当两个元组中具有相同序号位置的元素名称匹配时,类型推理将包括元素名称。 - 否则,如果隐式转换(§10.2)从
X到Y存在,但不存在从Y到X,则Y是条件表达式的类型。 - 否则,如果隐式枚举转换(§10.2.4)从
X到Y存在,则Y是条件表达式的类型。 - 否则,如果隐式枚举转换(§10.2.4)从
Y到X存在,则X是条件表达式的类型。 - 否则,如果隐式转换(§10.2)从
Y到X存在,但不存在从X到Y,则X是条件表达式的类型。 - 否则,无法确定表达式类型,并且会发生编译时错误。
- 如果存在标识转换,
- 如果
x和y中只有一个有类型,并且x和y都能隐式转换为该类型,那么该类型就是条件表达式的类型。 - 否则,无法确定表达式类型,并且会发生编译时错误。
对形式为 b ? ref x : ref y 的 ref 条件表达式的运行时处理包括以下步骤:
- 首先,计算
b,并确定bool的b值:- 如果存在从
b类型到bool的隐式转换,则执行此隐式转换以生成bool值。 - 否则,将调用
operator true类型定义的b以生成bool值。
- 如果存在从
- 如果上述步骤生成的
bool值是true,那么评估x后生成的变量引用将成为条件表达式的结果。 - 否则,将对
y进行求值,由此产生的变量引用将成为条件表达式的结果。
表单 b ? x : y 的条件表达式的运行时处理包括以下步骤:
- 首先,计算
b,并确定bool的b值:- 如果存在从
b类型到bool的隐式转换,则执行此隐式转换以生成bool值。 - 否则,将调用
operator true类型定义的b以生成bool值。
- 如果存在从
- 如果上述步骤生成的
bool值为true,那么就计算x,并将其转换为条件表达式类型,从而得到条件表达式的结果。 - 否则,将对
y进行求值并转换为条件表达式的类型,成为条件表达式的结果。
12.20 匿名函数表达式
12.20.1 常规
匿名函数 是表示“内联”方法定义的表达式。 匿名函数本身没有值或类型,但可转换为兼容的委托或表达式树类型。 匿名函数转换的求值取决于转换的目标类型:如果是委托类型,则转换的求值结果是引用匿名函数定义的方法的委托值。 如果它是一个表达式树类型,那么转换会生成一个表达式树,该表达式树表示方法结构作为对象结构。
注意:出于历史原因,匿名函数有两种语法风格,即 lambda_expression和 anonymous_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_expression 和 expression 替代项均适用,则应选择前者。
注意:此处的替代项的重叠和优先级只是为了描述性便利;可以详细说明语法规则以删除重叠。 ANTLR 和其他语法系统采用相同的便利性,因此 anonymous_function_body 自动具有指定的语义。 尾注
注意:如果将某种语法形式(如 )视为
x?.M(),而M的结果类型为void(§12.8.13),则它将是一个错误。 但是,当作为 null_conditional_invocation_expression 处理时,结果类型允许为void。 尾注
示例:
List<T>.Reverse的结果类型是void。 在下面的代码中,匿名表达式的主体是 null_conditional_invocation_expression,因此它不是一个错误。Action<List<int>> a = x => x?.Reverse();结束示例
=> 运算符的优先级与赋值(=)相同,并且是右关联运算符。
具有修饰符的 async 匿名函数是异步函数,遵循 §15.14 中所述的规则。
以 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 表达式和 匿名方法表达式的行为相同:
- anonymous_method_expression 允许完全省略参数列表,从而可以转换为任何值参数列表的委托类型。
- lambda_expression允许省略和推断参数类型,而 anonymous_method_expression要求显式声明参数类型。
- lambda_expression 的主体可以是表达式或块,而 anonymous_method_expression 的主体应为块。
- 只有 lambda_expression 可以转换为兼容的表达式树类型 (§8.6)。
12.20.2 匿名函数签名
匿名函数的 anonymous_function_signature 定义了匿名函数的参数名称和可选的类型。 匿名函数的参数范围是 anonymous_function_body(§7.7)。 匿名方法主体与参数列表(如果给定)一起构成声明空间(§7.3)。 因此,如果匿名函数参数的名称与局部变量、局部常量或参数的名称相匹配,而这些变量、常量或参数的作用域包括 anonymous_method_expression 或 lambda_expression,则属于编译时错误。
如果一个匿名函数具有 explicit_anonymous_function_signature,那么兼容的委托类型和表达式树类型集将被限制为那些参数类型和修饰符按相同顺序排列的类型(§10.7)。 与方法组转换(§10.8)相比,不支持匿名函数参数类型的逆变。 如果匿名函数没有 anonymous_function_signature,那么兼容的委托类型和表达式树类型的范围将仅限于那些没有输出参数的类型。
请注意,anonymous_function_signature 不能包含属性或参数数组。 不过,anonymous_function_signature 可能与参数列表包含参数数组的委托类型兼容。
另请注意,即使兼容,转换到表达式树类型仍可能在编译时失败(§8.6)。
12.20.3 匿名函数主体
匿名函数的正文(表达式 或 块)受以下规则的约束:
- 如果匿名函数包含签名,则签名中指定的参数在正文中可用。 如果匿名函数没有签名,则可以转换为具有参数的委托类型或表达式类型(§10.7),但不能在正文中访问参数。
- 除了在最邻近的封闭匿名函数的签名(如有)中指定的按引用参数外,主体访问按引用参数属于编译时错误。
- 除了在最近的封闭匿名函数的签名(如有)中指定的参数外,主体访问
ref struct类型的参数是一个编译时错误。 - 当
this的类型为结构类型时,主体访问this会导致编译时错误。 无论访问是显式(如this.x)还是隐式(如x,中x为结构体的实例成员),这都是正确的。 此规则只是禁止此类访问,并不影响成员查找是否会产生结构成员的结果。 - 正文有权访问匿名函数的外部变量(§12.20.6)。 外部变量的访问将引用在计算 lambda_expression 或 anonymous_method_expression 时处于活动状态的变量实例(§12.20.7)。
- 如果正文包含
goto语句、break语句或continue语句,而这些语句的目标在主体之外或包含在匿名函数的主体之内,则属于编译时错误。 - 主体中的
return语句从调用最近的封闭匿名函数返回控制,而不是从外层函数成员返回控制。
除了求值和调用 lambda_expression 或 anonymous_method_expression 之外,是否还有其他方法来执行匿名函数的块,这一点没有明确说明。 具体而言,编译器可以选择通过合成一个或多个命名方法或类型来实现匿名函数。 任何此类合成元素的名称应是为编译器使用而保留的格式(§6.4.3)。
12.20.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参数,用于从列表项中提取要求和的值。 提取的值可以是int或double,生成的总和同样是int或double。例如,
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.UnitCount与Func<Detail,int>和Func<Detail,double>兼容。 不过,重载决策会选择第一个Sum方法,因为转换为Func<Detail,int>比转换为Func<Detail,double>更好。在
orderDetails.Sum的第二次调用中,只有第二个Sum方法适用,因为匿名函数d => d.UnitPrice * d.UnitCount生成double类型的值。 因此,重载决策会为该调用选择第二个Sum方法。结束示例
12.20.5 匿名函数和动态绑定
匿名函数不能是动态绑定操作的接收方、参数或操作数。
12.20.6 外部变量
12.20.6.1 常规
在 lambda_expression 或 anonymous_method_expression 的范围内的任何局部变量、值参数或参数数组都称为匿名函数的 外部变量。 在类的实例函数成员中,此值被视为值参数,并且是函数成员中包含的任何匿名函数的外部变量。
12.20.6.2 捕获的外部变量
当匿名函数引用外部变量时,据说外部变量已被匿名函数 捕获
示例:在示例中
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结束示例
当匿名函数捕获局部变量或值参数时,局部变量或参数不再被视为固定变量(§24.4),而是被视为可移动变量。 但是,捕获的外部变量不能在语句(fixed)中使用,因此无法获取捕获的外部变量的地址。
注意:与未捕获的变量不同,捕获的局部变量可以同时向多个执行线程公开。 尾注
12.20.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.20.7 匿名函数表达式的计算
匿名函数 F 应始终转换为委托类型 D 或表达式树类型 E,直接或通过执行委托创建表达式 new D(F)。 此转换确定匿名函数的结果,如 §10.7中所述。
12.20.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.20.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.21 查询表达式
12.21.1 概述
查询表达式 为类似于 SQL 和 XQuery 等关系查询和分层查询语言的查询提供语言集成的语法。
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clause* select_or_group_clause query_continuation?
;
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 子句开头,以 select 或 group 子句结尾。 初始 from 子句后面可以有零或多个 from、let、where、join 或 orderby 子句。 每个 from 子句都是一个生成器,它引入了一个 范围变量,该变量的范围位于 序列的元素上。 每个 let 子句都会引入一个范围变量,该变量表示由以前的范围变量计算的值。 每个 where 子句都是一个筛选器,用于从结果中排除项目。 每个 join 子句将源序列的指定键与其他序列的键进行比较,从而生成匹配对。 每个 orderby 子句根据指定的条件对项重新排序。最终 select 或 group 子句指定结果的形状(以范围变量为单位)。 最后,通过将一个查询的结果视为后续查询中的生成器,可以使用 into 子句来“连接”查询。
12.21.2 查询表达式中的歧义性
查询表达式使用许多上下文关键字(§6.4.4):ascending、by、descending、equals、from、group、into、join、let、on、orderby、select 和 where。
为了避免使用这些标识符作为关键字和简单名称而引起的歧义,这些标识符在查询表达式中的任何位置都被视为关键字,除非它们以“@”(§6.4.4)作为前缀,在这种情况下,这些标识符被视为标识符。 为此,查询表达式是以“from标识符”开头,并且后接除了“;”、“=”或“,”之外的任何标记的表达式。
12.21.3 查询表达式转换
12.21.3.1 常规
C# 语言未指定查询表达式的执行语义。 相反,查询表达式将转换为遵循查询表达式模式的方法的调用(§12.21.4)。 具体而言,查询表达式将转换为名为 Where、Select、SelectMany、Join、GroupJoin、OrderBy、OrderByDescending、ThenBy、ThenByDescending、GroupBy和 Cast的方法调用。 这些方法应具有特定的签名和返回类型,如 §12.21.4 中所述。 这些方法可以是要查询的对象实例方法或对象外部的扩展方法。 这些方法实现查询的实际执行。
从查询表达式到方法调用的转换是在执行任何类型绑定或重载解析之前发生的语法映射。 查询表达式转换后,生成的方法调用将作为常规方法调用进行处理,这反过来可能会发现编译时错误。 这些错误条件包括但不限于不存在的方法、错误的类型的参数以及类型推理失败的泛型方法。
通过反复应用以下转换来处理查询表达式,直到无法进一步简化。 各项翻译按应用顺序列出:每个部分假定前面各节的翻译已完全完成,一旦完成,在处理同一个查询表达式时不会再次涉及该部分。
查询表达式如果包含给范围变量赋值,或者使用范围变量作为引用或输出参数,那么会导致编译时错误。
某些转换会注入带有透明标识符(以 * 表示)的范围变量。 这些内容在 §12.21.3.8 中进一步介绍。
12.21.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.21.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.21.3.4 退化查询表达式
查询表达式为
from «x» in «e» select «x»
被转换为
( «e» ) . Select ( «x» => «x» )
示例:示例
from c in customers select c被转换为
(customers).Select(c => c)结束示例
退化的查询表达式是一个简单选择源元素的表达式。
注意:翻译(§12.21.3.6 和 §12.21.3.7)的后续阶段通过将转换步骤替换为源来删除其他翻译步骤引入的退化查询。 但是,请务必确保查询表达式的结果绝不是源对象本身。 否则,返回此类查询的结果可能会无意中向调用方公开私有数据(例如元素数组)。 因此,此步骤通过在源上显式调用
Select来保护直接在源代码中编写的退化查询。 由Select和其他查询运算符的实现者负责确保,这些方法永远不会返回源对象本身。 尾注
12.21.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 customers 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 })其中,
x和y是编译器生成的标识符,否则不可见且不可访问。结束示例
一个 orderby 子句及其前面的 from 子句:
from «x» in «e»
orderby «k1» , «k2» , ... , «kn»
...
被转换为
from «x» in ( «e» ) .
OrderBy ( «x» => «k1» ) .
ThenBy ( «x» => «k2» ) .
... .
ThenBy ( «x» => «kn» )
...
如果 ordering 子句指定了降序方向指示符,则会改为生成 OrderByDescending 或 ThenByDescending 调用。
示例:示例
from o in orders orderby o.Customer.Name, o.Total descending select o最终的转换为
(orders) .OrderBy(o => o.Customer.Name) .ThenByDescending(o => o.Total)结束示例
以下翻译假定没有 let、where、join 或 orderby 子句,并且每个查询表达式中至多只有一个初始 from 子句。
12.21.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.21.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.21.3.8 透明标识符
某些转换会注入带有透明标识符(以 * 表示)的范围变量。 透明标识符仅作为查询表达式转换过程中的中间步骤存在。
当查询翻译注入透明标识符时,进一步的翻译步骤会将透明标识符传播到匿名函数和匿名对象初始值设定项。 在这些上下文中,透明标识符具有以下行为:
- 当透明标识符(transparent identifier)作为匿名函数的参数出现时,关联的匿名类型的成员将自动在匿名函数的作用域内。
- 当具有透明标识符的成员处于范围中时,该成员的成员也处于范围中。
- 当透明标识符作为匿名对象初始值设定项中的成员声明符出现时,它会引入一个具有透明标识符的成员。
在上述翻译步骤中,透明标识符始终与匿名类型一起引入,目的是将多个范围变量捕获为单个对象的成员。 允许 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 })其中,
x和y是编译器生成的标识符,否则不可见且不可访问。 结束示例
12.21.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>之间的关系,以确保ThenBy和ThenByDescending方法仅适用于OrderBy或OrderByDescending的结果。 尾注
注意:推荐的
GroupBy结果的形态是一个序列的序列,其中每个内部序列都有一个额外的Key属性。 尾注
注意:由于查询表达式通过语法映射转换为方法调用,因此类型在实现任何或全部查询表达式模式的方式方面具有相当大的灵活性。 例如,模式的方法可以作为实例方法或扩展方法实现,因为两者具有相同的调用语法,并且方法可以请求委托或表达式树,因为匿名函数可转换为两者。 仅实现部分查询表达式模式的类型只支持映射到该类型支持的方法的查询表达式转换。 尾注
注释:
System.Linq命名空间为实现System.Collections.Generic.IEnumerable<T>接口的任何类型提供查询表达式模式的实现。 尾注
12.22 赋值运算符
12.22.1 常规
除了一个赋值运算符,所有赋值运算符都会向变量、属性、事件或索引器元素赋值。 例外 = ref 将变量引用 (§9.5) 赋值给引用变量 (§9.7)。
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '??='
| right_shift_assignment
;
赋值的左操作数应是被归类为变量的表达式,或(= ref 除外)属性访问、索引器访问、事件访问或元组。 声明表达式不能直接用作左操作数,但可以作为解构赋值的求值步骤。
= 运算符称为 简单赋值运算符。 它将右操作数的值分配给由左操作数指定的变量、属性、索引器元素或元组元素。 简单赋值运算符的左操作数不得是事件访问(§15.8.2 中描述的情况除外)。 简单赋值运算符在 §12.22.2 中介绍。
运算符 = ref 被称为 ref 赋值运算符。 它使右操作数成为左操作数指定的引用变量的参照,而右操作数应为 variable_reference (§9.5)。 ref 赋值运算符在 §12.22.3 中介绍。
除了=和= ref运算符以外,其他赋值运算符称为复合赋值运算符。 这些运算符的处理方式如下:
- 对于
??=运算符,只有当左操作数的值为null时,才会评估右操作数,并将结果分配给左操作数指定的变量、属性或索引器元素。 - 否则,对两个操作数执行指定的操作,然后将结果值分配给左侧操作数所代表的变量、属性或索引器元素。 复合赋值运算符在 §12.22.4 中介绍。
作为左操作数的事件访问表达式的 += 和 -= 运算符称为 事件赋值运算符。 以事件访问作为左操作数时,其他赋值运算符均无效。 事件赋值运算符在 §12.22.5 中介绍。
赋值运算符是右关联运算符,这意味着运算是从右向左分组的。
示例:形式为
a = b = c的表达式会被作为a = (b = c)进行求值。 结束示例
12.22.2 简单赋值
= 运算符称为简单赋值运算符。
如果简单赋值的左操作数是 E.P 或 E[Ei],并且 E 具有编译时类型 dynamic,则该赋值将动态绑定(§12.3.3)。 在这种情况下,赋值表达式的编译时类型是 dynamic。下面所述的解决方法将在运行时根据 E的运行时类型进行。 如果左侧操作数的格式为 E[Ei],其中至少有一个 Ei 元素具有编译时类型 dynamic,并且 E 的编译时类型不是数组,则生成的索引器访问是动态绑定的,但具有有限的编译时检查(§12.6.5)。
左操作数被归类为元组的简单赋值也称为析构赋值。 如果左侧操作数的任何元组元素具有元素名称,则会发生编译时错误。 如果左操作数的任何元组元素是 declaration_expression 而任何其他元素不是 declaration_expression 或简单丢弃,则会出现编译时错误。
简单赋值 x = y 的类型就是 x 的 y 赋值的类型,其递归确定方法如下:
- 如果
x是元组表达式(x1, ..., xn),且y可以被解构为具有(y1, ..., yn)个元素的元组表达式n(§12.7),并且对xi的每个赋值具有类型yi,那么赋值Ti的类型为(T1, ..., Tn)。 - 否则,如果将
x分类为变量,则变量不会readonly,x具有类型T,并且y隐式转换为T,则赋值具有类型T。 - 否则,如果将
x分类为隐式类型化变量(即隐式类型声明表达式),并且y具有类型T,则变量的推断类型T,并且赋值具有类型T。 - 否则,如果
x被分类为属性或索引器访问,则属性或索引器具有可访问的集访问器,x具有类型T,并且y隐式转换为T,则赋值具有类型T。 - 否则,分配无效且发生绑定时错误。
对类型为 x = y 的 T 形式的简单赋值的运行时处理,是作为对 x 类型 y 的 T 的赋值进行的,包括以下递归步骤:
- 如果
x还尚未求值,则会对其进行求值。 - 如果
x被归类为变量,则会计算y,并根据需要通过隐式转换(T)转换为 。 - 如果
x被归类为属性或索引器访问: - 如果
x被归类为具有元组数(x1, ..., xn)的元组n:-
y与n元素一起解构为元组表达式e。 - 通过使用隐式元组转换将
t转换为e来创建结果元组T。 - 从左到右依次对每个
xi执行赋值到xi的t.Itemi,但不再对xi进行求值。 - 赋值的结果是
t。
-
注意:如果
x的编译时间类型是dynamic,并且从y的编译时间类型到dynamic存在隐式转换,则不需要进行运行时解析。 尾注
注意:数组共同方差规则(§17.6)允许数组类型的值
A[]作为对数组类型的实例B[]的引用,前提是存在从B到A的隐式引用转换。 由于这些规则,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.X、p.Y、r.A和r.B的赋值,因为p和r是变量。 但是,在示例中Rectangle r = new Rectangle(); r.A.X = 10; r.A.Y = 10; r.B.X = 100; r.B.Y = 100;赋值全部无效,因为
r.A和r.B不是变量。结束示例
12.22.3 Ref 赋值
= ref 运算符被称为 ref 赋值运算符。
左操作数应是绑定到引用变量(§9.7)、引用参数(非 this)、输出参数或输入参数的表达式。 右操作数应是一个表达式,它能产生一个 variable_reference,从而指定一个与左操作数类型相同的值。
如果左操作数的 ref-safe-context (§9.7.2) 宽于右操作数的 ref-safe-context,则属于编译时错误。
右操作数应在 ref 赋值时确定赋值。
当左操作数绑定到输出参数时,如果在 ref 赋值运算符的开头未明确分配该输出参数,则会出现错误。
如果左操作数是可写的引用(即,它指定除 ref readonly 本地或输入参数以外的任何内容),则右操作数应为可写的 变量引用。 如果右操作数变量是可写入的,则左操作数可以是可写或只读的 ref。
该操作使左操作数成为右操作数变量的别名。 即使右操作数变量是可写入的,也可以将别名设置为只读。
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.22.4 复合赋值
如果复合赋值左操作数采用 E.P 或 E[Ei] 形式,其中 E 具有编译时类型 dynamic,则赋值将动态绑定(§12.3.3)。 在这种情况下,赋值表达式的编译时类型是 dynamic。下面所述的解决方法将在运行时根据 E的运行时类型进行。 如果左侧操作数的格式为 E[Ei],其中至少有一个 Ei 元素具有编译时类型 dynamic,并且 E 的编译时类型不是数组,则生成的索引器访问是动态绑定的,但具有有限的编译时检查(§12.6.5)。
a ??= b 等效于 (T) (a ?? (a = b)),其中 a 只计算一次,T 是 a 的类型,当 b 是动态类型时,否则 T 为 a ?? b 的类型。
否则,形式为x «op»= y的操作通过应用二元运算符重载解析(§12.4.5)来处理,就像该操作写为x «op» y一样。 则
- 如果所选运算符的返回类型可以隐式转换为
x类型,则该操作将被计算为x = x «op» y,但对于x仅计算一次。 - 否则,如果所选运算符是预定义运算符,如果所选运算符的返回类型显式转换为
x类型,并且如果y隐式转换为x类型或运算符为 shift 运算符,则将将操作计算为x = (T)(x «op» y),其中T为x类型, 只计算x一次。 - 否则,复合赋值无效,并且会发生绑定时错误。
术语“只计算一次”表示在对 x «op» y进行计算时,会暂时存储 x 的所有组成表达式的结果,然后在对 x进行赋值操作时重复使用这些结果。
示例:在赋值
A()[B()] += C()中,其中A是返回int[]的方法,B和C是返回int的方法,方法仅按顺序A、B、C调用一次。 结束示例
当复合赋值的左操作数是属性访问或索引器访问时,属性或索引器应同时具有 get 访问器和 set 访问器。 如果情况并非如此,则会发生绑定时错误。
上面的第二个规则允许在某些上下文中将 x «op»= y 评估为 x = (T)(x «op» y)。 规则存在,因此,当左侧操作数的类型为 sbyte、byte、short、ushort或 char时,预定义运算符可用作复合运算符。 即使这两个参数都是其中一种类型,预定义运算符也会生成类型 int的结果,如 §12.4.7.3中所述。 因此,如果不进行强制转换,就无法将结果赋值给左操作数。
预定义运算符规则的直观效果即如果 x «op»= y 和 x «op» y 均被允许,那么 x = 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» y或x = (T)(x «op» y),因此计算规则隐式涵盖了提升运算符。 尾注
12.22.5 事件分配
如果 a += or -= 运算符的左操作数被归类为事件访问,那么表达式的求值过程如下:
- 对事件访问的实例表达式(如有)进行求值。
- 对
+=或-=运算符的右操作数进行求值,如果需要,通过隐式转换 (§10.2) 转换为左操作数的类型。 - 调用事件的事件访问器,参数列表由上一步中计算的值组成。 如果运算符
+=,则调用 add 访问器;如果运算符-=,则调用 remove 访问器。
事件赋值表达式不生成值。 因此,事件赋值表达式仅在 statement_expression 上下文中有效(§13.7)。
12.23 表达式
expression 要么是 non_assignment_expression,要么是 assignment。
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: declaration_expression
| conditional_expression
| lambda_expression
| query_expression
;
12.24 常量表达式
常量表达式是应在编译时完全计算的表达式。
constant_expression
: expression
;
常量表达式应具有值 null 或下列类型之一:
-
sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string; - 枚举类型;或
- 引用类型的默认值表达式 (§12.8.21)。
常量表达式中只允许以下构造:
- 字面量(包括
null字面量)。 - 对
const类、结构和接口类型的成员的引用。 - 对枚举类型成员的引用。
- 对本地常量的引用。
- 圆括号内的子表达式,它们本身是常量表达式。
- 强制转换表达式。
-
checked和unchecked表达式。 -
nameof表达式。 - 预定义的
+、-、!(逻辑求反)和~一元运算符。 - 预定义的
+、-、*、/、%、<<、>>、&、|、^、&&、||、==、!=、<、>、<=和>=二进制运算符。 -
?:条件运算符。 -
!null 包容运算符 (§12.8.9)。 -
sizeof如果非托管类型是 §24.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)
- 枚举成员声明 (§20.4)
- 参数列表的默认参数(§15.6.2)
-
case语句 (switch) 的 标签。 -
goto case语句 (§13.10.4) - 包含初始值设定项的数组创建表达式(§12.8.17.4)中的维度长度。
- 属性 (§23)
- 在 constant_pattern (§11.2.3) 中
隐式常量表达式转换(§10.2.11)允许 int 类型的常量表达式转换为 sbyte、byte、short、ushort、uint或 ulong,前提是常量表达式的值在目标类型范围内。
12.25 布尔表达式
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。 运算符(?:)的控制条件表达式遵循与boolean_expression相同的规则,但由于运算符优先级的原因被归类为null_coalescing_expression。
需要 boolean_expressionE 才能生成 bool 类型的值,如下所示:
- 如果 E 可隐式转换为
bool则在运行时应用隐式转换。 - 否则,一元运算符重载分辨率(§12.4.4)用于在
operator true上查找E的唯一最佳实现,并在运行时应用该实现。 - 如果未找到此类运算符,则会发生绑定时错误。