12 个表达式
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.19)的正文中有效。
对于作为较大表达式的子表达式的表达式,使用说明的限制,结果也可以归类为下列值之一:
- 一个命名空间。 具有此分类的表达式只能显示为member_access(§12.8.7)的左侧。 在任何其他上下文中,分类为命名空间的表达式会导致编译时错误。
- 类型。 具有此分类的表达式只能显示为member_access(§12.8.7)的左侧。 在任何其他上下文中,分类为类型的表达式会导致编译时错误。
- 方法组,这是由成员查找(§12.5)生成的一组重载方法。 方法组可能具有关联的实例表达式和关联的类型参数列表。 调用实例方法时,计算实例表达式的结果将成为由
this
(§12.8.14) 表示的实例。 方法组在invocation_expression(§12.8.10)或delegate_creation_expression(§12.8.17.6)中允许,并且可以隐式转换为兼容的委托类型(§10.8)。 在任何其他上下文中,分类为方法组的表达式会导致编译时错误。 - 事件访问。 每个事件访问都有一个关联类型,即事件的类型。 此外,事件访问可能具有关联的实例表达式。 事件访问可能显示为运算符
-=
(§12.21.5)的+=
左操作数。 在任何其他上下文中,分类为事件访问的表达式会导致编译时错误。 调用实例事件访问器的访问器时,计算实例表达式的结果将成为由this
(§12.8.14) 表示的实例。 - 一个引发表达式,可以使用它几个上下文在表达式中引发异常。 可以通过隐式转换为任何类型来转换引发表达式。
通过执行 get 访问器或 set 访问器的调用,属性访问或索引器访问始终重新分类为值。 特定访问器由属性或索引器访问的上下文确定:如果访问是分配的目标,则会调用 set 访问器来分配新值(§12.21.2)。 否则,将调用 get 访问器以获取当前值(§12.2.2)。
实例访问器是实例的属性访问、实例上的事件访问或索引器访问。
12.2.2 表达式的值
涉及表达式的大多数构造最终都需要表达式来表示 值。 在这种情况下,如果实际表达式表示命名空间、类型、方法组或无任何内容,则会发生编译时错误。 但是,如果表达式表示属性访问、索引器访问或变量,则隐式替换属性、索引器或变量的值:
- 变量的值只是当前存储在变量标识的存储位置中的值。 变量在获取其值之前应被视为明确赋值(§9.4),否则会发生编译时错误。
- 通过调用属性的 get 访问器获取属性访问表达式的值。 如果该属性没有 get 访问器,则会发生编译时错误。 否则,将执行函数成员调用(§12.6.6),调用的结果将成为属性访问表达式的值。
- 通过调用索引器的 get 访问器来获取索引器访问表达式的值。 如果索引器没有 get 访问器,则会发生编译时错误。 否则,函数成员调用(§12.6.6)与与索引器访问表达式关联的参数列表执行,调用的结果将成为索引器访问表达式的值。
- 元组表达式的值通过将隐式元组转换 (§10.2.13) 应用于元组表达式的类型来获取。 获取没有类型的元组表达式的值是错误的。
12.3 静态绑定和动态绑定
12.3.1 常规
绑定 是根据表达式的类型或值(参数、操作数、接收器)确定操作引用的操作的过程。 例如,方法调用的绑定取决于接收方和参数的类型。 运算符的绑定取决于其操作数的类型。
在 C# 中,操作的绑定通常在编译时根据其子表达式的编译时类型确定。 同样,如果表达式包含错误,编译器会检测并报告该错误。 此方法称为静态绑定。
但是,如果表达式是 动态表达式 (即具有类型 dynamic
),则表示其参与的任何绑定都应基于其运行时类型,而不是编译时所参与的类型。 因此,此类操作的绑定将延迟到在程序运行期间执行操作的时间。 这称为动态绑定。
当操作动态绑定时,编译器将执行很少或没有检查。 相反,如果运行时绑定失败,错误会在运行时报告为异常。
C# 中的以下操作受绑定约束:
- 成员访问权限:
e.M
- 方法调用:
e.M(e₁,...,eᵥ)
- 委托调用:
e(e₁,...,eᵥ)
- 元素访问:
e[e₁,...,eᵥ]
- 对象创建:新建
C(e₁,...,eᵥ)
- 重载的一元运算符:
+
、、-
(!
仅逻辑求反)、~
、++
、--
、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
。 之所以发生这种情况,是因为该参数是动态表达式 , 其编译时类型是动态的。 因此,第三次调用的绑定时间是 运行时。end 示例
12.3.3 动态绑定
此子引用是信息性的。
动态绑定允许 C# 程序与动态对象进行交互,即不遵循 C# 类型系统的正常规则的对象。 动态对象可以是具有不同类型系统的其他编程语言中的对象,也可能是以编程方式设置的对象,以便为不同的操作实现自己的绑定语义。
动态对象实现其自己的语义的机制是实现定义的。 给定接口(再次定义实现)由动态对象实现,以向 C# 运行时发出信号,这些对象具有特殊的语义。 因此,每当动态对象上的操作进行动态绑定时,其自己的绑定语义(而不是此规范中指定的 C# 的语义)接管。
虽然动态绑定的目的是允许与动态对象进行互操作,但 C# 允许对所有对象进行动态绑定,无论它们是否是动态的。 这允许更流畅地集成动态对象,因为对其操作的结果可能不是动态对象,但在编译时仍属于程序员未知的类型。 此外,即使没有涉及任何对象是动态对象,动态绑定也有助于消除基于错误的基于反射的代码。
12.3.4 子表达式的类型
当操作静态绑定时,子表达式的类型(例如接收方和参数、索引或操作数)始终被视为该表达式的编译时类型。
动态绑定操作时,子表达式的类型根据子表达式的编译时类型的不同方式确定:
- 编译时类型动态的子表达式被视为具有表达式在运行时计算结果的实际值的类型
- 编译时类型为类型参数的子表达式被视为具有类型参数在运行时绑定到的类型
- 否则,子表达式被视为具有其编译时类型。
12.4 运算符
12.4.1 常规
表达式是在操作数和运算符的基础之上构造而成。 表达式的运算符指明了向操作数应用的运算。
示例:运算符的示例包括
+
、-
、*
、/
和new
。 操作数的示例包括文本、字段、局部变量和表达式。 end 示例
有三种类型的运算符:
- 一元运算符。 一元运算符采用一个操作数,并使用前缀表示法(如
–x
)或后缀表示法(例如x++
)。 - 二进制运算符。 二进制运算符采用两个操作数,所有操作数都使用不合符表示法(例如
x + y
)。 - 三元运算符。 只有一个三元运算符存在
?:
;它采用三个操作数并使用虚数表示法(c ? x : y
)。
表达式中运算符的计算顺序取决于 运算符的优先级 和 关联性 (§12.4.2)。
表达式中的操作数从左到右计算。
示例:使用
F(i) + G(i++) * H(i)
旧值调用方法F
,然后使用旧值i
i
调用方法G
,最后,使用 i 的新值调用方法H
。 这与运算符优先级不同,与运算符优先级无关。 end 示例
特定的运算符可重载。 运算符重载(§12.4.3)允许为一个或多个操作数属于用户定义的类或结构类型的操作指定用户定义的运算符实现。
12.4.2 运算符优先级和关联性
如果某个表达式包含多个运算符,则运算符的优先顺序控制各个运算符的计算顺序。
注意:例如,表达式
x + y * z
的计算方式x + (y * z)
为,因为*
运算符的优先级高于二进制+
运算符。 end note
运算符的优先级由其关联的文法产生式的定义来确定。
注意:例如,additive_expression由或
-
运算符分隔+
的multiplicative_expression序列组成,从而为+
-
优先级低于*
和/
运算符的运算符和%
运算符。 end note
注意:下表按优先级从高到低的顺序汇总了所有运算符:
第 类别 运算符 §12.8 主要 x.y
x?.y
f(x)
a[x]
a?[x]
x++
x--
x!
new
typeof
default
checked
unchecked
delegate
stackalloc
§12.9 一元 +
-
!x
~
++x
--x
(T)x
await x
§12.10 乘法性的 *
/
%
§12.10 累加性 +
-
§12.11 Shift <<
>>
§12.12 关系和类型测试 <
>
<=
>=
is
as
§12.12 等式 ==
!=
§12.13 逻辑与 &
§12.13 逻辑“异或” ^
§12.13 逻辑或 \|
§12.14 条件“与” &&
§12.14 条件“或” \|\|
§12.15 和 §12.16 Null 合并和引发表达式 ??
throw x
§12.18 条件 ?:
§12.21 和 §12.19 赋值和 lambda 表达式 =
= ref
*=
/=
%=
+=
-=
<<=
>>=
&=
^=
\|=
=>
end note
如果操作数两边的两个运算符的优先级相同,那么运算符的结合性决定了运算的执行顺序:
- 除赋值运算符和 null 合并运算符外,所有二进制运算符都是 左关联运算符,这意味着从左到右执行操作。
示例:
x + y + z
计算为(x + y) + z
. end 示例 - 赋值运算符、null 合并运算符和条件运算符 (
?:
) 是 右关联运算符,这意味着从右到左执行运算。示例:
x = y = z
计算为x = (y = z)
. end 示例
可以使用括号控制优先级和结合性。
示例:
x + y * z
首先乘y
z
以结果,然后将结果x
添加到,但(x + y) * z
首先添加x
结果,y
然后乘以z
结果。 end 示例
12.4.3 运算符重载
所有一元运算符和二进制运算符都具有预定义的实现。 此外,还可以通过在类和结构中包含运算符声明(§15.10)来引入用户定义的实现。 用户定义的运算符实现始终优先于预定义运算符实现:只有不存在适用的用户定义运算符实现时,才会考虑预定义运算符实现,如 §12.4.4 和 §12.4.5 中所述。
可重载的一元运算符包括:
+ - !
(仅逻辑否定) ~ ++ -- true false
注意:虽然
true
表达式中未显式使用(false
因此未包含在 §12.4.2 中的优先表中),但它们被视为运算符,因为它们是在多个表达式上下文中调用的:布尔表达式(§12.24)和涉及条件逻辑运算符(§12.18)和条件逻辑运算符(§12.14)。 end note
注意:null 放弃运算符(后缀
!
, §12.8.9)不是可重载运算符。 end note
可重载的二进制运算符包括:
+ - * / % & | ^ << >> == != > < <= >=
只能重载上面列出的运算符。 具体而言,无法重载成员访问、方法调用或=
、、、||
、checked
??
new
typeof
?:
=>
unchecked
default
和is
as
运算符。 &&
重载二进制运算符时,相应的复合赋值运算符(如果有)也会隐式重载。
示例:运算符
*
重载也是运算符*=
的重载。 这在 §12.21 中进一步介绍。 end 示例
赋值运算符本身 (=)
不能重载。 赋值始终将值简单存储到变量(§12.21.2)。
强制转换操作(如 (T)x
)通过提供用户定义的转换(§10.5)来重载。
注意:用户定义的转换不会影响或
as
运算符的行为is
。 end note
元素访问(例如 a[x]
)不被视为可重载运算符。 而是通过索引器(§15.9)支持用户定义的索引编制。
在表达式中,运算符是使用运算符表示法引用的,在声明中,运算符是使用功能表示法引用的。 下表显示了一元运算符和二元运算符的运算符与功能表示法之间的关系。 在第一个条目中,«op» 表示任何可重载的一元前缀运算符。 在第二个条目中,«op» 表示一元后缀 ++
和 --
运算符。 在第三个条目中,«op» 表示任何可重载的二进制运算符。
注意:有关重载
++
和--
运算符的示例,请参阅 §15.10.2。 end note
运算符表示法 | 功能表示法 |
---|---|
«op» x |
operator «op»(x) |
x «op» |
operator «op»(x) |
x «op» y |
operator «op»(x, y) |
用户定义的运算符声明始终要求至少一个参数属于包含运算符声明的类或结构类型。
注意:因此,用户定义的运算符不可能具有与预定义运算符相同的签名。 end note
用户定义的运算符声明不能修改运算符的语法、优先级或关联性。
示例:运算符
/
始终是二进制运算符,始终具有 §12.4.2 中指定的优先级别,并且始终是左关联。 end 示例
注意:尽管用户定义的运算符可以执行它应得到的任何计算,但产生结果的实现不是直观预期的结果,但强烈建议不要这样做。 例如,运算符
==
的实现应比较两个操作数是否相等,并返回适当的bool
结果。 end note
§12.9 到 §12.21 中各个运算符的说明指定运算符的预定义实现以及适用于每个运算符的任何其他规则。 这些说明使用术语一元运算符重载解析、二元运算符重载解析、数值提升和提升运算符定义,这些定义在以下子项中找到。
12.4.4 一元运算符重载分辨率
窗体 «op» x
的操作,或者 x «op»
,其中 «op» 是可重载的一元运算符,并且 x
是类型的 X
表达式,按如下所示进行处理:
- 为操作提供的
X
候选用户定义运算符集是使用 §12.4.6 的规则确定的。operator «op»(x)
- 如果候选用户定义运算符集不为空,则这将成为操作的候选运算符集。 否则,预定义的二进制
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
由提供的Y
运算符«op»Y
的返回类型与提供的X
返回类型«op»X
相同,并且操作数类型的标识转换到相应的操作数类型«op»Y
«op»X
,则仅在«op»X
集中发生。
- 如果
- 如果候选用户定义运算符集不为空,则这将成为操作的候选运算符集。 否则,预定义的二进制
operator «op»
实现(包括其提升的表单)将成为操作的候选运算符集。 给定运算符的预定义实现在运算符的说明中指定。 对于预定义枚举和委托运算符,唯一考虑的运算符是枚举或委托类型提供的运算符,该枚举或委托类型是其中一个操作数的绑定时类型。 - §12.6.4 的重载解析规则将应用于候选运算符集,以选择与参数列表
(x, y)
相关的最佳运算符,此运算符将成为重载解析过程的结果。 如果重载解析无法选择单个最佳运算符,则会发生绑定时错误。
12.4.6 候选用户定义运算符
给定类型和 T
操作 operator «op»(A)
,其中 «op» 是可重载运算符,并且 A
是参数列表,则由运算符提供的 T
候选用户定义运算符 «op»(A)
集按如下所示确定:
- 确定类型
T₀
。 如果T
为可为 null 的值类型,T₀
则为其基础类型;否则T₀
等于T
。 - 对于此类运算符的所有
operator «op»
声明T₀
和所有提升形式,如果至少一个运算符适用于参数列表A
(§12.6.4.2),则候选运算符集由所有此类适用运算符T₀
组成。 - 否则,如果是
T₀
object
,则候选运算符集为空。 - 否则,提供的
T₀
候选运算符集是由直接基类T₀
提供的一组候选运算符,或者是类型T₀
参数的有效基类T₀
。
12.4.7 数字提升
12.4.7.1 常规
此子引用是信息性的。
§12.4.7 及其子项是合并效果的摘要:
数值提升包括自动执行预定义一元和二进制数值运算符的操作数的某些隐式转换。 数字提升不是一种独特的机制,而是将重载解析应用于预定义运算符的效果。 数值提升特别不会影响用户定义运算符的计算,尽管可以实现用户定义的运算符以显示类似的效果。
作为数字提升的示例,请考虑二进制 *
运算符的预定义实现:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
float operator *(float x, float y);
double operator *(double x, double y);
decimal operator *(decimal x, decimal y);
当重载解析规则(§12.6.4)应用于这组运算符时,效果是选择从操作数类型中存在隐式转换的第一个运算符。
示例:对于操作
b * s
,其中 ab
byte
和s
是一个short
,重载分辨率选择operator *(int, int)
为最佳运算符。 因此,效果是b
,并s
转换为int
,结果的类型为int
。 同样,对于操作i * d
,其中是i
一个int
且d
是一个double
,overload
分辨率选择operator *(double, double)
为最佳运算符。 end 示例
信息性文本的结尾。
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
,而另一个操作数的类型,或者int
两个操作数sbyte
short
都转换为类型long
。 - 否则,如果任一操作数的类型为类型
uint
,则另一个操作数将转换为类型uint
。 - 否则,这两个操作数将转换为类型
int
。
注意:第一个规则禁止将类型与
double
类型和float
类型混合decimal
的任何操作。 规则遵循以下事实:类型与double
float
类型之间decimal
没有隐式转换。 end note
注意:另一个操作数为有符号整型时,无法使操作数
ulong
的类型。 原因是不存在可表示全范围ulong
和有符号整型类型的整型。 end note
在上述两种情况下,都可以使用强制转换表达式将一个操作数显式转换为与另一个操作数兼容的类型。
示例:在以下代码中
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);
end 示例
信息性文本的结尾。
12.4.8 提升运算符
提升运算符 允许对不可为 null 的值类型进行操作的预定义运算符和用户定义的运算符也可用于这些类型的可为 null 形式。 提升运算符是从满足特定要求的预定义运算符和用户定义的运算符构造的,如下所述:
- 对于一元运算符、、、(
--
!
逻辑求反)和~
运算符的提升形式,如果操作数和结果类型都是不可为 null 的值类型,则存在运算符的提升形式。-
++
+
提升的窗体是通过向操作数和结果类型添加单个?
修饰符来构造的。 如果操作数为null
,则提升运算符将生成一个null
值。 否则,提升的运算符解包操作数、应用基础运算符并包装结果。 - 对于二进制运算符
+
,-
&
*
%
>>
/
|
^
<<
如果操作数和结果类型都是不可为 null 的值类型,则存在运算符的提升形式。 提升的窗体是通过向每个操作数和结果类型添加单个?
修饰符来构造的。 如果一个或两个操作数都是null
(异常是&
类型的运算符和|
运算符bool?
,如 §12.13.5 中所述),则提升运算符将生成一个null
值。 否则,提升的运算符解包操作数、应用基础运算符并包装结果。 - 对于相等运算符
==
,如果!=
操作数类型既是不可为 null 的值类型,又是结果类型bool
,则存在运算符的提升形式。 提升的窗体是通过向每个操作数类型添加一?
个修饰符来构造的。 提升运算符将两null
个null
值视为相等,值与任何非null
值不相等。 如果两个操作数都是非null
操作数,则提升的运算符会解包操作数,并应用基础运算符来生成bool
结果。 - 对于关系运算符
<
,>=
>
<=
如果操作数类型既是不可为 null 的值类型,也是结果类型bool
,则存在运算符的提升形式。 提升的窗体是通过向每个操作数类型添加一?
个修饰符来构造的。 如果一个或两个操作数均为null
,则提升运算符将生成值false
。 否则,提升的运算符会解包操作数,并应用基础运算符来生成bool
结果。
12.5 成员查找
12.5.1 常规
成员查找是指确定类型上下文中名称的含义的过程。 成员查找可以作为计算表达式中simple_name(§12.8.4)或member_access(§12.8.7)的一部分进行。 如果simple_name或member_access作为invocation_expression(§12.8.10.2)的primary_expression发生,则表示将调用该成员。
如果成员是方法或事件,或者它是委托类型(§20)或类型dynamic
(§8.2.4)的常量、字段或属性,则表示该成员可调用。
成员查找不仅考虑成员的名称,还考虑成员具有的类型参数数以及成员是否可访问。 出于成员查找的目的,泛型方法和嵌套泛型类型具有各自的声明中指示的类型参数数,所有其他成员都具有零类型参数。
处理类型中T
具有K
类型参数的名称N
的成员查找,如下所示:
- 首先,确定一组命名
N
的可访问成员: - 接下来,如果
K
为零,则删除其声明包括类型参数的所有嵌套类型。 如果K
不是零,则删除具有不同类型参数数量的所有成员。 如果K
为零,则不会删除具有类型参数的方法,因为类型推理过程(§12.6.3)可能能够推断类型参数。 - 接下来,如果调用该成员,则会从集中删除所有不可调用的成员。
- 接下来,从集中删除其他成员隐藏的成员。 对于集中的每个成员
S.M
,其中S
声明成员M
的类型,将应用以下规则:- 如果
M
常量、字段、属性、事件或枚举成员,则从集中删除在基类型S
中声明的所有成员。 - 如果是
M
类型声明,则会从集中删除基类型S
中声明的所有非类型声明,并且所有类型声明的类型参数M
与基类型中声明的相同数目的所有类型S
声明都将从集中删除。 - 如果
M
为方法,则会从集中删除在基类型S
中声明的所有非方法成员。
- 如果
- 接下来,类成员隐藏的接口成员将从集中删除。 此步骤仅在类型参数
T
且同时具有非空有效接口集(§15.2.5)object
的有效基类时才T
有效。 对于集中的每个成员S.M
,其中S
声明成员M
的类型,如果S
不是类声明object
,则应用以下规则:- 如果
M
常量、字段、属性、事件、枚举成员或类型声明,则会从集中删除接口声明中声明的所有成员。 - 如果
M
为方法,则会从集中删除接口声明中声明的所有非方法成员,并且从集中删除具有相同签名的所有具有相同签名M
的方法。
- 如果
- 最后,删除隐藏成员后,将确定查找结果:
- 如果集由不是方法的单个成员组成,则此成员是查找的结果。
- 否则,如果集仅包含方法,则此组方法是查找的结果。
- 否则,查找不明确,并且会发生绑定时错误。
对于类型参数和接口以外的类型中的成员查找,以及严格单一继承的接口中的成员查找(继承链中的每个接口只有零个或一个直接基接口),查找规则的效果只是派生成员隐藏具有相同名称或签名的基成员。 这种单继承查找从来就不明确。 §18.4.6 中介绍了可能由多继承接口中的成员查找产生的歧义。
注意:此阶段仅考虑一种歧义。 如果成员查找结果在方法组中,则方法组的进一步使用可能会由于歧义而失败,例如,如 §12.6.4.1 和 §12.6.6.2 中所述。 end note
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)。 end note
- 如果为class_type,则基类型
T
为基类T
,包括类类型object
。T
- 如果
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
value
y
指示分类为变量或值的表达式,T
指示分类为类型的表达式,F
是方法的简单名称,是P
属性的简单名称。
构造 示例 说明 方法调用 F(x, y)
重载解析应用于在包含类或结构中选择最佳方法 F
。 使用参数列表(x, y)
调用该方法。 如果方法不是static
,则实例表达式为this
。T.F(x, y)
重载解析应用于在类或结构 T
中选择最佳方法F
。 如果方法不是static
,则会发生绑定时错误。 使用参数列表(x, y)
调用该方法。e.F(x, y)
重载解析应用于选择类、结构或接口中由类型 e
给出的最佳方法F
。 如果方法为static
.,则会发生绑定时错误。 该方法使用实例表达式e
和参数列表(x, y)
调用。和 P
调用包含类或结构中属性 P
的 get 访问器。 如果P
仅写入,则会发生编译时错误。 如果P
不是static
,则实例表达式为this
。P = value
使用参数列表 (value)
调用包含类或结构中属性P
的 set 访问器。 如果P
为只读,则会发生编译时错误。 如果P
不是static
,则实例表达式为this
。T.P
调用类或结构 T
中属性P
的 get 访问器。 如果P
不是static
或仅P
写入,则会发生编译时错误。T.P = value
使用参数列表 (value)
调用类或结构T
中属性P
的 set 访问器。 如果P
不是static
或只读P
,则会发生编译时错误。e.P
使用实例表达式 e
调用类、结构或接口E
中属性P
的 get 访问器。 如果P
为static
或仅P
写入,则会发生绑定时错误。e.P = value
使用实例表达式 e
和参数列表(value)
调用类、结构或接口E
中属性P
的 set 访问器。 如果P
为static
只读或只读P
,则会发生绑定时错误。事件访问 E += value
调用包含类或结构中事件的 E
add 访问器。 如果E
不是static
,则实例表达式为this
。E -= value
调用包含类或结构中事件的 E
remove 访问器。 如果E
不是static
,则实例表达式为this
。T.E += value
调用类或结构 T
中事件的E
add 访问器。 如果E
不是static
,则会发生绑定时错误。T.E -= value
调用类或结构 T
中事件的E
remove 访问器。 如果E
不是static
,则会发生绑定时错误。e.E += value
使用实例表达式 e
调用类、结构或接口E
中事件的E
add 访问器。 如果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)
调用实例构造函数。end note
12.6.2 参数列表
12.6.2.1 常规
每个函数成员和委托调用都包含一个参数列表,该列表为函数成员的参数提供实际值或变量引用。 指定函数成员调用的参数列表的语法取决于函数成员类别:
- 例如,构造函数、方法、索引器和委托,参数指定为 argument_list,如下所示。 对于索引器,在调用 set 访问器时,参数列表还包含指定为赋值运算符右操作数的表达式。
注意:此附加参数不用于重载解析,仅在调用 set 访问器期间。 end note
- 对于属性,在调用 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_name的参数是一个位置参数。
argument_value可以采用以下形式之一:
- 一个表达式,指示参数作为值参数传递或转换为输入参数,然后作为该参数传递,由 (§12.6.4.2.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.3)。 变量必须明确分配(§9.4),然后才能将其作为引用参数传递。out
关键字后跟variable_reference(§9.5),指示参数作为输出参数传递(§15.6.2.3.4)。 变量在函数成员调用后被视为绝对赋值(§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
为第三个位置),因此应命名以下参数。 换句话说,仅当名称和位置结果查找相同的相应参数时,才允许使用非尾随命名参数。 end note
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。end 示例
对于输入、输出或引用参数,将计算变量引用,生成的存储位置将成为函数成员调用中的参数所表示的存储位置。 对于输入或引用参数,应在方法调用点明确分配变量。 如果变量引用作为输出参数提供,或者是reference_type的数组元素,则执行运行时检查以确保数组的元素类型与参数的类型相同。 如果此检查失败,则会引发 a
System.ArrayTypeMismatchException
。
注意:由于数组协变(§17.6),需要此运行时检查。 end note
示例:在以下代码中
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
。end 示例
方法、索引器和实例构造函数可能将其最右的参数声明为参数数组(§15.6.2.4)。 此类函数成员以正常形式或扩展形式调用,具体取决于适用的格式(§12.6.4.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
end 示例
当具有参数数组的函数成员在其扩展形式中使用至少一个扩展参数调用时,将像在扩展参数周围插入具有数组初始值设定项的数组创建表达式(§12.8.17.5)一样处理调用。 如果没有参数数组的参数,则传递空数组;无论传递的引用是新分配的还是现有的空数组,都未指定。
示例:给定声明
void F(int x, int y, params object[] args);
方法扩展形式的以下调用
F(10, 20, 30, 40); F(10, 20, 1, "hello", 3.0);
与
F(10, 20, new object[] { 30, 40 }); F(10, 20, new object[] { 1, "hello", 3.0 });
end 示例
当从具有相应可选参数的函数成员中省略参数时,将隐式传递函数成员声明的默认参数。 (这可以涉及创建存储位置,如上所述。
注意:由于这些始终是常量,因此其计算不会影响剩余参数的计算。 end note
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
自变量确定到方法。end 示例
类型推理在方法调用(§12.8.10.2)的绑定时处理过程中发生,在调用的重载解析步骤之前发生。 当在方法调用中指定特定方法组,并且没有将类型参数指定为方法调用的一部分时,类型推理将应用于方法组中的每个泛型方法。 如果类型推理成功,则推断的类型参数用于确定后续重载解析的参数类型。 如果重载解析选择泛型方法作为要调用的方法,则推断的类型参数将用作调用的类型参数。 如果特定方法的类型推理失败,则该方法不参与重载解析。 类型推理失败本身不会导致绑定时错误。 但是,当重载解析之后找不到任何适用的方法时,它通常会导致绑定时错误。
如果每个提供的参数都与方法(§12.6.2.2.2)中的一个参数不对应,或者没有相应的参数,则推理会立即失败。 否则,假定泛型方法具有以下签名:
Tₑ M<X₁...Xᵥ>(T₁ p₁ ... Tₓ pₓ)
通过表单M(E₁ ...Eₓ)
的方法调用,类型推理的任务是查找每个类型参数X₁...Xᵥ
的唯一类型参数S₁...Sᵥ
,以便调用M<S₁...Sᵥ>(E₁...Eₓ)
生效。
下面将类型推理的过程描述为算法。 如果符合性编译器在所有情况下都达到相同的结果,则可以使用替代方法实现符合性编译器。
在推理过程中,每个类型参数都固定到特定类型Sᵢ
或未固定到关联的边界集。 Xᵢ
每个边界都是某种类型T
。 最初,每个类型变量 Xᵢ
都未固定一组空边界。
类型推理分阶段进行。 每个阶段将尝试根据上一阶段的发现推断更多类型变量的类型参数。 第一阶段对边界进行一些初始推理,而第二阶段将类型变量修复为特定类型并推断进一步边界。 第二阶段可能需要重复多次。
注意:类型推理也用于其他上下文,包括用于方法组的转换(§12.6.3.14),并查找一组表达式的最佳常见类型(§12.6.3.15)。 end note
12.6.3.2 第一阶段
对于每个方法参数 Eᵢ
:
- 如果
Eᵢ
为匿名函数,则从Eᵢ
到的显式参数类型推理 (§12.6.3.8)Tᵢ
- 否则,如果
Eᵢ
具有类型U
,并且相应的参数是值参数(§15.6.2.2),则从中Tᵢ
生成U
下限推理(§12.6.3.10)。 - 否则,如果
Eᵢ
具有类型U
,并且相应的参数是引用参数(§15.6.2.3.3),或输出参数(§15.6.2.3.4),则从其Tᵢ
进行U
确切推理(§12.6.3.9)。 - 否则,如果
Eᵢ
具有类型U
,并且相应的参数是输入参数(§15.6.2.3.2),并且Eᵢ
是输入参数,则从其Tᵢ
进行U
确切推理(§12.6.3.9)。 - 否则,如果
Eᵢ
具有一个类型U
,并且相应的参数是输入参数(§15.6.2.3.2),则从中Tᵢ
生成U
下限推理(§12.6.3.10)。 - 否则,不对此参数进行推理。
12.6.3.3 第二阶段
第二阶段按如下所示进行:
- 所有不依赖的未固定类型变量
Xᵢ
(§12.6.3.6)都是Xₑ
固定的(§12.6.3.12)。 - 如果不存在此类类型变量,则所有未固定的类型变量
Xᵢ
都固定在以下所有变量中:- 至少有一个依赖于的类型变量
Xₑ
Xᵢ
Xᵢ
具有非空边界集
- 至少有一个依赖于的类型变量
- 如果不存在此类类型变量,并且仍有 未修复 的类型变量,则类型推理将失败。
- 否则,如果没有进一步 未修复 的类型变量存在,则类型推理会成功。
- 否则,对于具有相应参数类型的所有参数,其中输出类型(§12.6.3.5)包含未修复的类型变量
Xₑ
,但输入类型(§12.6.3.4)则不会生成Tᵢ
Eᵢ
输出类型推理(§12.6.3.7)。Tᵢ
Eᵢ
然后重复第二个阶段。
12.6.3.4 输入类型
如果是E
方法组或隐式类型匿名函数,并且T
是委托类型或表达式树类型,则所有参数类型T
都是具有类型的T
输入类型E
。
12.6.3.5 输出类型
如果是E
方法组或匿名函数,并且T
是委托类型或表达式树类型,则返回类型T
为具有类型的T
输出类型E
。
12.6.3.6 依赖
如果未固定的类型变量直接依赖于未固定的类型变量Xᵢ
Xₑ
,如果对于具有类型的Xₑ
Tᵥ
输入类型Eᵥ
Xᵢ
Tᵥ
且在类型Tᵥ
为类型的输出类型Eᵥ
中发生的某个参数。Eᵥ
Xₑ
取决于Xᵢ
如果Xₑ
直接Xᵢ
依赖于或Xᵢ
依赖于直接Xᵥ
依赖,则Xᵥ
为Xₑ
if。 因此,“依赖”是“直接依赖”的可传递性关闭,而不是反反关闭。
12.6.3.7 输出类型推理
输出类型推理采用以下方式从表达式E
到类型 T:
- 如果
E
匿名函数具有推断的返回类型(§12.6.3.13),并且T
是具有返回类型的Tₓ
U
委托类型或表达式树类型,则从中生成U
Tₓ
下限推理(§12.6.3.10)。 - 否则,如果
E
方法是方法组,并且T
是具有参数类型和T₁...Tᵥ
返回类型的委托类型或表达式树类型Tₓ
,并且类型的T₁...Tᵥ
重载解析E
将生成具有返回类型的U
单个方法,则从Tₓ
U
中生成下限推理。 - 否则,如果
E
为类型U
为表达式,则从T
U
中生成下限推理。 - 否则,不进行推理。
12.6.3.8 显式参数类型推理
显式参数类型推理采用以下方式从表达式E
到类型T
:
- 如果
E
显式类型为具有参数类型的U₁...Uᵥ
匿名函数,并且T
是具有参数类型的V₁...Vᵥ
委托类型或表达式树类型,则针对每个Uᵢ
确切推理(§12.6.3.9)从Uᵢ
相应的Vᵢ
函数进行。
12.6.3.9 精确推理
从类型U
到类型的V
精确推理如下所示:
- 如果是
V
未固定Xᵢ
的其中一个,则会U
添加到确切边界Xᵢ
集。 - 否则,通过检查以下任一情况是否适用来设置
V₁...Vₑ
和U₁...Uₑ
确定:V
是数组类型V₁[...]
,U
是同一排名的数组类型U₁[...]
V
是类型V₁?
,U
是类型U₁
V
是构造类型C<V₁...Vₑ>
,是U
构造类型C<U₁...Uₑ>
如果上述任一情况适用,则从每个Uᵢ
事例进行确切推理到相应的Vᵢ
推理。
- 否则,不进行推理。
12.6.3.10 下限推理
从类型到类型的U
V
下限推理按如下所示进行:
- 如果是
V
未固定Xᵢ
的其中一个,则会U
将其添加到下限Xᵢ
集。 - 否则,如果是类型并且是类型
U₁?
,则从U₁
下限推理到V₁
。U
V₁?
V
- 否则,通过检查以下任一情况是否适用来设置
U₁...Uₑ
和V₁...Vₑ
确定:V
是数组类型V₁[...]
,U
是同一排名的数组类型U₁[...]
V
是一个IEnumerable<V₁>
、ICollection<V₁>
、IReadOnlyList<V₁>>
或IReadOnlyCollection<V₁>
IList<V₁>
U
一个单维数组类型U₁[]
V
是一class
个构造的、interface
struct
或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.11 上限推理
从类型到类型的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.12 修复
具有一组边界的未固定类型变量Xᵢ
固定如下:
- 候选类型
Uₑ
集作为一组边界Xᵢ
中所有类型的集开始。 - 依次检查每个绑定
Xᵢ
:对于所有类型不完全相同U
的确切绑定 UXᵢ
Uₑ
,将从候选集中删除。 对于不存在隐式转换U
的所有类型的Uₑ
每个下限U
Xᵢ
,将从候选集中删除。 对于所有类型Uₑ
中没有隐式转换U
的每种上限 UXᵢ
,将从候选集中删除。 - 如果其余候选类型中有一个唯一类型
Uₑ
,从所有其他候选类型V
隐式转换,则Xᵢ
固定为V
。 - 否则,类型推理失败。
12.6.3.13 推断返回类型
匿名函数 F
的推断返回类型在类型推理和重载解析期间使用。 推断的返回类型只能确定为已知所有参数类型的匿名函数,无论是因为它们是显式给定的,通过匿名函数转换提供,还是在封闭泛型方法调用的类型推理期间推断。
推断的有效返回类型按如下方式确定:
- 如果正文
F
是具有 类型的表达式 ,则推断的有效返回类型F
为该表达式的类型。 - 如果块的主体
F
是一个块,并且块语句return
中的表达式集具有最佳通用类型T
(§12.6.3.15),则推断的有效返回类型F
为T
< a2.6.3.15。 - 否则,无法推断
F
有效的返回类型。
推断的返回类型按如下方式确定:
- 如果
F
异步且主体F
是分类为无表达式(§12.2)的表达式,或者没有return
表达式的块,则推断的返回类型为«TaskType»
(§15.15.1)。 - 如果
F
异步且具有推断的有效返回类型T
,则推断的返回类型为«TaskType»<T>»
(§15.15.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
指令导入的,并且给定了具有类型string
属性的类Customer
Name
,Select
则该方法可用于选择客户列表的名称:List<Customer> customers = GetCustomerList(); IEnumerable<string> names = customers.Select(c => c.Name);
扩展方法调用(§12.8.10.3)
Select
是通过将调用重写为静态方法调用来处理的:IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);
由于未显式指定类型参数,因此类型推理用于推断类型参数。 首先,customers 参数与源参数相关,推断
TSource
为Customer
。 然后,使用上述匿名函数类型推理过程,c
给定类型Customer
,表达式c.Name
与选择器参数的返回类型相关,推断TResult
为string
。 因此,调用等效于Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)
结果的类型
IEnumerable<string>
。以下示例演示匿名函数类型推理如何允许类型信息在泛型方法调用中的参数之间“流”。 给定以下方法和调用:
class A { static Z F<X,Y,Z>(X value, Func<X,Y> f1, Func<Y,Z> f2) { return f2(f1(value)); } static void M() { double hours = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalHours); } }
调用的类型推理继续如下:首先,参数“1:15:30”与值参数相关,推断
X
为字符串。 然后,第一个匿名函数s
的参数给定推断类型string
,表达式TimeSpan.Parse(s)
与返回类型f1
相关,推断Y
为System.TimeSpan
。 最后,第二个匿名函数t
的参数给定推断类型System.TimeSpan
,表达式t.TotalHours
与返回类型f2
相关,推断Z
为double
。 因此,调用的结果的类型double
为 。end 示例
12.6.3.14 方法组转换的类型推理
与泛型方法的调用类似,当包含泛型方法的方法组 M
转换为给定委托类型 D
(§10.8)时,还应应用类型推理。 给定方法
Tₑ M<X₁...Xᵥ>(T₁ x₁ ... Tₑ xₑ)
要分配给委托类型的D
方法组M
,类型推理的任务是查找类型参数S₁...Sᵥ
,以便表达式:
M<S₁...Sᵥ>
与 . 兼容 (§20.2)D
与泛型方法调用的类型推理算法不同,在这种情况下,只有参数类型,没有参数表达式。 具体而言,没有匿名函数,因此不需要多个推理阶段。
相反,所有Xᵢ
参数都被视为未修复,并且从每个参数类型的下限推理到相应的参数类型。Uₑ
D
Tₑ
M
如果发现任何 Xᵢ
边界,则类型推理失败。 否则,所有 Xᵢ
内容都 固定 为相应的 Sᵢ
,这是类型推理的结果。
12.6.3.15 查找一组表达式的最佳常见类型
在某些情况下,需要为一组表达式推断通用类型。 具体而言,隐式类型数组的元素类型以及具有 块 体的匿名函数的返回类型以这种方式找到。
一组表达式 E₁...Eᵥ
的最佳常见类型如下:
- 引入了新的 未固定 类型变量
X
。 - 对于每个表达式,输出类型推理(§12.6.3.7)将从该表达式
Ei
执行到X
。 X
是 固定 的(§12.6.3.12),如果可能,生成的类型是最好的常见类型。- 否则推理失败。
注意:直观地说,这种推理等效于使用
Eᵢ
as 参数和推理X
调用方法void M<X>(X x₁ ... X xᵥ)
。 end note
12.6.4 重载分辨率
12.6.4.1 常规
重载解析是一种绑定时机制,用于选择要在给定参数列表和一组候选函数成员的情况下调用的最佳函数成员。 重载解析选择要在 C# 中的以下不同上下文中调用的函数成员:
- 调用invocation_expression中命名的方法(§12.8.10)。
- 调用object_creation_expression中命名的实例构造函数(§12.8.17.2)。
- 通过element_access(§12.8.12)调用索引器访问器。
- 调用表达式中引用的预定义或用户定义的运算符(§12.4.4 和 §12.4.5)。
每个上下文都以自己的唯一方式定义候选函数成员集和参数列表。 例如,方法调用的候选项集不包括标记为重写的方法(§12.5),如果派生类中的任何方法适用(§12.8.10.2),则基类中的方法不是候选项。
确定候选函数成员和参数列表后,在所有情况下,最佳函数成员的选择都是相同的:
- 首先,候选函数成员集减少为与给定参数列表(§12.6.4.2)相关的那些函数成员。 如果此减少集为空,则会发生编译时错误。
- 然后,适用候选函数成员集中的最佳函数成员位于。 如果集仅包含一个函数成员,则该函数成员是最佳函数成员。 否则,最佳函数成员是与给定参数列表相关的所有其他函数成员更好的函数成员,前提是每个函数成员都与使用 §12.6.4.3 中的规则的所有其他函数成员进行比较。 如果没有完全比所有其他函数成员更好的函数成员,则函数成员调用不明确,并且会发生绑定时错误。
以下子项定义了适用的函数成员和更好的函数成员的术语的确切含义。
12.6.4.2 适用的函数成员
当以下所有内容都为 true 时,函数成员应是与参数列表A
相关的适用函数成员:
- 每个参数
A
都对应于函数成员声明中的参数,如 §12.6.2.2.2 中所述,最多一个参数对应于每个参数,任何参数都不对应的任何参数都是可选参数。 - 对于每个
A
参数,参数的参数传递模式与相应参数的参数传递模式相同,并且
对于包含参数数组的函数成员,如果函数成员适用上述规则,则表示其正常形式适用。 如果包含参数数组的函数成员在其普通形式中不适用,则函数成员可能改用其扩展形式:
- 扩展形式是通过将函数成员声明中的参数数组替换为参数数组的元素类型的零个或多个值参数来构造的,以便参数列表中的
A
参数数与参数总数匹配。 如果A
自变量数少于函数成员声明中的固定参数数,则无法构造函数成员的扩展形式,因此不适用。 - 否则,如果每个参数
A
都适用扩展窗体,则下列参数之一为 true:
当从参数类型到输入参数的参数类型的隐式转换是动态隐式转换(§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 }
end 示例
- 仅当方法组通过类型从 simple_name 或 member_access 结果时,静态方法才适用。
- 仅当方法组从simple_name、通过变量或值member_access或base_access的结果时,实例方法才适用。
- 如果方法组从simple_name结果,则实例方法仅当允许访问 §12.8.14 时才
this
适用。
- 如果方法组从simple_name结果,则实例方法仅当允许访问 §12.8.14 时才
- 当方法组从可通过实例或类型(如 §12.8.7.2 中所述)生成的member_access时,实例和静态方法均适用。
- 一个泛型方法,其类型参数(显式指定或推断)并不完全满足其约束不适用。
- 在方法组转换的上下文中,应存在从方法返回类型到委托返回类型的标识转换(§10.2.2.2)或隐式引用转换(§10.2.8)。 否则,候选方法不适用。
12.6.4.3 更好的函数成员
为了确定更好的函数成员,将构造带状参数列表 A
,只包含自变量表达式本身在原始参数列表中出现的顺序,并排除任何 out
或 ref
参数。
按以下方式构造每个候选函数成员的参数列表:
- 如果函数成员仅适用于扩展窗体,则使用扩展窗体。
- 没有相应参数的可选参数将从参数列表中删除
- 从参数列表中删除引用和输出参数
- 参数重新排序,使其与参数列表中的相应参数位于同一位置。
给定一个参数列表A
,其中包含一组参数表达式{E₁, E₂, ..., Eᵥ}
和两个适用的函数成员Mᵥ
以及Mₓ
参数类型{Q₁, Q₂, ..., Qᵥ}
{P₁, P₂, ..., Pᵥ}
,Mᵥ
并且定义为比如果更好的函数成员 Mₓ
- 对于每个参数,从隐式转换到
Qᵥ
的隐式转换Eᵥ
Eᵥ
Pᵥ
并不好,并且 - 对于至少一个参数,从
Eᵥ
到的Pᵥ
转换优于从Eᵥ
中转换到Qᵥ
的转换。
如果参数类型序列是{Q₁, Q₂, ..., Qᵥ}
等效的{P₁, P₂, ..., Pᵥ}
(即每个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ᵥ
对于每个参数,参数类型比 sMₓ
更具体,不小于特定Sx
,并且至少一个参数Rx
比Sx
:Rx
- 类型参数不特定于非类型参数。
- 以递归方式,如果至少有一个类型参数更具体,并且没有类型参数比另一个构造类型参数更具体,则构造类型比另一个构造的类型参数更具体( 具有相同的类型参数数)。
- 如果第一个数组的元素类型比第二个数组类型的元素类型更具体(维度数相同)。
- 否则,如果一个成员是非提升运算符,另一个成员是提升运算符,则非提升运算符更好。
- 如果两个函数成员都找不到更好的参数,并且所有参数
Mᵥ
都具有相应的参数,而默认参数需要替换为至少一个可选参数Mₓ
,则Mᵥ
比Mₓ
。 - 如果至少有一个参数
Mᵥ
使用比相应参数更好的Mₓ
参数传递选项(§12.6.4.4),并且使用的参数传递选项中没有比Mᵥ
使用更好的参数Mₓ
传递选项更好Mₓ
。Mᵥ
- 否则,没有函数成员更好。
12.6.4.4 更好的参数传递模式
如果两个参数中的一个参数具有值传递模式,则允许在两个重载方法中具有相应的参数,如下所示:
public static void M1(int p1) { ... }
public static void M1(in int p1) { ... }
给定 int i = 10;
时,根据 §12.6.4.2,调用 M1(i)
并 M1(i + 5)
导致两个重载都适用。 在这种情况下,具有值参数传递模式的方法是 更好的参数传递模式选择。
注意:输入、输出或引用传递模式的参数不存在此类选择,因为这些参数仅匹配完全相同的参数传递模式。 end note
12.6.4.5 更好的从表达式转换
给定从表达式转换为类型的隐式转换C₁
,以及从表达式E
转换为类型的T₂
隐式转换C₂
,C₁
与下列值之一相比,转换效果更好C₂
:T₁
E
E
完全匹配T₁
且E
不匹配T₂
(§12.6.4.6)E
完全匹配两者或两者T₁
T₂
,并且T₁
是一个比T₂
(§12.6.4.7) 更好的转换目标E
是一个方法组(§12.2),T₁
与方法组中用于转换的单个最佳方法兼容(§20.4),并且T₂
与方法组中用于转换C₁
的单个最佳方法不兼容C₂
12.6.4.6 完全匹配的表达式
给定表达式和类型时,E
如果以下任一项保留,则完全匹配T
:T
E
E
具有类型S
,并且存在标识S
转换T
E
是匿名函数,T
是委托类型或表达式树类型D
Expression<D>
,下列一项保留:- 参数列表
D
(§12.6.3.12)的上下文中存在E
推断的返回类型X
,并且存在从X
返回类型到返回类型的标识转换D
E
async
是没有返回值的 lambda,并且D
具有非泛型的返回类型«TaskType»
E
要么是非异步类型,也有D
返回类型或E
异步,并且D
具有返回类型Y
«TaskType»<Y>
(§15.15.1),以下一项保留:E
正文是一个完全匹配的表达式Y
E
正文是一个块,其中每个 return 语句返回一个完全匹配的表达式Y
- 参数列表
12.6.4.7 更好的转换目标
给定两种类型,并且是一个比T₂
下列任一类型T₁
更好的转换目标:T₂
T₁
- 从
T₁
隐式转换到T₂
存在,不存在从隐式转换T₂
到T₁
存在 T₁
是«TaskType»<S₁>
(§15.15.1),T₂
是«TaskType»<S₂>
,S₁
是一个比更好的转换目标S₂
T₁
是«TaskType»<S₁>
(§15.15.1),T₂
是«TaskType»<S₂>
,并且T₁
比专用化更专业T₂
T₁
是S₁
或其中S₁
是有符号整型类型,是T₂
S₂
S₂?
或其中S₂
是无符号整型类型。S₁?
具体说来:S₁
is 和S₂
isbyte
sbyte
、ushort
、uint
或ulong
S₁
is 和S₂
isushort
short
、uint
或ulong
S₁
isint
和S₂
isuint
,或ulong
S₁
islong
和S₂
isulong
泛型类中的 12.6.4.8 重载
注意:虽然声明的签名应是唯一的(§8.6),但类型参数的替换可能导致相同的签名。 在这种情况下,重载解析将选取原始签名(替换类型参数之前)最具体的(§12.6.4.3),如果存在,则报告错误。 end note
示例:以下示例显示根据此规则有效且无效的重载:
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); }
end 示例
12.6.5 动态成员调用的编译时检查
尽管动态绑定操作的重载解析在运行时发生,但有时在编译时可以知道将从中选择重载的函数成员列表:
- 对于委托调用(§12.8.10.4),该列表是一个函数成员,其参数列表与 调用delegate_type 相同
- 对于类型(§12.8.10.2)的方法调用(§12.8.10.2),或者对于静态类型不动态的值,方法组中的可访问方法集在编译时已知。
- 对于对象创建表达式(§12.8.17.2),类型中的可访问构造函数集在编译时已知。
- 对于索引器访问(§12.8.12.3),接收器中的可访问索引器集在编译时已知。
在这些情况下,对已知函数成员集中的每个成员执行有限的编译时检查,以确定它是否在运行时永远不会被调用。 对于每个函数成员 F
,构造修改的参数和参数列表:
- 首先,如果
F
提供了泛型方法和类型参数,则这些参数将替换为参数列表中的类型参数。 但是,如果未提供类型参数,则不会发生此类替换。 - 然后,类型为打开的任何参数(即,包含类型参数;请参阅 §8.4.3)已执行,以及相应的参数(s)。
若要 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),则E
为以下类型之一:- 输入参数 (§15.6.2.3.2), 或
- 字段
readonly
(§15.5.3),或 - 引用
readonly
变量或返回 (§9.7),
然后,将创建一个类型临时局部变量
E
,并将该值E
分配给该变量。E
然后重新分类为对该临时局部变量的引用。 临时变量可以按内部M
方式this
访问,但不能以任何其他方式访问。 因此,只有可以写入时E
,调用方才能观察M
对this
它所做的更改。- 参数列表的计算方式为 §12.6.2 中所述。
- 调用
M
。 所E
引用的变量将成为引用的this
变量。
- 评估
否则:
- 评估
E
。 如果此评估导致异常,则不会执行进一步的步骤。 - 参数列表的计算方式为 §12.6.2 中所述。
- 如果类型为value_type,则执行装箱转换(§10.2.9)以转换为
E
class_type,并在E
以下步骤中被视为该class_type。E
如果value_type是enum_type,则class_typeSystem.Enum;
为System.ValueType
。 - 检查其
E
值是否有效。 如果值为E
null,则会引发 a,并且不执行进一System.NullReferenceException
步的步骤。 - 确定要调用的函数成员实现:
- 调用在上述步骤中确定的函数成员实现。 所
E
引用的对象将成为由此引用的对象。
- 评估
调用实例构造函数(§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
。 end note - 当函数成员是接口函数成员的实现,并通过interface_type的实例表达式调用。
- 通过委托调用函数成员时。
在这些情况下,装箱实例被视为包含value_type变量,此变量将成为函数成员调用中由此引用的变量。
注意:具体而言,这意味着在装箱实例上调用函数成员时,函数成员可以修改装箱实例中包含的值。 end note
12.7 解构
解构是一个进程,其中表达式转换为单个表达式的元组。 当简单赋值的目标为元组表达式时,将使用析构来获取要分配给每个元组元素的值。
表达式E
按以下方式解构为元n
组表达式::
- 如果
E
元组表达式包含n
元素,则析构的结果是表达式E
本身。 - 否则,如果具有具有
n
元素的元组类型(T1, ..., Tn)
,则E
计算为临时变量__v
,解构的结果为表达式(__v.Item1, ..., __v.Itemn)
。E
- 否则,如果表达式
E.Deconstruct(out var __v1, ..., out var __vn)
在编译时解析为唯一实例或扩展方法,则计算该表达式,解构的结果为表达式(__v1, ..., __vn)
。 此类方法称为 析构函数。 - 否则,
E
无法解构。
此处, __v
引用 __v1, ..., __vn
其他不可见且不可访问的临时变量。
注意:无法解构类型的
dynamic
表达式。 end note
12.8 主要表达式
12.8.1 常规
主表达式包括最简单的表达式形式。
primary_expression
: primary_no_array_creation_expression
| array_creation_expression
;
primary_no_array_creation_expression
: literal
| interpolated_string_expression
| simple_name
| parenthesized_expression
| tuple_expression
| member_access
| null_conditional_member_access
| invocation_expression
| element_access
| null_conditional_element_access
| this_access
| base_access
| post_increment_expression
| post_decrement_expression
| null_forgiving_expression
| object_creation_expression
| delegate_creation_expression
| anonymous_object_creation_expression
| typeof_expression
| sizeof_expression
| checked_expression
| unchecked_expression
| default_value_expression
| nameof_expression
| anonymous_method_expression
| pointer_member_access // unsafe code support
| pointer_element_access // unsafe code support
| stackalloc_expression
;
注意:这些语法规则不是 ANTLR 就绪的,因为它们是 ANTLR 不处理的一组相互左递归规则(
primary_expression
、、、primary_no_array_creation_expression
、invocation_expression
member_access
element_access
、post_increment_expression
、post_decrement_expression
、和null_forgiving_expression
pointer_member_access
pointer_element_access
)的一部分。 标准技术可用于转换语法以删除相互左递归。 这并没有完成,因为并非所有分析策略都需要它(例如 LALR 分析器不会),这样做会模糊化结构和说明。 end note
pointer_member_access(§23.6.3)和pointer_element_access(§23.6.4)仅在不安全的代码(§23)中可用。
主表达式在 array_creation_expressions 和 primary_no_array_creation_expression之间划分。 以这种方式处理array_creation_expression,而不是将其与其他简单表达式形式一起列出,使语法禁止可能令人困惑的代码,例如
object o = new int[3][1];
否则将被解释为
object o = (new int[3])[1];
12.8.2 文本
由文本(§6.4.5)组成的primary_expression被归类为值。
12.8.3 内插字符串表达式
interpolated_string_expression由$
字符中的文本组成,$@
或@$
紧跟文本"
。 在带引号的文本中,有零个或多个用字符分隔{
}
的内插,每个内插都包含表达式和可选格式规范。
内插字符串表达式有两种形式:正则(interpolated_regular_string_expression)和逐字(interpolated_verbatim_string_expression);在词法上与字符串文本的两种形式(§6.4.5.6)不同,但语义上不同。
interpolated_string_expression
: interpolated_regular_string_expression
| interpolated_verbatim_string_expression
;
// interpolated regular string expressions
interpolated_regular_string_expression
: Interpolated_Regular_String_Start Interpolated_Regular_String_Mid?
('{' regular_interpolation '}' Interpolated_Regular_String_Mid?)*
Interpolated_Regular_String_End
;
regular_interpolation
: expression (',' interpolation_minimum_width)?
Regular_Interpolation_Format?
;
interpolation_minimum_width
: constant_expression
;
Interpolated_Regular_String_Start
: '$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Regular_String_Mid
: Interpolated_Regular_String_Element+
;
Regular_Interpolation_Format
: ':' Interpolated_Regular_String_Element+
;
Interpolated_Regular_String_End
: '"'
;
fragment Interpolated_Regular_String_Element
: Interpolated_Regular_String_Character
| Simple_Escape_Sequence
| Hexadecimal_Escape_Sequence
| Unicode_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Regular_String_Character
// Any character except " (U+0022), \\ (U+005C),
// { (U+007B), } (U+007D), and New_Line_Character.
: ~["\\{}\u000D\u000A\u0085\u2028\u2029]
;
// interpolated verbatim string expressions
interpolated_verbatim_string_expression
: Interpolated_Verbatim_String_Start Interpolated_Verbatim_String_Mid?
('{' verbatim_interpolation '}' Interpolated_Verbatim_String_Mid?)*
Interpolated_Verbatim_String_End
;
verbatim_interpolation
: expression (',' interpolation_minimum_width)?
Verbatim_Interpolation_Format?
;
Interpolated_Verbatim_String_Start
: '$@"'
| '@$"'
;
// the following three lexical rules are context sensitive, see details below
Interpolated_Verbatim_String_Mid
: Interpolated_Verbatim_String_Element+
;
Verbatim_Interpolation_Format
: ':' Interpolated_Verbatim_String_Element+
;
Interpolated_Verbatim_String_End
: '"'
;
fragment Interpolated_Verbatim_String_Element
: Interpolated_Verbatim_String_Character
| Quote_Escape_Sequence
| Open_Brace_Escape_Sequence
| Close_Brace_Escape_Sequence
;
fragment Interpolated_Verbatim_String_Character
: ~["{}] // Any character except " (U+0022), { (U+007B) and } (U+007D)
;
// lexical fragments used by both regular and verbatim interpolated strings
fragment Open_Brace_Escape_Sequence
: '{{'
;
fragment Close_Brace_Escape_Sequence
: '}}'
;
上面定义的词法规则中有六个区分 上下文 ,如下所示:
规则 | 上下文要求 |
---|---|
Interpolated_Regular_String_Mid | 仅在Interpolated_Regular_String_Start后、任何以下内插之间以及相应Interpolated_Regular_String_End之前识别。 |
Regular_Interpolation_Format | 仅在regular_interpolation内识别,并且当起始冒号(:)未嵌套在任何类型的括号(括号/大括号/正方形)中时。 |
Interpolated_Regular_String_End | 仅在Interpolated_Regular_String_Start后识别,并且仅当任何干预令牌都Interpolated_Regular_String_Mid或可以属于regular_interpolation的令牌时,包括此类内插中包含的任何interpolated_regular_string_expression的令牌。 |
Interpolated_Verbatim_String_Mid Verbatim_Interpolation_Format Interpolated_Verbatim_String_End | 识别这三个规则遵循上述相应规则的对应规则,每个提到的 常规 语法规则替换为相应的 逐字 规则。 |
注意:上述规则区分上下文,因为其定义与语言中的其他标记的定义重叠。 end note
注意:由于上下文敏感词法规则,上述语法未准备就绪。 与其他词法器生成器一样,ANTLR 支持上下文敏感词法规则,例如使用其 词法模式,但这是实现详细信息,因此不是此规范的一部分。 end note
interpolated_string_expression被归类为值。 如果它立即转换为System.IFormattable
System.FormattableString
或具有隐式内插字符串转换(§10.2.5),则内插字符串表达式具有该类型。 否则,它具有类型 string
。
注意:interpolated_string_expression的可能类型之间的差异可能取决于 (§C.2) 和
System.FormattableString
(§C.3) 的文档System.String
。 end note
内插(regular_interpolation和verbatim_interpolation)的含义是,根据Regular_Interpolation_Format或Verbatim_Interpolation_Format指定的格式,或根据表达式类型的默认格式将表达式string
的值格式化为格式。 然后,interpolation_minimum_width修改格式化字符串,以生成要内插到interpolated_string_expression中的最终string
字符串(如果有)。
注意:如何确定类型的默认格式在文档
System.String
(§C.2) 和System.FormattableString
(§C.3) 中详细说明。 标准格式的说明与Regular_Interpolation_Format和Verbatim_Interpolation_Format相同,请参阅标准库(§C.4)的文档System.IFormattable
和标准库(§C)中的其他类型。 end note
在interpolation_minimum_width,constant_expression应具有隐式转换。int
让 字段宽度 成为此 constant_expression 的绝对值, 对齐方式 是此 constant_expression值的正值(正值或负值):
- 如果字段宽度的值小于或等于格式化字符串的长度,则不会修改格式化字符串。
- 否则,格式化字符串将填充空格字符,使其长度等于字段宽度:
- 如果对齐方式为正,则格式化字符串通过追加填充来右对齐,
- 否则,它通过追加填充来左对齐。
interpolated_string_expression的整体含义(包括上述内插格式和填充)是通过将表达式转换为方法调用定义的:如果表达式的类型为System.IFormattable
或System.FormattableString
该方法System.Runtime.CompilerServices.FormattableStringFactory.Create
是返回类型的值(§C.3),则类型应为 string
,并且该方法为string.Format
返回类型的string
System.FormattableString
值(§C.2)。
在这两种情况下,调用的参数列表都包含一个 格式字符串文本 ,其中包含 每个内插的格式规范 ,以及对应于格式规范的每个表达式的参数。
格式字符串文本构造如下,其中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" |
end 示例
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出现在直接包含局部变量、参数或名称I
常量的局部变量声明空间(§7.3)内,则simple_name引用该局部变量、参数或常量,并归类为变量或值。 - 如果
e
为零且simple_name出现在泛型方法声明中,但在其method_declaration的属性之外,如果该声明包含名称I
的类型参数,则simple_name引用该类型参数。 - 否则,对于每个实例类型(§15.3.2),从立即封闭类型
T
声明的实例类型开始,并继续每个封闭类或结构声明的实例类型(如果有):- 如果
e
为零且包含名称I
的类型参数的声明T
,则simple_name引用该类型参数。 - 否则,如果具有
e
类型参数的成员查找(§12.5)I
T
将生成匹配项:- 如果
T
实例类型是立即封闭类或结构类型的实例类型,并且查找标识了一个或多个方法,则结果是具有关联的实例表达式this
的方法组。 如果指定了类型参数列表,则用于调用泛型方法(§12.8.10.2)。 - 否则,如果
T
是立即封闭类或结构类型的实例类型,如果查找标识实例成员,并且引用发生在实例构造函数、实例方法或实例访问器(§12.2.1)块内,则结果与表单this.I
的成员访问(§12.8.7)相同。 这只能在零时e
发生。 - 否则,结果与表单或表单
T.I
T.I<A₁, ..., Aₑ>
的成员访问 (§12.8.7) 相同。
- 如果
- 如果
- 否则,对于每个命名空间
N
,从发生simple_name的命名空间开始,继续每个封闭命名空间(如果有),以全局命名空间结尾,将计算以下步骤,直到实体所在的位置:- 如果
e
为零并且I
是命名空间的名称N
,则:- 如果发生simple_name的位置由命名空间声明括起来,并且命名空间声明
N
包含将名称I
与命名空间或类型关联的extern_alias_directive或using_alias_directive,则simple_name不明确且发生编译时错误。 - 否则,simple_name引用名为
I
的N
命名空间。
- 如果发生simple_name的位置由命名空间声明括起来,并且命名空间声明
- 否则,如果
N
包含具有名称和I
e
类型参数的可访问类型,则:- 如果
e
为零且发生simple_name的位置由命名空间声明括起来,并且命名空间声明N
包含与命名空间或类型关联的I
extern_alias_directive或using_alias_directive,则simple_name不明确且发生编译时错误。 - 否则, namespace_or_type_name 引用使用给定类型参数构造的类型。
- 如果
- 否则,如果发生simple_name的位置由命名空间声明括起来,则为
N
:- 如果
e
为零,并且命名空间声明包含将名称I
与导入的命名空间或类型关联的extern_alias_directive或using_alias_directive,则simple_name引用该命名空间或类型。 - 否则,如果命名空间声明的 using_namespace_directive 导入的命名空间仅包含一种具有名称和
e
I
类型参数的类型,则simple_name引用使用给定类型参数构造的类型。 - 否则,如果命名空间声明的 using_namespace_directive 导入的命名空间包含具有名称和
e
I
类型参数的多个类型,则simple_name不明确且发生编译时错误。
- 如果
注意:整个步骤与处理namespace_or_type_name(§7.8)中的相应步骤完全并行。 end note
- 如果
- 否则,如果
e
为零且I
为标识符_
, 则simple_name 是一个简单的 放弃,它是声明表达式的形式(§12.17)。 - 否则, simple_name 未定义且发生编译时错误。
12.8.5 括号表达式
parenthesized_expression由括在括号中的表达式组成。
parenthesized_expression
: '(' expression ')'
;
通过计算括号内的表达式来计算parenthesized_expression。 如果括号中的表达式表示命名空间或类型,则会发生编译时错误。 否则,parenthesized_expression的结果是包含表达式的计算结果。
12.8.6 元组表达式
tuple_expression表示元组,由两个或更多个逗号分隔和可选命名的表达式组成,括在括号中。 deconstruction_expression是包含隐式类型声明表达式的元组的速记语法。
tuple_expression
: '(' tuple_element (',' tuple_element)+ ')'
| deconstruction_expression
;
tuple_element
: (identifier ':')? expression
;
deconstruction_expression
: 'var' deconstruction_tuple
;
deconstruction_tuple
: '(' deconstruction_element (',' deconstruction_element)+ ')'
;
deconstruction_element
: deconstruction_tuple
| identifier
;
tuple_expression分类为元组。
tuple_expressionvar (e1, ..., en)
的速记(var e1, ..., var en)
deconstruction_expression遵循相同的行为。 这以递归方式应用于deconstruction_expression中的任何嵌套deconstruction_tuple。 因此,嵌套在deconstruction_expression中的每个标识符都会引入声明表达式(§12.17)。 因此, deconstruction_expression 只能在简单分配的左侧发生。
元组表达式具有一个类型,前提是其每个元素表达式 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.21.2)的目标,从元组表达式中获取。
示例:
(int i, string) t1 = (i: 1, "One"); (long l, string) t2 = (l: 2, null); var t3 = (i: 3, "Three"); // (int i, string) var t4 = (i: 4, null); // Error: no type
在此示例中,所有四个元组表达式都有效。 前两
t1
个,并且t2
不使用元组表达式的类型,而是应用隐式元组转换。 在这种情况下t2
,隐式元组转换依赖于从2
/long
向/从null
string
隐式转换。 第三个元组表达式具有一个类型(int i, string)
,因此可以重新分类为该类型的值。t4
另一方面,声明是一个错误:元组表达式没有类型,因为它的第二个元素没有类型。if ((x, y).Equals((1, 2))) { ... };
此示例显示元组有时可能会导致多层括号,尤其是在元组表达式是方法调用的唯一参数时。
end 示例
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_memberI
)是单个标识符,是<A₁, ..., Aₑ>
可选的type_argument_list。 如果未 指定type_argument_list ,请考虑 e
为零。
具有primary_expression类型的dynamic
member_access动态绑定(§12.3.3)。 在这种情况下,编译器将成员访问分类为类型的 dynamic
属性访问。 然后,使用以下规则来确定member_access的含义,然后使用运行时类型而不是primary_expression的编译时类型应用。 如果此运行时分类导致方法组,则成员访问应为invocation_expression的primary_expression。
计算 member_access并将其 分类为:
- 如果
e
为零并且E
是命名空间,并且E
包含名称为I
嵌套命名空间,则结果为该命名空间。 - 否则,如果
E
命名空间包含E
具有名称和I
K
类型参数的可访问类型,则结果是使用给定类型参数构造的类型。 - 如果
E
被归类为类型,如果E
不是类型参数,并且具有K
类型参数的成员查找(§12.5)I
E
将生成匹配项,则E.I
计算并分类如下:注意:当此类成员查找的结果为方法组且
K
为零时,方法组可以包含具有类型参数的方法。 这允许将此类方法视为类型参数推理。 end note- 如果
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
,并且具有K
类型参数的成员查找(§12.5)I
T
将生成匹配项,然后E.I
计算并分类如下:- 首先,如果是
E
属性或索引器访问,则获取属性或索引器访问的值(§12.2.2),E 将重新分类为值。 - 如果
I
标识一个或多个方法,则结果是具有关联的实例表达式E
的方法组。 - 如果
I
标识实例属性,则结果是具有关联的实例表达式E
的属性访问,以及属于该属性类型的关联类型。 如果是T
类类型,则从从起始T
时找到的属性的第一个声明或重写中选择关联的类型,然后搜索其基类。 - 如果是
T
class_type,并I
标识该class_type的实例字段:- 如果值为
E
null
,则会引发 aSystem.NullReferenceException
。 - 否则,如果字段是只读的,并且引用发生在声明字段的类的实例构造函数之外,则结果为一个值,即所
E
引用对象中字段I
的值。 - 否则,结果是一个变量,即对象中引用的
E
字段I
。
- 如果值为
- 如果是
T
struct_type并I
标识该struct_type的实例字段:- 如果
E
为值,或者字段是只读的,并且引用发生在声明字段的结构的实例构造函数之外,则结果为一个值,即结构实例中给定E
的字段I
的值。 - 否则,结果是一个变量,即结构实例中给定的
E
字段I
。
- 如果
- 如果
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
与type_name(§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
该字段的标识符则不进行分隔。end 示例
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
为可为 null 的值类型:让
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 操作,则也是如此。end note
null_conditional_projection_initializer是null_conditional_member_access的限制,具有相同的语义。 它仅在匿名对象创建表达式(§12.8.17.7)中作为投影初始值设定项发生。
12.8.9 Null-forgiving 表达式
12.8.9.1 常规
null 表示表达式的值、类型、分类(§12.2)和安全上下文(§16.4.12)是其 primary_expression的值、类型、分类和安全上下文。
null_forgiving_expression
: primary_expression null_forgiving_operator
;
null_forgiving_operator
: '!'
;
注意:后缀 null 求值运算符和前缀逻辑求反运算符(§12.9.4)是不同的,而由同一词法标记(!
)表示。 只有后者可以重写(§15.10),null 放弃运算符的定义是固定的。 end note
将 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)
end 示例
此子集合的其余部分和以下兄弟姐妹子项有条件地规范化。
执行静态 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 的值”警告。end 示例
示例: 应谨慎使用 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
。end 示例
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
的参数rv
lv
的类型与输出参数相同string?
lv
,它执行简单的赋值。方法
M
将变量s
(类型)string
作为Assign
输出参数传递,编译器使用的警告s
不是可为 null 的变量。 假设Assign
第二个参数不能为 null,则 null 表示运算符用于取消警告。end 示例
有条件的规范文本的结尾。
12.8.10 调用表达式
12.8.10.1 常规
invocation_expression用于调用方法。
invocation_expression
: primary_expression '(' argument_list? ')'
;
如果primary_expression具有delegate_type,则null_forgiving_expression。
如果以下至少一项 保留,则invocation_expression 动态绑定 (§12.3.3):
- primary_expression具有编译时类型
dynamic
。 - 可选 argument_list 至少有一个参数具有编译时类型
dynamic
。
在这种情况下,编译器将invocation_expression分类为类型的dynamic
值。 然后,使用以下规则来确定invocation_expression的含义,然后使用运行时类型而不是具有编译时类型的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调用 returns-no-value 方法(§15.6.1)或 returns-no-value 委托,则结果不为任何。 只有在statement_expression(§13.7)或lambda_expression(§12.19)的上下文中,才允许归类为无项的表达式。 否则,将发生绑定时错误。
- 否则,如果 invocation_expression 调用 returns-by-ref 方法(§15.6.1)或 returns-by-ref 委托,则结果是一个变量,其返回类型为方法或委托的关联类型。 如果调用是实例方法,并且接收方是类类型
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,包括以下步骤:
- 构造方法调用的候选方法集。 对于与方法组
M
关联的每个方法F
:- 如果
F
为非泛型,F
则为候选项,当:M
没有类型参数列表,并且F
适用于A
(§12.6.4.2)。
- 如果
F
为泛型且M
没有类型参数列表,F
则为候选项,当: - 如果
F
为泛型且M
包含类型参数列表,F
则为候选项,前提是:
- 如果
- 候选方法集减少为仅包含来自最派生类型的方法:对于集中的每个方法
C.F
,其中C
声明了该方法F
的类型,从集中删除在基类型C
中声明的所有方法。 此外,如果C
不是类object
类型,则从集中删除在接口类型中声明的所有方法。注意:当方法组是类型参数具有有效基类
object
而非非空有效接口集的成员查找的结果时,后一个规则才有效。 end note - 如果生成的候选方法集为空,则会放弃执行以下步骤的进一步处理,而是尝试将调用作为扩展方法调用处理(§12.8.10.3)。 如果此操作失败,则不存在适用的方法,并且会发生绑定时错误。
- 使用 §12.6.4 的重载解析规则标识候选方法集的最佳方法。 如果无法识别单个最佳方法,则方法调用不明确,并且会发生绑定时错误。 执行重载解析时,在替换相应方法类型参数的类型参数(提供或推断)后,将考虑泛型方法的参数。
通过上述步骤在绑定时选择并验证方法后,将根据 §12.6.6 中所述的函数成员调用规则处理实际的运行时调用。
注意:上述解析规则的直观效果如下:若要查找方法调用所调用的特定方法,请从方法调用指示的类型开始,并继续继承链,直到找到至少一个适用、可访问的非替代方法声明。 然后对该类型中声明的适用、可访问的非重写方法集执行类型推理和重载解析,并调用因此选择的方法。 如果未找到任何方法,请尝试将调用作为扩展方法调用进行处理。 end note
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
将传递给扩展方法,因为它将通过常规静态方法调用。 由扩展方法实现决定如何响应此类调用。 end note
上述规则意味着实例方法优先于扩展方法,内部命名空间声明中提供的扩展方法优先于外部命名空间声明中提供的扩展方法,并且直接在命名空间中声明的扩展方法优先于使用 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
's 方法优先于第一个扩展方法,C
's 方法优先于这两个扩展方法。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.F
。C.F
end 示例
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
<a0/>,则会引发 a,并且不执行进一 System.NullReferenceException
步的步骤。 - 否则,
D
是对委托实例的引用。 函数成员调用(§12.6.6)对委托调用列表中的每个可调用实体执行。 对于包含实例和实例方法的可调用实体,调用的实例是可调用实体中包含的实例。
有关没有参数的多个调用列表的详细信息,请参阅 §20.6 。
12.8.11 Null 条件调用表达式
null_conditional_invocation_expression在语法上是null_conditional_member_access(§12.8.8)或null_conditional_element_access(§12.8.13),其中最终dependent_access是调用表达式(§12.8.10)。
null_conditional_invocation_expression发生在statement_expression(§13.7)、anonymous_function_body(§12.19.1)或method_body(§15.6.1)的上下文中。
与语法上等效 的null_conditional_member_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表达式是形式P?A
;其中A
语法上等效的余下null_conditional_member_access或null_conditional_element_access,A
因此将以.
或开头[
。E
让我们 PA
表示 P
和 A
。
当作为statement_expression发生时E
,其E
含义与语句的含义相同:
if ((object)P != null) PA
P
只计算一次除外。
作为anonymous_function_body或method_body发生时E
,其含义E
取决于其分类:
如果
E
分类为无内容,则其含义与块的含义相同:{ if ((object)P != null) PA; }
P
只计算一次除外。否则,其
E
含义与块的含义相同:{ return E; }
反过来,此 块 的含义取决于语法上是
E
等效于 null_conditional_member_access (§12.8.8)还是 null_conditional_element_access (§12.8.13)。
12.8.12 元素访问
12.8.12.1 常规
element_access由一个primary_no_array_creation_expression组成,后跟一个“[
”令牌,后跟一个argument_list,后跟一个“]
”令牌。 argument_list由一个或多个参数组成,用逗号分隔。
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
不允许element_access argument_list包含out
或ref
参数。
如果至少保留以下一项 ,则element_access 将动态绑定(§12.3.3):
- primary_no_array_creation_expression具有编译时类型
dynamic
。 - argument_list至少有一个表达式具有编译时类型
dynamic
,并且primary_no_array_creation_expression没有数组类型。
在这种情况下,编译器将element_access分类为类型值dynamic
。 然后,使用以下规则来确定运行时element_access的含义,使用运行时类型而不是具有编译时类型的primary_no_array_creation_expression和argument_list表达式的编译时类型dynamic
。 如果primary_no_array_creation_expression没有编译时类型dynamic
,则元素访问将进行有限的编译时检查,如 §12.6.5 中所述。
如果element_access的primary_no_array_creation_expression是array_type的值,则element_access是数组访问(§12.8.12.2)。 否则,primary_no_array_creation_expression应是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下,element_access是索引器访问(§12.8.12.3)。
12.8.12.2 数组访问
对于数组访问,element_access的primary_no_array_creation_expression应为array_type的值。 此外, 不允许数组访问argument_list 包含命名参数。 argument_list中的表达式数应与array_type的级别相同,每个表达式的类型、long
int
uint
类型、或ulong,
可隐式转换为其中一个或多个类型。
计算数组访问的结果是数组的元素类型的变量,即由argument_list中表达式的值(s)选择的数组元素。
窗体P[A]
的数组访问的运行时处理(其中 P
primary_no_array_creation_expression array_type且A
是一个argument_list),包括以下步骤:
- 评估
P
。 如果此评估导致异常,则不会执行进一步的步骤。 - 按从左到右的顺序计算argument_list的索引表达式。 对每个索引表达式进行计算后,将执行隐式转换(§10.2)到以下类型之一:
int
、、ulong
long
uint
。 选择隐式转换的此列表中的第一个类型。 例如,如果索引表达式的类型为类型short
,则执行隐式转换int
,因为可以进行从short
/int
向的short
long
隐式转换。 如果索引表达式或后续隐式转换的计算会导致异常,则不会计算进一步的索引表达式,也不会执行进一步的步骤。 - 检查其
P
值是否有效。 如果值为P
<a0/>,则会引发 a,并且不执行进一 System.NullReferenceException
步的步骤。 - 根据argument_list引用
P
的数组实例的每个维度的实际边界检查argument_list中的每个表达式的值。 如果一个或多个值超出范围,则会引发 a,并且不执行进一System.IndexOutOfRangeException
步的步骤。 - 计算索引表达式(s)给出的数组元素的位置,此位置将成为数组访问的结果。
12.8.12.3 索引器访问
对于索引器访问,element_access的primary_no_array_creation_expression应为类、结构或接口类型的变量或值,并且此类型应实现一个或多个适用于element_access argument_list的索引器。
表单P[A]
索引器访问的绑定时处理,其中P
类、结构或接口类型的T
primary_no_array_creation_expression,并且A
是一个argument_list,包括以下步骤:
- 构造由
T
提供的索引器集。 该集由声明的所有T
索引器或不是替代声明的T
基类型组成,可在当前上下文(§7.5)中访问。 - 该集将减少到那些适用且不受其他索引器隐藏的索引器。 以下规则应用于集中的每个索引器,其中
S
声明索引器S.I
I
的类型: - 如果生成的候选索引器集为空,则不存在适用的索引器,并发生绑定时错误。
- 候选索引器集的最佳索引器是使用 §12.6.4 的重载解析规则标识的。 如果无法识别单个最佳索引器,则索引器访问不明确,并且会发生绑定时错误。
- 按从左到右的顺序计算argument_list的索引表达式。 处理索引器访问的结果是分类为索引器访问的表达式。 索引器访问表达式引用在上述步骤中确定的索引器,并具有关联的实例表达式
P
和关联的参数列表A
,以及作为索引器类型的关联类型。 如果T
为类类型,则从从索引器的第一个声明或重写中选择关联的类型,从其基类开始T
和搜索时找到。
根据所使用的上下文,索引器访问会导致调用 get 访问器或索引器的 set 访问器。 如果索引器访问是分配的目标,则会调用 set 访问器来分配新值(§12.21.2)。 在所有其他情况下,调用 get 访问器以获取当前值(§12.2.2)。
12.8.13 Null 条件元素访问
null_conditional_element_access包含一个primary_no_array_creation_expression,后跟两个标记“?
”和“”[
,后跟一个argument_list,后跟一个“]
”标记,后跟零个或多个dependent_accesses,其中任何一个标记都可以前面有一个null_forgiving_operator。
null_conditional_element_access
: primary_no_array_creation_expression '?' '[' argument_list ']'
(null_forgiving_operator? dependent_access)*
;
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
为可为 null 的值类型:让我们
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₁ a0>。 如果表达式是一系列 null_conditional_element_access或 null_conditional_member_access §12.8.8 操作,则也是如此。 end note
12.8.14 此访问权限
this_access由关键字this
组成。
this_access
: 'this'
;
只能在实例构造函数块、实例方法、实例访问器(§12.2.1)或终结器中允许this_access。 它具有以下含义之一:
- 当在类的实例构造函数中的primary_expression中使用时
this
,它被归类为值。 值的类型是在其中发生用法的类的实例类型(§15.3.2),该值是对所构造对象的引用。 - 在类的实例方法或实例访问器中的primary_expression中使用时
this
,它被归类为值。 该值的类型是在其中发生用法的类的实例类型(§15.3.2),该值是对调用方法或访问器的对象的引用。 - 当在结构的实例构造函数中的primary_expression中使用时
this
,它将分类为变量。 变量的类型是在其中发生用法的结构的实例类型(§15.3.2),变量表示正在构造的结构。- 如果构造函数声明没有构造函数初始值设定项,则
this
变量的行为与结构类型的输出参数完全相同。 具体而言,这意味着变量应在实例构造函数的每个执行路径中明确分配。 - 否则,变量
this
的行为与结构类型的参数完全相同ref
。 具体而言,这意味着该变量最初被视为已分配。
- 如果构造函数声明没有构造函数初始值设定项,则
- 当在结构的实例方法或实例访问器中的primary_expression中使用时
this
,它将分类为变量。 变量的类型是在其中发生用法的结构的实例类型(§15.3.2)。
this
在上面列出的上下文中的primary_expression中使用是编译时错误。 具体而言,不能在静态方法、静态属性访问器或字段声明的variable_initializer中引用this
。
12.8.15 基本访问
base_access包含关键字基,后跟“.
”标记和标识符和可选type_argument_list或括在方括号中的argument_list:
base_access
: 'base' '.' identifier type_argument_list?
| 'base' '[' argument_list ']'
;
base_access用于访问由当前类或结构中类似命名的成员隐藏的基类成员。 只有在实例构造函数、实例方法、实例访问器(§12.2.1)或终结器主体中才允许base_access。 在类或结构中发生时 base.I
,应表示该类或结构的基类的成员。 同样,在类中发生时 base[E]
,基类中应存在适用的索引器。
在绑定时,base_access窗体的表达式,其base[E]
计算方式与写入((B)this)[E]
((B)this).I
的表达式完全相同,其中B
是发生构造的类或结构的基类。base.I
因此, base.I
除了 base[E]
this.I
this[E]
被视为基类的实例外, this
还对应于。
当base_access引用虚拟函数成员(方法、属性或索引器)时,将更改在运行时调用的函数成员(§12.6.6)。 所调用的函数成员是通过查找函数成员中派生最多的实现(§15.6.4)来确定的(而不是相对于运行时类型的函数成员B
this
,就像在非基访问中一样)。 因此,在虚拟函数成员的重写中, 可以使用base_access 调用函数成员的继承实现。 如果base_access引用的函数成员是抽象的,则会发生绑定时错误。
注意:与不同
this
,base
它本身不是表达式。 它是仅在base_access或constructor_initializer(§15.11.2)上下文中使用的关键字。 end note
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
char
uint
ulong
float
long
double
和decimal
任意枚举类型。 预定义 ++
运算符返回通过添加到 1
操作数生成的值,预定义 --
运算符返回通过从操作数中减去 1
生成的值。 在选中的上下文中,如果此加法或减法的结果超出结果类型的范围,并且结果类型是整型类型或枚举类型,则会引发一个 System.OverflowException
。
应有从所选一元运算符的返回类型到primary_expression类型的隐式转换,否则会发生编译时错误。
窗体 x++
的后缀递增或递减操作的运行时处理,或 x--
由以下步骤组成:
- 如果
x
分类为变量:x
的计算结果为生成变量。- 保存的值
x
。 - 保存的值
x
将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。 - 运算符返回的值将转换为早期计算
x
结果给出的位置的类型x
并存储。 - 保存的值
x
将成为操作的结果。
- 如果
x
分类为属性或索引器访问:- 实例表达式(如果
x
不是static
)和参数列表(如果x
为索引器访问)与x
计算,结果用于后续 get 和 set 访问器调用。 - 调用 get 访问器
x
并保存返回的值。 - 保存的值
x
将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。 - 运算符返回的值将转换为类型
x
,并使用此值作为其值参数调用的 set 访问器x
。 - 保存的值
x
将成为操作的结果。
- 实例表达式(如果
和++
--
运算符还支持前缀表示法(§12.9.6)。 操作之前的结果x++
或x--
为值x
,而操作后的结果++x
或--x
值 x
。 在任一情况下, x
自身在操作后具有相同的值。
可以使用后缀或前缀表示法调用运算符 ++
或运算符 --
实现。 不能为这两个表示法使用单独的运算符实现。
12.8.17 新运算符
12.8.17.1 常规
该 new
运算符用于创建新类型的实例。
有三种形式的新表达式:
- 对象创建表达式和匿名对象创建表达式用于创建新的类类型和值类型的实例。
- 数组创建表达式用于创建新数组类型的实例。
- 委托创建表达式用于获取委托类型的实例。
运算符 new
表示创建类型的实例,但不一定意味着内存的分配。 具体而言,值类型的实例不需要超出它们所在的变量的附加内存,并且当用于创建值类型的实例时 new
不会发生分配。
注意:委托创建表达式并不总是创建新实例。 当表达式的处理方式与方法组转换(§10.8)或匿名函数转换(§10.7)相同时,可能会导致重复使用现有的委托实例。 end note
12.8.17.2 对象创建表达式
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的类型应为class_type、value_type或type_parameter。 类型不能是tuple_type或抽象或静态class_type。
仅当类型为class_type或struct_type时,才允许可选的argument_list(§12.6.2)。
如果对象创建表达式包含对象初始值设定项或集合初始值设定项,则可以省略构造函数参数列表并括住括号。 省略构造函数参数列表和封闭括号等效于指定空参数列表。
处理包含对象初始值设定项或集合初始值设定项的对象创建表达式包括先处理实例构造函数,然后处理对象初始值设定项(§12.8.17.3)或集合初始值设定项(§12.8.17.4)指定的成员或元素初始化。
如果可选argument_list中的任何参数具有编译时类型dynamic
,则object_creation_expression将动态绑定(§12.3.3),并且以下规则在运行时使用具有编译时类型的argument_list的这些参数的运行时类型dynamic
应用以下规则。 但是,对象创建将经历有限的编译时检查,如 §12.6.5 中所述。
新窗体的object_creation_expression的绑定时处理(即T
class_type或value_type),并且A
是可选的argument_list,包括以下步骤:T(A)
- 如果
T
为 value_type 且A
不存在:- object_creation_expression是默认构造函数调用。 object_creation_expression的结果是类型
T
值,即 §8.3.3 中定义的默认值T
。
- object_creation_expression是默认构造函数调用。 object_creation_expression的结果是类型
- 否则,如果
T
为 type_parameter 且A
不存在:- 如果未指定
T
任何值类型约束或构造函数约束(§15.2.5),则会发生绑定时错误。 - object_creation_expression的结果是类型参数绑定到的运行时类型的值,即调用该类型的默认构造函数的结果。 运行时类型可以是引用类型或值类型。
- 如果未指定
- 否则,如果是
T
class_type或struct_type:- 如果
T
为抽象或静态 class_type,则会发生编译时错误。 - 要调用的实例构造函数是使用 §12.6.4 的重载解析规则确定的。 候选实例构造函数集由声明的所有
T
可访问实例构造函数组成,这些构造函数适用于 A(§12.6.4.2)。 如果候选实例构造函数集为空,或者无法识别单个最佳实例构造函数,则会发生绑定时错误。 - object_creation_expression的结果是一个类型
T
值,即调用在上述步骤中确定的实例构造函数生成的值。 - 否则, object_creation_expression 无效,并发生绑定时错误。
- 如果
即使 object_creation_expression 是动态绑定的,编译时类型仍为 T
。
新窗体object_creation_expression的运行时处理,其中T
class_type或struct_type,并且A
是可选的argument_list,包括以下步骤: T(A)
- 如果是
T
class_type: - 如果是
T
struct_type:- 通过分配临时局部变量来创建类型的
T
实例。 由于struct_type的实例构造函数必须明确为要创建的实例的每个字段赋值,因此不需要初始化临时变量。 - 实例构造函数根据函数成员调用规则(§12.6.6.6)调用。 对新分配的实例的引用会自动传递给实例构造函数,可以从该构造函数中访问该实例,如下所示。
- 通过分配临时局部变量来创建类型的
12.8.17.3 对象初始值设定项
对象初始值设定项指定对象零个或多个字段、属性或索引元素的值。
object_initializer
: '{' member_initializer_list? '}'
| '{' member_initializer_list ',' '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: initializer_target '=' initializer_value
;
initializer_target
: identifier
| '[' argument_list ']'
;
initializer_value
: expression
| object_or_collection_initializer
;
对象初始值设定项由成员初始值设定项序列组成,由标记括 {
起来 }
,用逗号分隔。 每个 member_initializer 应指定初始化的目标。 标识符应为正在初始化的对象命名可访问的字段或属性,而括在方括号中的argument_list应为正在初始化的对象指定可访问索引器的参数。 对象初始值设定项包含同一字段或属性的多个成员初始值设定项是错误的。
注意:虽然不允许对象初始值设定项多次设置相同的字段或属性,但索引器没有此类限制。 对象初始值设定项可能包含引用索引器的多个初始值设定项目标,甚至可以多次使用相同的索引器参数。 end note
每个 initializer_target 后跟等号和表达式、对象初始值设定项或集合初始值设定项。 对象初始值设定项中的表达式无法引用它正在初始化的新创建的对象。
一个成员初始值设定项,该初始值设定项指定在等号之后的处理方式与目标赋值 (§12.21.2) 相同。
在等号后指定对象初始值设定项的成员初始值设定项是 嵌套对象初始值设定项,即嵌入对象的初始化。 嵌套对象初始值设定项中的赋值被视为对字段或属性成员的赋值,而不是为字段或属性分配新值。 嵌套对象初始值设定项不能应用于具有值类型的属性,也不能应用于具有值类型的只读字段。
在等号后指定集合初始值设定项的成员初始值设定项是嵌入集合的初始化。 初始值设定项中指定的元素将添加到目标引用的集合中,而不是将新集合分配给目标字段、属性或索引器。 目标应为满足 §12.8.17.4 中指定的要求的集合类型。
当初始值设定项目标引用索引器时,索引器的参数应始终准确计算一次。 因此,即使参数最终永远不会被使用(例如,由于空嵌套初始值设定项),它们也会评估其副作用。
示例:以下类表示具有两个坐标的点:
public class Point { public int X { get; set; } public int Y { get; set; } }
可以创建和初始化实例
Point
,如下所示:Point a = new Point { X = 0, Y = 1 };
这与
Point __a = new Point(); __a.X = 0; __a.Y = 1; Point a = __a;
其中
__a
是一个不可见且不可访问的临时变量。以下类显示了从两个点创建的矩形,以及实例的
Rectangle
创建和初始化:public class Rectangle { public Point P1 { get; set; } public Point P2 { get; set; } }
可以创建和初始化实例
Rectangle
,如下所示:Rectangle r = new Rectangle { P1 = new Point { X = 0, Y = 1 }, P2 = new Point { X = 2, Y = 3 } };
这与
Rectangle __r = new Rectangle(); Point __p1 = new Point(); __p1.X = 0; __p1.Y = 1; __r.P1 = __p1; Point __p2 = new Point(); __p2.X = 2; __p2.Y = 3; __r.P2 = __p2; Rectangle r = __r;
其中
__r
,__p1
是__p2
其他不可见且不可访问的临时变量。如果
Rectangle
构造函数分配两个嵌入Point
实例,则它们可用于初始化嵌入Point
实例,而不是分配新实例:public class Rectangle { public Point P1 { get; } = new Point(); public Point P2 { get; } = new Point(); }
以下构造可用于初始化嵌入
Point
实例,而不是分配新实例:Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } };
这与
Rectangle __r = new Rectangle(); __r.P1.X = 0; __r.P1.Y = 1; __r.P2.X = 2; __r.P2.Y = 3; Rectangle r = __r;
end 示例
12.8.17.4 集合初始值设定项
集合初始值设定项指定集合的元素。
collection_initializer
: '{' element_initializer_list '}'
| '{' element_initializer_list ',' '}'
;
element_initializer_list
: element_initializer (',' element_initializer)*
;
element_initializer
: non_assignment_expression
| '{' expression_list '}'
;
expression_list
: expression
| expression_list ',' expression
;
集合初始值设定项由元素初始值设定项序列组成,由标记括 {
起来, }
用逗号分隔。 每个元素初始值设定项指定要添加到要初始化的集合对象的元素,并包含一个表达式列表,这些表达式 {
由标记和 }
逗号分隔。 无需大括号即可编写单表达式元素初始值设定项,但不能是赋值表达式,以避免与成员初始值设定项不明确。 non_assignment_expression生产在 §12.22 中定义。
示例:下面是包含集合初始值设定项的对象创建表达式的示例:
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
end 示例
应用集合初始值设定项的集合对象应为实现 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
其他不可见且不可访问的临时变量。end 示例
12.8.17.5 数组创建表达式
array_creation_expression用于创建array_type的新实例。
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
第一个窗体的数组创建表达式分配类型数组实例,该实例从表达式列表中删除每个单独的表达式。
示例:数组创建表达式
new int[10,20]
生成一个类型的int[,]
数组实例,并且数组创建表达式新int[10][,]
生成一个类型的int[][,]
数组实例。 end 示例
表达式列表中的每个表达式应为类型 int
、 uint
类型、 long
或 ulong
隐式转换为这些类型的一个或多个。 每个表达式的值确定新分配的数组实例中相应维度的长度。 由于数组维度的长度应为非负值,因此在表达式列表中具有具有负值的常量表达式是编译时错误。
除了不安全的上下文(§23.2),数组的布局未指定。
如果第一个窗体的数组创建表达式包含数组初始值设定项,则表达式列表中的每个表达式应为常量,表达式列表指定的排名和维度长度应与数组初始值设定项的表达式匹配。
在第二种或第三种形式的数组创建表达式中,指定的数组类型或排名说明符的排名应与数组初始值设定项的排名匹配。 从数组初始值设定项的每个相应嵌套级别中的元素数推断各个维度长度。 因此,以下声明中的初始值设定项表达式
var a = new int[,] {{0, 1}, {2, 3}, {4, 5}};
与
var a = new int[3, 2] {{0, 1}, {2, 3}, {4, 5}};
第三种形式的数组创建表达式称为 隐式类型的数组创建表达式。 它类似于第二种形式,只是未显式指定数组的元素类型,而是确定为数组初始值设定项集中表达式集的最佳常用类型(§12.6.3.15)。 对于多维数组,即rank_specifier至少包含一个逗号的数组,此集包含嵌套array_initializers 中找到的所有表达式。
数组初始值设定项在 §17.7 中进一步介绍。
计算数组创建表达式的结果被分类为一个值,即对新分配的数组实例的引用。 数组创建表达式的运行时处理包括以下步骤:
- 按从左到右的顺序计算expression_list的维度长度表达式。 对每个表达式进行以下计算后,将执行隐式转换(§10.2)到以下类型之一:
int
、、uint
long
ulong
。 选择隐式转换的此列表中的第一个类型。 如果表达式的计算或后续隐式转换导致异常,则不会计算进一步的表达式,也不会执行进一步的步骤。 - 验证维度长度的计算值,如下所示:如果一个或多个值小于零,则会引发 a
System.OverflowException
,并且不执行其他步骤。 - 分配具有给定维度长度的数组实例。 如果没有足够的内存来分配新实例,则会引发 a,并且不会执行进一
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]; }
end 示例
注意:当数组数组具有“矩形”形状时,即当子数组的长度相同时,使用多维数组会更高效。 在上面的示例中,数组数组的实例化将创建 101 个对象,即一个外部数组和 100 个子数组。 相比之下,
int[,] a = new int[100, 5];
仅创建一个对象,一个二维数组,并在单个语句中完成分配。
end note
示例:下面是隐式类型化数组创建表达式的示例:
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[]
或者,其中一个元素可以强制转换为通用基类型,该类型随后将成为推断的元素类型。end 示例
隐式类型化数组创建表达式可以与匿名对象初始值设定项(§12.8.17.7)结合使用来创建匿名类型化的数据结构。
示例:
var contacts = new[] { new { Name = "Chris Smith", PhoneNumbers = new[] { "206-555-0101", "425-882-8080" } }, new { Name = "Bob Harris", PhoneNumbers = new[] { "650-555-0199" } } };
end 示例
12.8.17.6 委托创建表达式
delegate_creation_expression用于获取delegate_type的实例。
delegate_creation_expression
: 'new' delegate_type '(' expression ')'
;
委托创建表达式的参数应为方法组、匿名函数或编译时类型 dynamic
或 delegate_type的值。 如果参数是方法组,则它标识方法,并为实例方法标识要为其创建委托的对象。 如果参数是匿名函数,则直接定义委托目标的参数和方法主体。 如果参数是一个值,则它标识要为其创建副本的委托实例。
如果表达式具有编译时类型dynamic
,则delegate_creation_expression是动态绑定的(§12.8.17.6),并且以下规则使用表达式的运行时类型在运行时应用。 否则,规则在编译时应用。
新窗体delegate_creation_expression的绑定时处理,其中D
delegate_type且E
是表达式,包括以下步骤:D(E)
如果
E
为方法组,则委托创建表达式的处理方式与方法组转换(§10.8)E
D
相同。如果是
E
匿名函数,则委托创建表达式的处理方式与匿名函数转换(§10.7)E
D
相同。如果
E
为值,应与E
兼容(§20.2),D
结果是对新创建的委托的引用,其中包含调用E
的单项调用列表。
新窗体delegate_creation_expression的运行时处理,其中D
delegate_type,是E
表达式,包括以下步骤: D(E)
- 如果
E
为方法组,则委托创建表达式的计算结果为方法组转换(§10.8)。E
D
- 如果
E
为匿名函数,则将委托创建计算为从E
到D
(§10.7) 的匿名函数转换。 - 如果
E
值为 delegate_type:- 评估
E
。 如果此评估导致异常,则不会执行进一步的步骤。 - 如果值为
E
<a0/>,则会引发 a,并且不执行进一 System.NullReferenceException
步的步骤。 - 分配委托类型的
D
新实例。 如果没有足够的内存来分配新实例,则会引发 a,并且不会执行进一System.OutOfMemoryException
步的步骤。 - 使用调用
E
的单项调用列表初始化新的委托实例。
- 评估
委托的调用列表在实例化委托时确定,然后在委托的整个生存期内保持不变。 换句话说,创建委托后,无法更改委托的目标可调用实体。
注意:请记住,当两个委托合并或从另一个委托中删除一个委托时,新的委托结果;没有现有委托更改其内容。 end note
无法创建引用属性、索引器、用户定义的运算符、实例构造函数、终结器或静态构造函数的委托。
示例:如上所述,从方法组创建委托时,委托的参数列表和返回类型决定了要选择的重载方法。 在示例中
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
种方法,则会发生编译时错误。end 示例
12.8.17.7 匿名对象创建表达式
anonymous_object_creation_expression用于创建匿名类型的对象。
anonymous_object_creation_expression
: 'new' anonymous_object_initializer
;
anonymous_object_initializer
: '{' member_declarator_list? '}'
| '{' member_declarator_list ',' '}'
;
member_declarator_list
: member_declarator (',' member_declarator)*
;
member_declarator
: simple_name
| member_access
| null_conditional_projection_initializer
| base_access
| identifier '=' expression
;
匿名对象初始值设定项声明匿名类型并返回该类型的实例。 匿名类型是直接继承自 object
的无名称类类型。 匿名类型的成员是从用于创建该类型的实例的匿名对象初始值设定项推断出的只读属性序列。 具体而言,表单的匿名对象初始值设定项
new {
p₁ =
e₁ ,
p 日下午 =
e 也就是说 ,
… pv =
ev }
声明表单的匿名类型
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
属于同一匿名类型。end 示例
Equals
匿名类型的方法和GetHashcode
方法将替代继承自object
的方法,并且根据Equals
属性和GetHashcode
属性进行定义,以便同一匿名类型的两个实例在所有属性相等时且仅当其所有属性相等时才相等。
成员声明符可以缩写为简单名称(§12.8.4)、成员访问(§12.8.7)、null 条件投影初始值设定项 §12.8.8 或基本访问(§12.8.15)。 这称为 投影初始值设定项 ,是声明和赋给同名属性的简写。 具体而言,表单的成员声明符
«identifier»
、«expr» . «identifier»
和 «expr» ? . «identifier»
分别等效于以下内容:
«identifer» = «identifier»
、«identifier» = «expr» . «identifier»
和 «identifier» = «expr» ? . «identifier»
因此,在投影初始值设定项中,标识符同时选择分配值的字段或属性。 直观地说,投影初始值设定项不仅投影值,还投影值的名称。
12.8.18 typeof 运算符
该 typeof
运算符用于获取 System.Type
类型的对象。
typeof_expression
: 'typeof' '(' type ')'
| 'typeof' '(' unbound_type_name ')'
| 'typeof' '(' 'void' ')'
;
unbound_type_name
: identifier generic_dimension_specifier?
| identifier '::' identifier generic_dimension_specifier?
| unbound_type_name '.' identifier generic_dimension_specifier?
;
generic_dimension_specifier
: '<' comma* '>'
;
comma
: ','
;
typeof_expression的第一种typeof
形式由后跟括号类型的关键字组成。 此窗体的表达式的结果是 System.Type
所指示类型的对象。 对于任何给定类型,只有一个 System.Type
对象。 这意味着对于类型 T
, typeof(T) == typeof(T)
始终为 true。 此类型不能为 dynamic
。
第二种 形式的typeof_expression 包含一个 typeof
关键字,后跟带括号 的unbound_type_name。
注意:unbound_type_name与type_name(§7.8)非常相似,但unbound_type_name包含generic_dimension_specifier,其中type_name包含type_argument_lists。 end note
当typeof_expression的操作数是满足unbound_type_name和type_name语法的标记序列时,即它既不包含generic_dimension_specifier也不是type_argument_list,则令牌序列被视为type_name。 unbound_type_name的含义如下:
- 将标记序列转换为type_name,方法是将每个generic_dimension_specifier替换为具有与每个type_argument具有相同数量的逗号和关键字
object
的type_argument_list。 - 计算生成的 type_name,同时忽略所有类型参数约束。
- unbound_type_name解析为与生成的构造类型(§8.4)关联的未绑定泛型类型。
类型名称为可以为 null 的引用类型是错误的。
typeof_expression的结果是System.Type
生成的未绑定泛型类型的对象。
typeof_expression的第三种typeof
形式由后跟括号void
关键字的关键字组成。 此窗体的表达式的结果是 System.Type
表示缺少类型的对象。 返回的类型 typeof(void)
对象与为任意类型返回的类型对象不同。
注意:此特殊
System.Type
对象在类库中非常有用,这些对象允许反射到语言中的方法,这些方法希望通过实例System.Type
表示任何方法(包括void
方法)的返回类型。 end note
typeof
运算符可用于类型参数。 如果已知类型名称为可以为 null 的引用类型,则为编译时错误。 结果是 System.Type
绑定到类型参数的运行时类型的对象。 如果运行时类型是可为 null 的引用类型,则结果是相应的不可为 null 的引用类型。 typeof
该运算符还可用于构造的类型或未绑定泛型类型(§8.4.4)。 System.Type
未绑定泛型类型的对象与实例类型 (§15.3.2) 的对象不同System.Type
。 实例类型始终是运行时的已关闭构造类型,因此其 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>)
结果。end 示例
12.8.19 sizeof 运算符
运算符 sizeof
返回给定类型的变量占用的 8 位字节数。 指定为 sizeof 的操作数的类型应为 unmanaged_type (§8.8)。
sizeof_expression
: 'sizeof' '(' unmanaged_type ')'
;
对于某些预定义类型, sizeof
运算符生成常量 int
值,如下表所示:
Expression | Result |
---|---|
sizeof(sbyte) |
1 |
sizeof(byte) |
1 |
sizeof(short) |
2 |
sizeof(ushort) |
2 |
sizeof(int) |
4 |
sizeof(uint) |
4 |
sizeof(long) |
8 |
sizeof(ulong) |
8 |
sizeof(char) |
2 |
sizeof(float) |
4 |
sizeof(double) |
8 |
sizeof(bool) |
1 |
sizeof(decimal) |
16 |
对于枚举类型 T
,表达式 sizeof(T)
的结果是一个常量值,等于其基础类型的大小,如上所述。 对于所有其他操作数类型,sizeof
运算符在 §23.6.9 中指定。
12.8.20 已选中和未选中的运算符
和checked
unchecked
运算符用于控制整型算术运算和转换的溢出检查上下文。
checked_expression
: 'checked' '(' expression ')'
;
unchecked_expression
: 'unchecked' '(' expression ')'
;
该 checked
运算符计算已检查上下文中的包含表达式,运算符 unchecked
在未选中的上下文中计算包含的表达式。 checked_expression或unchecked_expression与parenthesized_expression(§12.8.5)完全对应,但在给定的溢出检查上下文中计算包含的表达式除外。
溢出检查上下文也可以通过 checked
语句 unchecked
(§13.12)进行控制。
以下操作受已检查和未选中运算符和语句建立的溢出检查上下文的影响:
- 当操作数为整型或枚举类型时,预定义运算符和
--
运算符(§12.8.16 和 §12.9.6)。++
- 当操作数为整型时,预定义
-
的一元运算符(§12.9.3)。 - 当两个操作数均为整型或枚举类型时,预定义
+
运算符、-
*
二/
进制运算符(§12.10)。 - 显式数值转换(§10.3.2)从一个整型或枚举类型转换为另一个整型或枚举类型,或者从
float
double
整数或枚举类型转换到另一个整数或枚举类型。
当上述操作之一生成在目标类型中表示太大的结果时,将执行该操作的上下文控制生成的行为:
checked
在上下文中,如果操作是常量表达式(§12.23),则会发生编译时错误。 否则,在运行时执行操作时,将引发 aSystem.OverflowException
。unchecked
在上下文中,通过放弃不符合目标类型的任何高序位来截断结果。
对于非常量表达式(§12.23)(在运行时计算的表达式)未由任何checked
unchecked
运算符或语句括起来,除非外部因素(如编译器开关和执行环境配置)调用检查已检查的计算,否则默认溢出检查上下文将被取消选中。
对于常量表达式(§12.23)(可在编译时完全计算的表达式),始终检查默认溢出检查上下文。 除非在上下文中 unchecked
显式放置常量表达式,否则在表达式的编译时计算期间发生的溢出始终会导致编译时错误。
匿名函数的正文不受匿名unchecked
函数发生上下文的影响checked
。
示例:在以下代码中
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
。end 示例
示例:在以下代码中
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
,因此不会报告溢出。end 示例
和checked
unchecked
运算符仅影响在“”(
和“”)
标记中文本包含的这些操作的溢出检查上下文。 运算符对作为计算包含表达式的结果调用的函数成员没有影响。
示例:在以下代码中
class Test { static int Multiply(int x, int y) => x * y; static int F() => checked(Multiply(1000000, 1000000)); }
在 F 中的
checked
使用不会影响 inMultiply
的x * y
计算,因此x * y
在默认溢出检查上下文中进行评估。end 示例
在 unchecked
十六进制表示法中编写带符号整型类型的常量时,运算符很方便。
示例:
class Test { public const int AllBits = unchecked((int)0xFFFFFFFF); public const int HighBit = unchecked((int)0x80000000); }
上述两个十六进制常量都是类型
uint
。 由于常量不在范围之外int
,没有unchecked
运算符,因此要int
生成的转换将生成编译时错误。end 示例
注意:
checked
unchecked
运算符和语句允许程序员控制某些数值计算的某些方面。 但是,某些数值运算符的行为取决于其操作数的数据类型。 例如,乘以两个小数始终会导致溢出异常,即使在显式未选中的构造中也是如此。 同样,相乘两个浮点永远不会导致溢出异常,即使在显式检查的构造中也是如此。 此外,其他运算符永远不会受到检查模式的影响,无论是默认还是显式。 end note
12.8.21 默认值表达式
默认值表达式用于获取类型的默认值 (§9.3)。
default_value_expression
: explictly_typed_default
| default_literal
;
explictly_typed_default
: 'default' '(' type ')'
;
default_literal
: 'default'
;
default_literal表示默认值(§9.3)。 它没有类型,但可以通过默认文本转换(§10.2.16)转换为任何类型。
default_value_expression的结果是explictly_typed_default中显式类型的默认值(§9.3),或default_value_expression转换的目标类型。
如果类型为下列值之一,则default_value_expression是常量表达式(§12.23):
- 引用类型
- 已知为引用类型的类型参数(§8.2):
- 以下值类型之一:
sbyte
、、、short
byte
ushort
、uint
int
、long
char
ulong
、 或float
double
decimal
bool,
- 任何枚举类型。
12.8.22 堆栈分配
堆栈分配表达式从执行堆栈分配内存块。 执行 堆栈 是存储局部变量的内存区域。 执行堆栈不是托管堆的一部分。 当当前函数返回时,将自动恢复用于本地变量存储的内存。
堆栈分配表达式的安全上下文规则在 §16.4.12.7 中介绍。
stackalloc_expression
: 'stackalloc' unmanaged_type '[' expression ']'
| 'stackalloc' unmanaged_type? '[' constant_expression? ']'
stackalloc_initializer
;
stackalloc_initializer
: '{' stackalloc_initializer_element_list '}'
;
stackalloc_initializer_element_list
: stackalloc_element_initializer (',' stackalloc_element_initializer)* ','?
;
stackalloc_element_initializer
: expression
;
仅在两个上下文中允许stackalloc_expression:
- local_variable_declaration的初始化表达式
E
(§13.6.2);和 - 简单赋值(§12.21.2)的右操作数表达式
E
(§12.21.2)本身作为expression_statement(§13.7)
在这两个上下文中,仅允许stackalloc_expression发生为:
E
或- conditional_expression(§12.18)的第二个和/或第三个操作数,它本身是整个
E
操作数。
unmanaged_type (§8.8) 指示将存储在新分配位置中的项的类型,表达式指示这些项目的数目。 这些组合在一起,指定所需的分配大小。 表达式的类型应隐式转换为该类型int
。
由于堆栈分配的大小不能为负,因此,将项数指定为 计算结果为负值的constant_expression 是编译时错误。
在运行时,如果要分配的项数为负值,则行为是未定义的。 如果为零,则不进行分配,返回的值是实现定义的。 如果没有足够的可用内存来分配项,则会引发项 System.StackOverflowException
。
存在stackalloc_initializer时:
- 如果 省略unmanaged_type ,则会根据最佳通用类型(§12.6.3.15)的规则(§12.6.3.15)推断 出stackalloc_element_initializer集。
- 如果省略constant_expression,则推断为stackalloc_element_initializer数。
- 如果 存在constant_expression ,它应等于 stackalloc_element_initializer数。
每个 stackalloc_element_initializer 应隐式转换为 unmanaged_type (§10.2)。 stackalloc_element_initializer以递增顺序初始化已分配内存中的元素,从索引为零的元素开始。 如果没有 stackalloc_initializer,则未定义新分配内存的内容。
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)返回到已分配块的项,并检查范围。
注意:在不安全的代码中发生时,stackalloc_expression的结果可能的类型不同,请参阅 (§23.9)。 end note
不允许在或块中使用catch
堆栈分配初始值设定项(§13.11)。finally
注意:无法显式释放使用分配的
stackalloc
内存。 end note
当函数成员返回时,在函数成员执行期间创建的所有堆栈分配内存块都会自动放弃。
除运算符外 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>
,如下所示。 end 示例
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
的调用在语法上不明确。 出于兼容性原因,如果名称的名称查找(§12.8.4)成功,则无论调用是否有效,表达式都将被视为invocation_expression。nameof
否则,它是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
表达式的结果,假设命名空间中System.Collections.Generic
声明了泛型类型List<T>
: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”。 end 示例
12.8.24 匿名方法表达式
anonymous_method_expression是定义匿名函数的两种方法之一。 这些内容在 §12.19 中进一步介绍。
12.9 一元运算符
12.9.1 常规
+
!
-
仅限逻辑求反 §12.9.4)、~
、、++
、--
强制转换和await
运算符称为一元运算符。
注意:后缀 null-forgiving 运算符(§12.8.9),
!
由于其编译时间和不可重载的唯一性质,将从上面的列表中排除。 end note
unary_expression
: primary_expression
| '+' unary_expression
| '-' unary_expression
| logical_negation_operator unary_expression
| '~' unary_expression
| pre_increment_expression
| pre_decrement_expression
| cast_expression
| await_expression
| pointer_indirection_expression // unsafe code support
| addressof_expression // unsafe code support
;
pointer_indirection_expression(§23.6.2)和addressof_expression(§23.6.5)仅在不安全的代码(§23)中可用。
如果unary_expression的操作数具有编译时类型dynamic
,则会动态绑定 (§12.3.3.3)。 在这种情况下,unary_expression的编译时类型为dynamic
,下面所述的解决方法将使用操作数的运行时类型在运行时进行。
12.9.2 一元加运算符
对于窗体 +x
的操作,将应用一元运算符重载分辨率(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 预定义的一元加运算符为:
int operator +(int x);
uint operator +(uint x);
long operator +(long x);
ulong operator +(ulong x);
float operator +(float x);
double operator +(double x);
decimal operator +(decimal x);
对于每个运算符,结果只是操作数的值。
上面定义的未提升预定义一元运算符的提升形式(§12.4.8)也是预定义的。
12.9.3 一元减号运算符
对于窗体 –x
的操作,将应用一元运算符重载分辨率(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 预定义的一元减号运算符为:
整数求反:
int operator –(int x); long operator –(long x);
通过从零减去
X
计算结果。 如果值为X
操作数类型的最小可表示值(对于 为 <2int
⁶⁶ーlong
),则操作数类型的数学求反X
值在操作数类型内不可表示。 如果在上下文中checked
发生这种情况,则会引发 aSystem.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
。NaN
如果是x
,则结果也是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.8.9),而由同一词法标记(!
)表示,则不同。 end note
12.9.5 位补运算符
对于窗体 ~x
的操作,将应用一元运算符重载分辨率(§12.4.4)来选择特定的运算符实现。 操作数转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 预定义的按位补运算符为:
int operator ~(int x);
uint operator ~(uint x);
long operator ~(long x);
ulong operator ~(ulong x);
对于每个运算符,运算的结果是按位补数 x
。
每个枚举类型 E
隐式提供以下按位补运算符:
E operator ~(E x);
计算~x
的结果(其中X
是具有基础类型的U
枚举类型的E
表达式)与计算(E)(~(U)x)
完全相同,只不过转换到E
的执行方式与上下文(unchecked
§12.8.20)一样。
上面定义的未提升预定义按位补运算符的提升形式(§12.4.8)也是预定义的。
12.9.6 前缀递增和递减运算符
pre_increment_expression
: '++' unary_expression
;
pre_decrement_expression
: '--' unary_expression
;
前缀递增或递减操作的操作数应是分类为变量、属性访问或索引器访问的表达式。 操作的结果是与操作数类型相同的值。
如果前缀递增或递减操作的操作数是属性或索引器访问,则属性或索引器应同时具有 get 和 set 访问器。 如果情况并非如此,则会发生绑定时错误。
一元运算符重载解析(§12.4.4)应用于选择特定的运算符实现。 ++
预定义的运算符--
和运算符适用于以下类型:sbyte
、、byte
、short
、ushort
、int
char
uint
ulong
float
long
double
和decimal
任意枚举类型。 预定义 ++
运算符返回通过添加到 1
操作数生成的值,预定义 --
运算符返回通过从操作数中减去 1
生成的值。 checked
在上下文中,如果此加法或减法的结果超出结果类型的范围,并且结果类型是整型类型或枚举类型,则会引发 aSystem.OverflowException
。
应有从所选一元运算符的返回类型到unary_expression类型的隐式转换,否则会发生编译时错误。
窗体 ++x
的前缀递增或递减操作的运行时处理,或 --x
由以下步骤组成:
- 如果
x
分类为变量:x
的计算结果为生成变量。x
值将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。- 运算符返回的值将转换为类型
x
。 生成的值存储在由计算x
结果给出的位置。 - 并成为操作的结果。
- 如果
x
分类为属性或索引器访问:- 实例表达式(如果
x
不是static
)和参数列表(如果x
为索引器访问)与x
计算,结果用于后续 get 和 set 访问器调用。 - 调用 get 访问器
x
。 - get 访问器返回的值将转换为所选运算符的操作数类型,并使用此值作为其参数调用运算符。
- 运算符返回的值将转换为类型
x
。 使用此值作为其值参数调用 的x
set 访问器。 - 此值也将成为操作的结果。
- 实例表达式(如果
和++
--
运算符还支持后缀表示法(§12.8.16)。 操作之前的结果 x++
或 x--
为值 x
,而操作后的结果 ++x
或 --x
值 x
。 在任一情况下, x
自身在操作后具有相同的值。
可以使用后缀或前缀表示法调用运算符 ++
或运算符 --
实现。 不能为这两个表示法使用单独的运算符实现。
还预定义了上面定义的未提升预定义前缀增量和递减运算符的提升形式(§12.4.8)。
12.9.7 强制转换表达式
cast_expression用于将表达式显式转换为给定类型。
cast_expression
: '(' type ')' unary_expression
;
窗体的cast_expression,其中T
是一种类型,E
并且是一个unary_expression,它执行到类型的T
值的E
显式转换(§10.3)。(T)E
如果不存在从绑定 E
时 T
到的显式转换,则会发生绑定时错误。 否则,结果是由显式转换生成的值。 即使表示变量, E
结果也始终被分类为值。
cast_expression的语法会导致某些语法歧义。
示例:表达式
(x)–y
可以解释为cast_expression(类型强制转换x
–y
)或additive_expression与parenthesized_expression(计算值x – y
)。 end 示例
若要解决cast_expression歧义,存在以下规则:仅当以下至少一个为 true 时,括号中包含的一个或多个标记(§6.4)的序列被视为cast_expression的开头:
- 标记序列是类型的正确语法,但不适用于表达式。
- 标记序列是类型的正确语法,右括号后面的标记是标记“”、“标记”
~
、标记“(
!
、标识符(§6.4.3)、文本(§6.4.5)或任何关键字(§6.4.4.4 除外)。is
as
上述术语“正确语法”仅表示令牌序列应符合特定的语法生产。 它特别不考虑任何构成标识符的实际含义。
示例:如果
x
和y
为标识符,则x.y
为类型的正确语法,即使x.y
实际上没有表示类型也是如此。 end 示例
注意:从消除歧义规则中,它遵循以下条件:如果
x
和为标识符、(x)(y)
(x)y
标识符y
、以及(x)(-y)
cast_expression,但(x)-y
不是,即使x
标识类型也是如此。 但是,如果x
关键字标识预定义类型(例如int
),则所有四种形式都是 cast_expression的(因为此类关键字本身不可能是表达式)。 end note
12.9.8 Await 表达式
12.9.8.1 常规
运算符 await
用于暂停对封闭异步函数的计算,直到操作数表示的异步操作完成。
await_expression
: 'await' unary_expression
;
仅在异步函数(§15.15)的正文中允许await_expression。 在最近的封闭异步函数中, 不应在这些位置发生await_expression :
- 嵌套(非异步)匿名函数内部
- 在lock_statement块 内
- 在匿名函数转换为表达式树类型(§10.7.3)
- 在不安全的上下文中
注意:await_expression无法在query_expression内的大多数位置发生,因为这些位置经过语法转换以使用非异步 lambda 表达式。 end note
在异步函数中, await
不应用作 available_identifier ,尽管可以使用逐字标识符 @await
。 因此,await_expression与涉及标识符的各种表达式之间没有语法歧义。 在异步函数之外, await
充当普通标识符。
await_expression的操作数称为任务。 它表示在计算await_expression时可能或可能尚未完成的异步操作。 运算符的目的是 await
暂停封闭异步函数的执行,直到等待的任务完成,然后获取其结果。
12.9.8.2 可等待表达式
需要等待await_expression的任务。 t
如果下列任一项保留,则表达式可等待:
t
是编译时类型dynamic
t
具有一个可访问的实例或扩展方法,GetAwaiter
不带参数,没有类型参数,以及以下所有参数都保留的返回类型A
:A
实现接口System.Runtime.CompilerServices.INotifyCompletion
(后INotifyCompletion
称为简洁性)A
具有类型可访问的可读实例属性IsCompleted
bool
A
具有无参数和无类型参数的可访问实例方法GetResult
该方法的目的是 GetAwaiter
获取 任务的等待程序 。 该类型A
称为 await 表达式的 awaiter 类型。
该属性的目的是 IsCompleted
确定任务是否已完成。 如果是这样,则无需暂停评估。
该方法的目的是 INotifyCompletion.OnCompleted
向任务注册“延续”;即,任务完成后将调用的委托(类型 System.Action
)。
该方法的目的是 GetResult
在任务完成后获取任务的结果。 此结果可能成功完成,可能具有结果值,也可能是方法引发的 GetResult
异常。
12.9.8.3 await 表达式分类
await t
表达式的分类方式与表达式(t).GetAwaiter().GetResult()
相同。 因此,如果返回类型 GetResult
为 void
, 则await_expression 分类为无。 如果它具有非void
返回类型 T
, 则await_expression 被归类为类型值 T
。
12.9.8.4 await 表达式的运行时计算
在运行时,表达式 await t
的计算方式如下:
- 通过计算表达式
(t).GetAwaiter()
来获取 awaitera
。 - 通过计算表达式
(a).IsCompleted
来获取 A。bool
b
false
如果是b
,则评估取决于是否a
实现接口System.Runtime.CompilerServices.ICriticalNotifyCompletion
(后称为ICriticalNotifyCompletion
简洁)。 此检查是在绑定时完成的;例如,如果编译时类型dynamic
为运行时a
,则为运行时;否则为编译时。 请r
表示恢复委托(§15.15):- 如果未
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 算术运算符
12.10.1 常规
*
、/
、%
+
和-
运算符称为算术运算符。
multiplicative_expression
: unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression
;
additive_expression
: multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression
;
如果算术运算符的操作数具有编译时类型 dynamic
,则表达式将动态绑定 (§12.3.3.3)。 在这种情况下,表达式的编译时类型为 dynamic
,下面所述的解决方法将在运行时使用具有编译时类型的操作数的运行时类型 dynamic
进行。
12.10.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
在上下文中,如果产品超出结果类型的范围,则会引发 aSystem.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
-0
NaN
NaN
NaN
-0
-0
+0
-0
+0
NaN
NaN
NaN
+∞
+∞
-∞
NaN
NaN
+∞
-∞
NaN
-∞
-∞
+∞
NaN
NaN
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(除非另有说明,但在 §12.10.2–§12.10.6 的浮点表中,使用“
+
”表示值为正;使用“”-
表示值为负;缺少符号意味着该值可能是正值或负数或没有符号(NaN)。十进制乘法:
decimal operator *(decimal x, decimal y);
如果生成的值的大小太大而无法以十进制格式表示,则会引发 a
System.OverflowException
。 由于舍入,即使两个操作数都不为零,结果也可能为零。 结果的刻度(在任何舍入之前)是两个操作数的刻度之和。 十进制乘法等效于使用类型的System.Decimal
乘法运算符。
上面定义的未提升预定义乘法运算符的提升形式(§12.4.8)也是预定义的。
12.10.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);
如果右操作数的值为零,则会引发 a
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
-0
NaN
-x
-z
+z
-∞
+∞
-0
+0
NaN
+0
+0
-0
NaN
NaN
+0
-0
NaN
-0
-0
+0
NaN
NaN
-0
+0
NaN
+∞
+∞
-∞
+∞
-∞
NaN
NaN
NaN
-∞
-∞
+∞
-∞
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
十进制除法:
decimal operator /(decimal x, decimal y);
如果右操作数的值为零,则会引发 a
System.DivideByZeroException
。 如果生成的值的大小太大而无法以十进制格式表示,则会引发 aSystem.OverflowException
。 由于舍入,即使第一个操作数不是零,结果也可能为零。 在任何舍入之前,结果的刻度是最接近首选刻度,将保留结果等于确切结果。 首选刻度x
是小数位数y
。小数除法等效于使用类型的
System.Decimal
除法运算符。
上面定义的未提升预定义除法运算符的提升形式(§12.4.8)也是预定义的。
12.10.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
为零,则会引发 aSystem.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
+z
NaN
NaN
+x
+x
NaN
-x
-z
-z
NaN
NaN
-x
-x
NaN
+0
+0
+0
NaN
NaN
+0
+0
NaN
-0
-0
-0
NaN
NaN
-0
-0
NaN
+∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
十进制余数:
decimal operator %(decimal x, decimal y);
如果右操作数的值为零,则会引发 a
System.DivideByZeroException
。 当引发(或子类)时System.ArithmeticException
,它是实现定义的。 在未引发异常x % y
的任何情况下x / y
,一个符合要求的实现不应引发异常。 结果的刻度(在任何舍入之前)是两个操作数的刻度越大,结果的符号(如果非零)与它x
相同。十进制余数等效于使用类型的
System.Decimal
余数运算符。注意:这些规则可确保在所有类型中,结果永远不会有左操作数的相反符号。 end note
上面定义的未提升预定义余数运算符的提升形式(§12.4.8)也是预定义的。
12.10.5 加法运算符
对于窗体 x + y
的操作,将应用二进制运算符重载分辨率(§12.4.5),以选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
下面列出了预定义的加法运算符。 对于数值和枚举类型,预定义加法运算符计算两个操作数的总和。 当一个或两个操作数的类型相同 string
时,预定义的加法运算符将连接操作数的字符串表示形式。
整数加法:
int operator +(int x, int y); uint operator +(uint x, uint y); long operator +(long x, long y); ulong operator +(ulong x, ulong y
checked
在上下文中,如果求和超出结果类型的范围,则会引发一个System.OverflowException
。unchecked
在上下文中,不会报告溢出,结果类型范围之外的任何重大高阶位将被丢弃。浮点添加:
float operator +(float x, float y); double operator +(double x, double y);
根据 IEC 60559 算术规则计算总和。 下表列出了非零有限值、零、无穷大和 NaN 的所有可能组合的结果。 在表中,
x
和y
是非零有限值,z
是x + y
的结果。 如果x
和y
度量值相同但异号,则z
为正零。 如果x + y
太大而无法表示目标类型,z
则为一个无穷大,其符号与x + y
相同。y
+0
-0
+∞
-∞
NaN
x
z
x
x
+∞
-∞
NaN
+0
y
+0
+0
+∞
–∞
NaN
-0
y
+0
-0
+∞
-∞
NaN
+∞
+∞
+∞
+∞
+∞
NaN
NaN
-∞
-∞
-∞
-∞
NaN
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
小数加法:
decimal operator +(decimal x, decimal y);
如果生成的值的大小太大而无法以十进制格式表示,则会引发 a
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
操作数都通过调用从类型object
继承的虚拟ToString
方法转换为其字符串表示形式。 如果ToString
返回null
,则替换空字符串。示例:
class Test { static void Main() { string s = null; Console.WriteLine("s = >" + s + "<"); // Displays s = >< int i = 1; Console.WriteLine("i = " + i); // Displays i = 1 float f = 1.2300E+15F; Console.WriteLine("f = " + f); // Displays f = 1.23E+15 decimal d = 2.900m; Console.WriteLine("d = " + d); // Displays d = 2.900 } }
注释中显示的输出是美国英语系统上的典型结果。 精确的输出可能取决于执行环境的区域设置。 字符串串联运算符本身在每种情况下的行为方式相同,但在
ToString
执行过程中隐式调用的方法可能会受到区域设置的影响。end 示例
字符串串联运算符的结果包括
string
左操作数的字符,后跟右操作数的字符。 字符串串联运算符永远不会返回值null
。 如果没有足够的可用内存来分配生成的字符串,则可能引发 ASystem.OutOfMemoryException
。委托组合。 每个委托类型都隐式提供以下预定义运算符,其中
D
委托类型如下:D operator +(D x, D y);
如果第一个操作数
null
是,则运算的结果是第二个操作数的值(即使也是null
)。 否则,如果第二个操作数为null
,则该操作的结果为第一个操作数的值。 否则,操作的结果是一个新的委托实例,其调用列表由第一个操作数的调用列表中的元素组成,后跟第二个操作数的调用列表中的元素。 也就是说,生成的委托的调用列表是两个操作数的调用列表的串联。注意:有关委托组合的示例,请参阅 §12.10.6 和 §20.6。 由于
System.Delegate
不是委托类型,因此未为其定义运算符 +。 end note
上面定义的未提升预定义加法运算符的提升形式(§12.4.8)也是预定义的。
12.10.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
在上下文中,如果差异超出结果类型的范围,则会引发 aSystem.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
+∞
-∞
NaN
x
z
x
x
-∞
+∞
NaN
+0
-y
+0
+0
-∞
+∞
NaN
-0
-y
-0
+0
-∞
+∞
NaN
+∞
+∞
+∞
+∞
NaN
+∞
NaN
-∞
-∞
-∞
-∞
-∞
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
(在上表中,
-y
条目表示反数y
,而不是值为负值)。十进制减法:
decimal operator –(decimal x, decimal y);
如果生成的值的大小太大而无法以十进制格式表示,则会引发 a
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
,则该操作的结果为第一个操作数的值。 - 否则,这两个操作数都表示非空调用列表(§20.2)。
- 如果列表比较相等,由委托相等运算符(§12.12.9)确定,则运算的结果为
null
。 - 否则,该操作的结果是一个新的调用列表,其中包含第一个操作数的列表,其中删除了第二个操作数的条目,前提是第二个操作数的列表是第一个操作数的子列表。 (若要确定子列表相等性,将比较相应的条目作为委托相等运算符。如果第二个操作数的列表与第一个操作数列表中连续条目的多个子列表匹配,则会删除连续条目的最后一个匹配子列表。
- 否则,操作的结果是左操作数的值。
- 如果列表比较相等,由委托相等运算符(§12.12.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 } }
end 示例
- 如果第一个操作数为
上面定义的未提升预定义减法运算符的提升形式(§12.4.8)也是预定义的。
12.11 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
范围之外的高阶位,剩余位左移,低序空位位置设置为零。向右移动:
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
。 end 示例
上面定义的未提升预定义换班运算符的提升形式(§12.4.8)也是预定义的。
12.12 关系运算符和类型测试运算符
12.12.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
右操作数必须首先作为类型进行测试,然后作为可能跨越多个标记的表达式。 如果操作数是 expreesion,则模式表达式的优先级必须至少为 shift_expression。 end note
is
运算符在 §12.12.12 中介绍,as
运算符在 §12.12.13 中介绍。
==
、!=
、<
>
<=
和>=
运算符是比较运算符。
如果将default_literal(§12.8.21)用作操作数<
,>
<=
则>=
会发生编译时错误。
如果将default_literal用作或!=
运算符的两个==
操作数,则会发生编译时错误。 如果将default_literal用作或as
运算符的is
左操作数,则会发生编译时错误。
如果比较运算符的操作数具有编译时类型 dynamic
,则表达式将动态绑定 (§12.3.3.3)。 在这种情况下,表达式的编译时类型为 dynamic
,下面所述的解决方法将在运行时使用具有编译时类型的操作数的运行时类型 dynamic
进行。
对于窗体 x «op» y
的操作,其中 «op» 是比较运算符,将应用重载分辨率 (§12.4.5) 以选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。 如果equality_expression的两个操作数都是null
文本,则不执行重载解析,并且表达式的计算结果为常量值true
,或者false
根据运算符==
是还是!=
。
以下子项描述了预定义的比较运算符。 所有预定义的比较运算符都返回布尔类型的结果,如下表所述。
操作 | Result |
---|---|
x == y |
true 如果 x 等于 y , false 则为 |
x != y |
true 如果 x 不等于 y , false 则为 |
x < y |
如果 x 小于 y ,则为 true ;否则为 false |
x > y |
如果 x 大于 y ,则为 true ;否则为 false |
x <= y |
如果 x 小于等于 y ,则为 true ;否则为 false |
x >= y |
如果 x 大于等于 y ,则为 true ;否则为 false |
12.12.2 整数比较运算符
预定义的整数比较运算符为:
bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
bool operator !=(int x, int y);
bool operator !=(uint x, uint y);
bool operator !=(long x, long y);
bool operator !=(ulong x, ulong y);
bool operator <(int x, int y);
bool operator <(uint x, uint y);
bool operator <(long x, long y);
bool operator <(ulong x, ulong y);
bool operator >(int x, int y);
bool operator >(uint x, uint y);
bool operator >(long x, long y);
bool operator >(ulong x, ulong y);
bool operator <=(int x, int y);
bool operator <=(uint x, uint y);
bool operator <=(long x, long y);
bool operator <=(ulong x, ulong y);
bool operator >=(int x, int y);
bool operator >=(uint x, uint y);
bool operator >=(long x, long y);
bool operator >=(ulong x, ulong y);
每个运算符比较两个整数操作数的数值,并返回一个 bool
值,该值指示特定关系 true
是还是 false
。
上面定义的未提升预定义整数比较运算符的提升形式(§12.4.8)也是预定义的。
12.12.3 浮点比较运算符
预定义的浮点比较运算符为:
bool operator ==(float x, float y);
bool operator ==(double x, double y);
bool operator !=(float x, float y);
bool operator !=(double x, double y);
bool operator <(float x, float y);
bool operator <(double x, double y);
bool operator >(float x, float y);
bool operator >(double x, double y);
bool operator <=(float x, float y);
bool operator <=(double x, double y);
bool operator >=(float x, float y);
bool operator >=(double x, double y);
运算符根据 IEC 60559 标准的规则比较操作数:
如果任一操作数为 NaN,则结果false
针对除结果true
以外的!=
所有运算符。 对于任意两个操作数, x != y
始终生成与 !(x == y)
.. 但是,当一个操作数或两个操作数为 NaN 时,<
和 >
>=
<=
运算符不会生成与相反运算符的逻辑否定相同的结果。
示例:如果任一
x
项为y
NaN,则x < y
false
为 NaN,但!(x >= y)
为true
。 end 示例
当两个操作数都不为 NaN 时,运算符将两个浮点操作数的值与排序进行比较
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
其中 min
,可以是 max
以给定浮点格式表示的最小和最大正有限值。 此排序的明显效果是:
- 负零和正零被视为相等。
- 负无穷大被视为小于所有其他值,但等于另一个负无穷大。
- 正无穷大被视为大于所有其他值,但等于另一个正无穷大。
上面定义的未提升预定义浮点比较运算符的提升形式(§12.4.8)也是预定义的。
12.12.4 十进制比较运算符
预定义的小数比较运算符为:
bool operator ==(decimal x, decimal y);
bool operator !=(decimal x, decimal y);
bool operator <(decimal x, decimal y);
bool operator >(decimal x, decimal y);
bool operator <=(decimal x, decimal y);
bool operator >=(decimal x, decimal y);
每个运算符比较两个十进制操作数的数值,并返回一个 bool
值,该值指示特定关系 true
是还是 false
。 每个小数比较等效于使用类型的 System.Decimal
相应关系运算符或相等运算符。
上面定义的未提升的预定义小数比较运算符的提升形式(§12.4.8)也是预定义的。
12.12.5 布尔相等运算符
预定义的布尔相等运算符为:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
其结果==
为两者x
以及true
y
两者或两者。y
false
x
true
否则,结果为 false
。
其结果!=
为两者x
以及true
y
两者或两者。y
false
x
false
否则,结果为 true
。 当操作数的类型为类型 bool
时, !=
运算符将生成与 ^
运算符相同的结果。
上面定义的未提升预定义布尔相等运算符的提升形式(§12.4.8)也是预定义的。
12.12.6 枚举比较运算符
每个枚举类型都隐式提供以下预定义的比较运算符
bool operator ==(E x, E y);
bool operator !=(E x, E y);
bool operator <(E x, E y);
bool operator >(E x, E y);
bool operator <=(E x, E y);
bool operator >=(E x, E y);
计算x «op» y
的结果,其中 x 和 y 是具有基础类型的U
枚举类型的E
表达式,«op» 是比较运算符之一,与计算((U)x) «op» ((U)y)
完全相同。 换句话说,枚举类型比较运算符只是比较两个操作数的基础整数值。
上面定义的未提升的预定义枚举比较运算符的提升形式(§12.4.8)也是预定义的。
12.12.7 引用类型相等运算符
每个类类型 C
都隐式提供以下预定义的引用类型相等运算符:
bool operator ==(C x, C y);
bool operator !=(C x, C y);
除非为 存在预定义的相等运算符,否则为 C
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 的值类型,则结果==
为 <!=
true
a0/>。 - 如果运行时
T
为可以为 null 的值类型,则结果根据HasValue
操作数的属性计算,如 (§12.12.10) 中所述。 - 如果运行时
T
是引用类型,则结果为true
操作数,false
否则为操作数null
。
- 如果运行时
除非其中一个条件为 true,否则会发生绑定时错误。
注意:这些规则的显著影响包括:
- 使用预定义的引用类型相等运算符比较两个已知在绑定时不同引用是一个绑定时错误。 例如,如果操作数的绑定时类型是两个类类型,如果两个操作数都不派生自另一个类类型,则两个操作数不可能引用同一对象。 因此,该操作被视为绑定时错误。
- 预定义的引用类型相等运算符不允许比较值类型操作数(除非与专门处理的类型参数进行比较
null
)。- 预定义引用类型相等运算符的操作数永远不会装箱。 执行此类装箱操作毫无意义,因为对新分配的装箱实例的引用必然不同于所有其他引用。
对于窗体
x == y
的操作,或者x != y
,如果存在任何适用的用户定义operator ==
或operator !=
存在,运算符重载解析规则(§12.4.5)将选择该运算符,而不是预定义的引用类型相等运算符。 始终可以通过显式将一个或两个操作数强制转换为类型object
来选择预定义的引用类型相等运算符。end note
示例:以下示例检查非约束类型参数类型的参数是否为
null
。class C<T> { void F(T x) { if (x == null) { throw new ArgumentNullException(); } ... } }
x == null
即使T
可以表示不可为 null 的值类型,也允许构造,结果只是定义为false
非可以为 null 的值类型。T
end 示例
对于窗体 x == y
的操作,或者 x != y
,如果适用 operator ==
或 operator !=
存在,运算符重载解析(§12.4.5)规则将选择该运算符,而不是预定义的引用类型相等运算符。
注意:始终可以通过将两个操作数显式转换为类型
object
来选择预定义的引用类型相等运算符。 end note
示例:示例
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
时,会选择预定义的字符串相等运算符(§12.12.8)。 其余比较所有输出False
,因为当任一操作数具有绑定时类型为 <a0/> 时,类型中的 string
重载operator ==
不适用。请注意,上述技术对值类型没有意义。 示例
class Test { static void Main() { int i = 123; int j = 123; Console.WriteLine((object)i == (object)j); } }
输出
False
,因为强制转换创建对两个单独的装箱int
值实例的引用。end 示例
12.12.8 字符串相等运算符
预定义的字符串相等运算符为:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
string
当下列值之一为 true 时,两个值被视为相等:
- 这两个值都是
null
。 - 这两个值都是对
null
每个字符位置具有相同长度和相同字符的字符串实例的非引用。
字符串相等运算符比较字符串值,而不是字符串引用。 当两个单独的字符串实例包含完全相同的字符序列时,字符串的值相等,但引用不同。
注意:如 §12.12.7 中所述,引用类型相等运算符可用于比较字符串引用而不是字符串值。 end note
12.12.9 委托相等运算符
预定义委托相等运算符为:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
两个委托实例被视为相等,如下所示:
- 如果任一委托实例是
null
,则它们仅在两个null
实例都是时相等的。 - 如果委托具有不同的运行时类型,则它们永远不会相等。
- 如果两个委托实例都有调用列表(§20.2),则这些实例仅在调用列表的长度相同时才相等,并且其中一个调用列表中的每个条目都等于相应条目(如下所示),按顺序在其他调用列表中。
以下规则控制调用列表条目的相等性:
- 如果两个调用列表条目都引用相同的静态方法,则条目相等。
- 如果两个调用列表项都引用同一目标对象(由引用相等运算符定义)上的同一非静态方法,则这些条目是相等的。
- 允许(但不要求)相等(可能为空)的调用列表项从语义上相同的匿名函数(§12.19)的计算中生成。
如果运算符重载解析为任一委托相等运算符,并且两个操作数的绑定时间类型都是委托类型,而不是 §20 System.Delegate
中所述,并且绑定类型操作数类型之间没有标识转换,则会发生绑定时错误。
注意:此规则阻止由于引用不同类型的委托实例而无法将非
null
值视为相等的比较。 end note
12.12.10 可以为 null 的值类型和 null 文本之间的相等运算符
和==
!=
运算符允许一个操作数是可为 null 的值类型,另一个操作数是null
文本,即使操作不存在预定义运算符或用户定义的运算符(以未提升或提升的形式)。。
对于其中一个窗体的操作
x == null null == x x != null null != x
其中 x
,如果运算符重载解析(§12.4.5)找不到适用的运算符,则结果将改为从 HasValue
其属性 x
计算。 具体而言,前两种形式已转换为 !x.HasValue
,最后两种形式将转换为 x.HasValue
。
12.12.11 元组相等运算符
元组相等运算符按词法顺序对元组操作数的元素应用成对。
如果每个操作数和某个或运算符被分类为元组或元组类型为值(§8.3.11),则运算符为元组相等运算符。!=
==
y
x
如果将操作数 e
分类为元组,则元素 e1...en
应是计算元组表达式的元素表达式的结果。 否则,如果e
为元组类型的值,则元素应为t.Item1...t.Itemn
t
计算e
结果。
操作数 x
和 y
元组相等运算符应具有相同的仲裁,或发生编译时错误。 对于每个元素 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.12.12 Is 运算符
运算符有两种形式 is
。 一个是 is-type 运算符,它右侧有一个类型。 另一个是 is-pattern 运算符,它右侧有一个模式。
12.12.12.1 is-type 运算符
is-type 运算符用于检查对象的运行时类型是否与给定类型兼容。 检查在运行时执行。 操作 E is T
的结果(其中 E
是表达式, T
是一种非 dynamic
类型)是一个布尔值,指示是否 E
为非 null,并且可以通过引用转换、装箱转换、取消装箱转换、包装转换或取消包装转换成功转换为类型 T
。
操作的计算方式如下:
- 如果是
E
匿名函数或方法组,则会发生编译时错误 - 如果
E
为文本,或者其值为E
null
,则结果为false
null
。 - 否则:
- 让我们
R
成为 . 的E
运行时类型。 - 让我们
D
从R
中派生,如下所示: - 如果
R
为可为 null 的值类型,D
则为基础类型R
。 - 否则
D
为R
。 - 结果取决于
D
以下T
情况: - 如果
T
为引用类型,则结果为true
:- 标识转换存在于 <
/> 之间 D
D
是引用类型和从存在到T
存在的隐式引用转换D
,或者- 任一类型:
D
是值类型和装D
箱转换,从T
存在到存在。
或者:D
是一个值类型,T
是一种接口类型,由D
它实现。
- 标识转换存在于 <
- 如果
T
为可以为 null 的值类型,则结果是true
D
基础类型的值类型T
。 - 如果
T
为不可为 null 的值类型,则结果为true
ifD
和T
are the same type。 - 否则,结果为
false
。
运算符不考虑 is
用户定义的转换。
注意:在
is
运行时计算运算符时,所有类型参数都已被替换,并且没有开放类型(§8.4.3)需要考虑。 end note
注意:
is
可以在编译时类型和转换方面理解运算符,如下所示,其中C
编译时类型E
为:
- 如果编译时类型与编译时类型
e
相同,或者隐式引用转换(§10.2.8)、装箱转换(§10.2.9)、包装转换(§10.6)或显式解包转换(§10.6)从编译时类型E
存在到T
:T
- 如果
C
为不可为 null 的值类型,则操作的结果为true
。- 否则,运算的结果等效于计算
E != null
。- 否则,如果显式引用转换(§10.3.5)或取消装箱转换(§10.3.7)存在
C
,T
或者如果C
或T
为开放类型(§8.4.3),则运行时检查应为 peformed。- 否则,无法对类型
T
进行引用、装箱、包装或解包转换E
,并且操作的结果为false
。 编译器可以根据编译时类型实现优化器。end note
12.12.12.2 is-pattern 运算符
is-pattern 运算符用于检查表达式计算的值是否与给定模式(§11)匹配。 检查在运行时执行。 如果值与模式匹配,则 is-pattern 运算符的结果为 true;否则为 false。
对于窗体 E is P
的表达式,其中 E
是类型 T
的关系表达式, P
并且是一种模式,如果以下任一保留,则为编译时错误:
E
不指定值或没有类型。- 该模式
P
不适用于类型T
(§11.2)。
12.12.13 作为运算符
运算符 as
用于将值显式转换为给定引用类型或可以为 null 的值类型。 与强制转换表达式(§12.9.7)不同, 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)、显式引用(§10.3.5)或包装 (§8.3.12) 转换存在。
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
。end 示例
12.13 逻辑运算符
12.13.1 常规
运算符&,
^
和|
运算符称为逻辑运算符。
and_expression
: equality_expression
| and_expression '&' equality_expression
;
exclusive_or_expression
: and_expression
| exclusive_or_expression '^' and_expression
;
inclusive_or_expression
: exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression
;
如果逻辑运算符的操作数具有编译时类型 dynamic
,则表达式将动态绑定 (§12.3.3.3)。 在这种情况下,表达式的编译时类型为 dynamic
,下面所述的解决方法将在运行时使用具有编译时类型的操作数的运行时类型 dynamic
进行。
对于窗体 x «op» y
的操作,其中 «op» 是逻辑运算符之一,将应用重载解析(§12.4.5)以选择特定的运算符实现。 操作数将转换为所选运算符的参数类型,结果的类型是运算符的返回类型。
以下子项描述了预定义的逻辑运算符。
12.13.2 整数逻辑运算符
预定义的整数逻辑运算符为:
int operator &(int x, int y);
uint operator &(uint x, uint y);
long operator &(long x, long y);
ulong operator &(ulong x, ulong y);
int operator |(int x, int y);
uint operator |(uint x, uint y);
long operator |(long x, long y);
ulong operator |(ulong x, ulong y);
int operator ^(int x, int y);
uint operator ^(uint x, uint y);
long operator ^(long x, long y);
ulong operator ^(ulong x, ulong y);
运算符 &
计算两个操作数的按位逻辑 AND, |
运算符计算两个操作数的按位逻辑 OR, ^
运算符计算两个操作数的按位逻辑排他或。 这些操作无法溢出。
上面定义的未提升预定义整数逻辑运算符的提升形式(§12.4.8)也是预定义的。
12.13.3 枚举逻辑运算符
每个枚举类型 E
都隐式提供以下预定义的逻辑运算符:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
计算x «op» y
的结果(其中x
和y
表达式是具有基础类型的枚举类型的E
U
表达式),«op» 是逻辑运算符之一,与计算(E)((U)x «op» (U)y)
完全相同。 换句话说,枚举类型逻辑运算符只需对两个操作数的基础类型执行逻辑操作。
上面定义的未提升的预定义枚举逻辑运算符的提升形式(§12.4.8)也是预定义的。
12.13.4 布尔逻辑运算符
预定义的布尔逻辑运算符为:
bool operator &(bool x, bool y);
bool operator |(bool x, bool y);
bool operator ^(bool x, bool y);
如果 x
和 y
都为 true
,则 x & y
的结果为 true
。 否则,结果为 false
。
结果x | y
为true
:如果为或x
y
为 true
。 否则,结果为 false
。
其结果x ^ y
为 true
if true
x
和 y
isfalse
,或 x
is false
和 y
istrue
。 否则,结果为 false
。 当操作数的类型为类型 bool
时, ^
运算符将计算与 !=
运算符相同的结果。
12.13.5 可为 Null 的布尔值和 |运营商
可以为 null 的布尔类型bool?
可以表示三个值,true
false
以及null
。
与其他二进制运算符一样,逻辑运算符 &
的提升形式和 |
(§12.13.4) 也已预定义:
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
提升 &
的语义和 |
运算符由下表定义:
x |
y |
x & y |
x \| y |
---|---|---|---|
true |
true |
true |
true |
true |
false |
false |
true |
true |
null |
null |
true |
false |
true |
false |
true |
false |
false |
false |
false |
false |
null |
false |
null |
null |
true |
null |
true |
null |
false |
false |
null |
null |
null |
null |
null |
注意:该
bool?
类型在概念上类似于 SQL 中用于布尔表达式的三值类型。 上表遵循与 SQL 相同的语义,而将 §12.4.8 的规则应用于&
和|
运算符则不会。 §12.4.8 的规则已经为提升^
运算符提供类似于 SQL 的语义。 end note
12.14 条件逻辑运算符
12.14.1 常规
&&
和 ||
运算符称为条件逻辑运算符。 它们也称为“短路”逻辑运算符。
conditional_and_expression
: inclusive_or_expression
| conditional_and_expression '&&' inclusive_or_expression
;
conditional_or_expression
: conditional_and_expression
| conditional_or_expression '||' conditional_and_expression
;
和&&
||
运算符是条件版本的&
和|
运算符:
- 该操作对应于该操作
x && y
,但仅当y
不是false
时x
,才会计算该操作x & y
。 - 该操作对应于该操作
x || y
,但仅当y
不是true
时x
,才会计算该操作x | y
。
注意:短路使用“不真实”和“非 false”条件的原因是使用户定义的条件运算符能够在应用短路时定义。 用户定义的类型可能处于返回
false
和operator false
返回false
的状态operator true
。 在这些情况下,既&&
||
不会短路。 end note
如果条件逻辑运算符的操作数具有编译时类型 dynamic
,则表达式将动态绑定 (§12.3.3.3)。 在这种情况下,表达式的编译时类型为 dynamic
,下面所述的解决方法将在运行时使用具有编译时类型的操作数的运行时类型 dynamic
进行。
窗体x && y
的操作或x || y
通过应用重载解析(§12.4.5)进行处理,就像写入x & y
操作一x | y
样。 那么:
- 如果重载解析未能找到单个最佳运算符,或者如果重载解析选择预定义的整数逻辑运算符之一或可为 null 的布尔逻辑运算符(§12.13.5),则会发生绑定时错误。
- 否则,如果所选运算符是预定义的布尔逻辑运算符之一(§12.13.4),则按照 §12.14.2 中所述处理该操作。
- 否则,所选运算符是用户定义的运算符,并且按照 §12.14.3 中所述处理该操作。
无法直接重载条件逻辑运算符。 但是,由于条件逻辑运算符根据常规逻辑运算符进行评估,因此常规逻辑运算符的重载具有某些限制,也被视为条件逻辑运算符的重载。 这在 §12.14.3 中进一步介绍。
12.14.2 布尔条件逻辑运算符
如果操作数的类型或操作数&&
的类型,或者操作数的类型未定义适用operator &
,operator |
但确实定义了隐式转换,bool
则按如下方式处理该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.14.3 用户定义的条件逻辑运算符
声明适用用户定义operator &
operator |
或||
以下两种类型的操作数&&
为 true 时,其中T
声明所选运算符的类型为 true:
- 返回类型和所选运算符的每个参数的类型应为
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
将求值,并在之前计算x
的值上调用所选operator &
值,并调用为生成y
操作结果而计算的值。 - 该操作
x || y
的计算结果T.true(x) ? x : T.|(x, y)
为,其中T.true(x)
调用operator true
了声明的,T
并且T.|(x, y)
是所选operator |
的调用。 换句话说,x
首先对结果进行求值,operator true
以确定是否x
为 true。 然后,如果x
绝对为 true,则操作的结果是之前计算的值x
。 否则,y
将求值,并在之前计算x
的值上调用所选operator |
值,并调用为生成y
操作结果而计算的值。
在上述任一操作中,给定的 x
表达式只计算一次,而给定 y
的表达式不计算或计算一次。
12.15 Null 合并运算符
运算符 ??
称为 null 合并运算符。
null_coalescing_expression
: conditional_or_expression
| conditional_or_expression '??' null_coalescing_expression
| throw_expression
;
在窗体 a ?? b
的 null 合并表达式中,如果 a
为非null
,则结果为 a
;否则为结果 b
。 该操作仅当为null
时a
计算 b
。
null 合并运算符是右关联运算符,这意味着操作从右到左分组。
示例:窗体
a ?? b ?? c
的表达式的计算结果为 .?? (b ?? c)
一般情况下,窗体E1 ?? E2 ?? ... ?? EN
的表达式返回非null
操作数的第一个操作数,或者null
所有操作数均为null
。 end 示例
表达式 a ?? b
的类型取决于操作数上可用的隐式转换。 根据首选项顺序,类型为、或(其中类型a
A
为类型)、类型(提供 B
b
a
的类型)、类型(如果提供b
具有类型),并且A₀
是基础类型(如果A
为可为 null 的值类型),否则A
为基础类型。A
B
A
A₀
a ?? b
具体而言, a ?? b
按如下所示进行处理:
- 如果
A
存在且不是可为 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,则变成结果。 否则,b
将计算并转换为类型A
,这将成为结果。 - 否则,如果
A
存在且为可以为 null 的值类型,b
则存在类型B
,并且存在A₀
隐式转换,B
则结果类型为B
。 在运行时,a
首先评估。 如果没有a
,a
则解包为类型A₀
并转换为类型B
,null
这将成为结果。 否则,b
将计算并成为结果。 - 否则,如果
b
存在一个类型和B
隐式转换a
,B
则结果类型为B
。 在运行时,a
首先评估。 如果a
不是null
,a
则转换为类型B
,这将成为结果。 否则,b
将计算并成为结果。
否则, a
并且 b
不兼容,并且 a
会发生编译时错误。
12.16 引发表达式运算符
throw_expression
: 'throw' null_coalescing_expression
;
throw_expression会引发通过计算null_coalescing_expression生成的值。 表达式应可隐式转换为 System.Exception
表达式,在引发表达式之前将转换为 System.Exception
该表达式的结果。 引发表达式计算运行时的行为与为 throw 语句指定的行为相同(§13.10.6)。
throw_expression没有类型。 throw_expression可通过隐式引发转换转换为每种类型。
引发表达式应仅在以下语法上下文中发生:
- 作为三元条件运算符的第二个或第三个操作数(
?:
)。 - 作为 null 合并运算符的第二个操作数(
??
)。 - 作为 expression-bodied lambda 或成员的主体。
12.17 声明表达式
声明表达式声明局部变量。
declaration_expression
: local_variable_type identifier
;
local_variable_type
: type
| 'var'
;
如果简单名称查找未找到关联的声明(§12.8.4),则simple_name_
也被视为声明表达式。 用作声明表达式时, _
称为简单 放弃。 它在语义上等效 var _
于,但允许在更多地方使用。
声明表达式应仅在以下语法上下文中发生:
out
作为argument_list中的argument_value。- 作为由简单赋值左侧组成的简单放弃
_
(§12.21.2)。 - 作为一个或多个递归嵌套tuple_expression中的tuple_element,其中最外层由析构赋值左侧组成。 deconstruction_expression产生这种位置的声明表达式,即使声明表达式不是语法上存在的。
注意:这意味着无法对声明表达式进行括号化。 end note
对于在声明argument_list内引用declaration_expression声明的隐式类型变量,这是一个错误。
对于在 析构赋值中引用declaration_expression 声明的变量,这是一个错误。
一个声明表达式,它是一个简单的放弃或local_variable_type是标识符var
被分类为隐式类型变量。 表达式没有类型,根据语法上下文推断局部变量的类型,如下所示:
- 在argument_list变量的推断类型是相应参数的声明类型。
- 作为简单赋值左侧,变量的推断类型是赋值右侧的类型。
- 在简单赋值左侧的tuple_expression中,变量的推断类型是赋值右侧(解构后)相应元组元素的类型。
否则,声明表达式被归类为 显式类型变量 ,表达式的类型以及声明的变量应由 local_variable_type给出。
具有标识符 _
的声明表达式是一个放弃(§9.2.9.1),并且不为变量引入名称。 除将该名称引入最近的封闭局部变量声明空间(§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);
此示例演示如何在析构赋值中对变量和放弃使用隐式和显式类型化的声明表达式。 当找不到
_
声明时,simple_name_
等效var _
。void M1(out int i) { ... } void M2(string _) { M1(out _); // Error: `_` is a string M1(out var _); }
此示例演示如何在
var _
不可用时_
提供隐式类型放弃,因为它在封闭范围内指定变量。end 示例
12.18 条件运算符
运算符 ?:
称为条件运算符。 有时也称为三元运算符。
conditional_expression
: null_coalescing_expression
| null_coalescing_expression '?' expression ':' expression
| null_coalescing_expression '?' 'ref' variable_reference ':'
'ref' variable_reference
;
如果ref
存在,则条件运算符中不允许引发表达式 (§12.16)。
表单 b ? x : y
的条件表达式首先计算条件 b
。 然后,如果是b
true
,x
则计算并成为操作的结果。 否则, y
将计算并成为操作的结果。 条件表达式从不计算和 x
y
。
条件运算符是右关联运算符,这意味着操作从右到左分组。
示例:将窗体
a ? b : c ? d : e
的表达式计算为a ? b : (c ? d : e)
. end 示例
运算符的第一个操作数 ?:
应是可以隐式转换为 bool
的表达式,或者是实现 operator true
的类型表达式。 如果这两项要求均未满足,则会发生编译时错误。
如果 ref
存在:
- 标识转换应存在于两 个variable_reference的类型之间,结果的类型可以是任一类型。 如果任一类型为
dynamic
,则类型推理首选dynamic
(§8.7)。 如果任一类型是元组类型(§8.3.11),则当具有相同序号位置的元素名称在这两个元组中匹配时,类型推理将包括元素名称。 - 结果是一个变量引用,如果两 个variable_reference都是可写的,则此引用是可写的。
注意:存在时
ref
, conditional_expression 返回变量引用,该引用可以使用运算符或作为引用/输入/输出参数传递来分配给引用变量= ref
。 end note
如果ref
不存在,则第二个和第三个操作数,x
以及y
?:
运算符控制条件表达式的类型:
- 如果
x
具有类型X
且y
具有类型Y
,则为- 如果存在标识转换,
X
Y
则结果是一组表达式的最佳常见类型(§12.6.3.15)。 如果任一类型为dynamic
,则类型推理首选dynamic
(§8.7)。 如果任一类型是元组类型(§8.3.11),则当具有相同序号位置的元素名称在这两个元组中匹配时,类型推理将包括元素名称。 - 否则,如果隐式转换(§10.2)存在
X
,Y
但不存在于从X
Y
中,则Y
为条件表达式的类型。 - 否则,如果隐式枚举转换(§10.2.4)存在
X
,Y
则Y
为条件表达式的类型。 - 否则,如果隐式枚举转换(§10.2.4)存在
Y
,X
则X
为条件表达式的类型。 - 否则,如果隐式转换(§10.2)存在
Y
,X
但不存在于从Y
X
中,则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.19 匿名函数表达式
12.19.1 常规
匿名函数是表示“内联”方法定义的表达式。 匿名函数本身没有值或类型,但可转换为兼容的委托或表达式树类型。 匿名函数转换的计算取决于转换的目标类型:如果是委托类型,则转换计算结果为引用匿名函数定义的方法的委托值。 如果它是表达式树类型,则转换计算结果为表示方法结构作为对象结构的表达式树。
注意:出于历史原因,匿名函数有两种语法风格,即 lambda_expression和 anonymous_method_expression。 对于几乎所有目的,lambda_expression比anonymous_method_expression更简洁、更具表现力,后者仍以语言保持向后兼容。 end note
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和表达式替代项均适用,则应选择前者。
注意:此处的替代项的重叠和优先级只是为了描述性便利;可以详细说明语法规则以删除重叠。 ANTLR 和其他语法系统采用相同的便利性,因此 anonymous_function_body 自动具有指定的语义。 end note
注意:当被视为表达式时,如果结果类型
M
为void
(§12.8.13),则语法形式(如x?.M()
错误)。 但是,当被视为null_conditional_invocation_expression时,允许结果类型。void
end note
示例:结果类型
List<T>.Reverse
为void
. 在以下代码中,匿名表达式的正文是 null_conditional_invocation_expression,因此它不是错误。Action<List<int>> a = x => x?.Reverse();
end 示例
运算符 =>
的优先级与赋值 (=
) 相同,并且是右关联。
具有修饰符的async
匿名函数是异步函数,遵循 §15.15 中所述的规则。
可以显式或隐式类型化lambda_expression形式的匿名函数的参数。 在显式类型化参数列表中,显式声明每个参数的类型。 在隐式类型化参数列表中,参数的类型是从匿名函数发生的上下文推断的,具体而言,当匿名函数转换为兼容的委托类型或表达式树类型时,该类型提供参数类型(§10.7)。
在具有单个隐式类型参数的lambda_expression中,可以从参数列表中省略括号。 换句话说,表单的匿名函数
( «param» ) => «expr»
可以缩写为
«param» => «expr»
anonymous_method_expression形式的匿名函数的参数列表是可选的。 如果指定,则应显式键入参数。 否则,匿名函数可转换为具有不包含输出参数的任何参数列表的委托。
匿名函数的块体始终可访问(§13.2)。
示例:下面是匿名函数的一些示例:
x => x + 1 // Implicitly typed, expression body x => { return x + 1; } // Implicitly typed, block body (int x) => x + 1 // Explicitly typed, expression body (int x) => { return x + 1; } // Explicitly typed, block body (x, y) => x * y // Multiple parameters () => Console.WriteLine() // No parameters async (t1,t2) => await t1 + await t2 // Async delegate (int x) { return x + 1; } // Anonymous method expression delegate { return 1 + 1; } // Parameter list omitted
end 示例
除以下几点外,lambda_expressions 和 anonymous_method_expression的行为相同:
- anonymous_method_expression允许完全省略参数列表,从而将可转换性转换为任何值参数列表的委托类型。
- lambda_expression允许省略和推断参数类型,而anonymous_method_expression要求显式声明参数类型。
- lambda_expression的主体可以是表达式或块,而anonymous_method_expression的主体应为块。
- 只有 lambda_expression转换到兼容的表达式树类型(§8.6)。
12.19.2 匿名函数签名
匿名函数的anonymous_function_signature定义匿名函数的名称和可选参数的类型。 匿名函数的参数的范围是 anonymous_function_body (§7.7)。 匿名方法主体与参数列表(如果给定)一起构成声明空间(§7.3)。 因此,匿名函数的参数名称与局部变量、局部常量或参数的名称匹配,其范围包括 anonymous_method_expression 或 lambda_expression,这是编译时错误。
如果匿名函数具有 explicit_anonymous_function_signature,则兼容的委托类型和表达式树类型集仅限于具有相同参数类型和修饰符(§10.7)的用户。 与方法组转换(§10.8)相比,不支持匿名函数参数类型的逆差。 如果匿名函数没有 anonymous_function_signature,则兼容的委托类型和表达式树类型集仅限于没有输出参数的委托类型和表达式树类型。
请注意, anonymous_function_signature 不能包含属性或参数数组。 然而, anonymous_function_signature 可能与其参数列表包含参数数组的委托类型兼容。
另请注意,即使兼容,转换为表达式树类型仍可能在编译时失败(§8.6)。
12.19.3 匿名函数主体
匿名函数的正文(表达式 或 块)受以下规则的约束:
- 如果匿名函数包含签名,则签名中指定的参数在正文中可用。 如果匿名函数没有签名,则可以转换为具有参数的委托类型或表达式类型(§10.7),但不能在正文中访问这些参数。
- 除了签名中指定的按引用参数(如果有)最接近的封闭匿名函数外,正文访问按引用参数是编译时错误。
- 除了在最接近封闭匿名函数的签名(如果有)中指定的参数外,正文访问类型的参数
ref struct
是编译时错误。 - 当类型为结构类型
this
时,它是正文访问this
的编译时错误。 无论访问是显式(如 inthis.x
)还是隐式(如x
结构实例成员所在的位置x
)都是如此。 此规则只是禁止此类访问,并且不会影响成员查找是否导致结构的成员。 - 正文有权访问匿名函数的外部变量(§12.19.6)。 外部变量的访问将引用在计算lambda_expression或anonymous_method_expression时处于活动状态的变量实例(§12.19.7)。
- 对于正文,它是一个编译时错误,它包含一个
goto
语句、一个break
语句或一个continue
语句,其目标位于正文外部或包含匿名函数的正文内。 - 正文中的语句
return
从调用最近的封闭匿名函数(而不是封闭函数成员)返回控件。
通过计算和调用 lambda_expression 或 anonymous_method_expression之外,是否有任何方法可以执行匿名函数的块,这明确未指定。 具体而言,编译器可以选择通过合成一个或多个命名方法或类型来实现匿名函数。 任何此类合成元素的名称应为保留供编译器使用的形式(§6.4.3)。
12.19.4 重载分辨率
参数列表中的匿名函数参与类型推理和重载解析。 有关 确切规则,请参阅 §12.6.3 和 §12.6.4 。
示例:以下示例说明了匿名函数对重载解析的影响。
class ItemList<T> : List<T> { public int Sum(Func<T, int> selector) { int sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } public double Sum(Func<T, double> selector) { double sum = 0; foreach (T item in this) { sum += selector(item); } return sum; } }
该
ItemList<T>
类有两Sum
种方法。 每个参数都采用一个selector
参数,该参数提取要从列表项中求和的值。 提取的值可以是一个或一个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
种方法。end 示例
12.19.5 匿名函数和动态绑定
匿名函数不能是动态绑定操作的接收方、参数或操作数。
12.19.6 外部变量
12.19.6.1 常规
范围包括lambda_expression或anonymous_method_expression的任何局部变量、值参数或参数数组称为匿名函数的外部变量。 在类的实例函数成员中,此值被视为值参数,并且是函数成员中包含的任何匿名函数的外部变量。
12.19.6.2 捕获的外部变量
当匿名函数引用外部变量时,据说外部变量已被 匿名函数捕获 。 通常,局部变量的生存期仅限于执行与其关联的块或语句(§9.2.9)。 但是,至少将扩展捕获的外部变量的生存期,直到从匿名函数创建的委托或表达式树有资格进行垃圾回收。
示例:在示例中
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
end 示例
当匿名函数捕获局部变量或值参数时,局部变量或参数不再被视为固定变量(§23.4),而是被视为可移动变量。 但是,捕获的外部变量不能在语句(§23.7)中使用fixed
,因此无法获取捕获的外部变量的地址。
注意:与未捕获的变量不同,捕获的局部变量可以同时向多个执行线程公开。 end note
12.19.6.3 局部变量的实例化
在执行进入变量范围时,将被视为 实例化 局部变量。
例如:调用以下方法时,局部变量
x
将实例化并初始化三次,每次循环迭代一次。static void F() { for (int i = 0; i < 3; i++) { int x = i * 2 + 1; ... } }
但是,移动循环外部的
x
声明会导致单个实例化x
:static void F() { int x; for (int i = 0; i < 3; i++) { x = i * 2 + 1; ... } }
end 示例
如果未捕获,则无法准确观察实例化局部变量的频率,因为实例化生存期不相交,因此每个实例化都可能只使用相同的存储位置。 但是,当匿名函数捕获局部变量时,实例化的效果变得明显。
示例:示例
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)。
end 示例
如果 for-loop 声明迭代变量,该变量本身被视为在循环外部声明。
示例:因此,如果示例已更改以捕获迭代变量本身:
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
end 示例
匿名函数委托可以共享一些捕获的变量,但仍具有其他变量的单独实例。
示例:例如,如果
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
end 示例
单独的匿名函数可以捕获外部变量的同一实例。
示例:在示例中:
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
end 示例
12.19.7 匿名函数表达式的计算
匿名函数 F
应始终转换为委托类型 D
或表达式树类型 E
,直接或通过执行委托创建表达式 new D(F)
。 此转换确定匿名函数的结果,如 §10.7 中所述。
12.19.8 实现示例
此子引用是信息性的。
此子引用描述了匿名函数转换在其他 C# 构造方面的可能实现。 此处所述的实现基于商业 C# 编译器使用的相同原则,但绝不是授权实现,也不是唯一可能的实现。 它只简要提到对表达式树的转换,因为他们的确切语义超出了此规范的范围。
此子引用的其余部分提供了几个包含具有不同特征的匿名函数的代码示例。 对于每个示例,将提供仅使用其他 C# 构造的代码的相应转换。 在示例中, D
标识符由表示以下委托类型来假定:
public delegate void D();
匿名函数的最简单形式是捕获任何外部变量的函数:
delegate void D();
class Test
{
static void F()
{
D d = () => Console.WriteLine("test");
}
}
这可以转换为委托实例化,该实例化引用编译器生成的静态方法,在该方法中放置匿名函数的代码:
delegate void D();
class Test
{
static void F()
{
D d = new D(__Method1);
}
static void __Method1()
{
Console.WriteLine("test");
}
}
在以下示例中,匿名函数引用以下项的 this
实例成员:
delegate void D();
class Test
{
int x;
void F()
{
D d = () => Console.WriteLine(x);
}
}
这可以转换为编译器生成的实例方法,其中包含匿名函数的代码:
delegate void D();
class Test
{
int x;
void F()
{
D d = new D(__Method1);
}
void __Method1()
{
Console.WriteLine(x);
}
}
在此示例中,匿名函数捕获局部变量:
delegate void D();
class Test
{
void F()
{
int y = 123;
D d = () => Console.WriteLine(y);
}
}
局部变量的生存期现在必须至少扩展到匿名函数委托的生存期。 这可以通过将局部变量“提升”到编译器生成的类的字段中来实现。 然后实例化局部变量(§12.19.6.3)对应于创建编译器生成的类的实例,访问本地变量对应于访问编译器生成的类实例中的字段。 此外,匿名函数将成为编译器生成的类的实例方法:
delegate void D();
class Test
{
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1
{
public int y;
public void __Method1()
{
Console.WriteLine(y);
}
}
}
最后,以下匿名函数捕获 this
以及两个具有不同生存期的局部变量:
delegate void D();
class Test
{
int x;
void F()
{
int y = 123;
for (int i = 0; i < 10; i++)
{
int z = i * 2;
D d = () => Console.WriteLine(x + y + z);
}
}
}
在这里,会为每个捕获局部变量的块创建编译器生成的类,以便不同块中的局部变量可以具有独立的生存期。 编译器为内部块生成的类的实例 __Locals2
包含局部变量 z
和引用实例的 __Locals1
字段。 编译器为外部块生成的类的实例 __Locals1
包含局部变量 y
和引用 this
封闭函数成员的字段。 借助这些数据结构,可以通过实例 __Local2
访问所有捕获的外部变量,因此匿名函数的代码可以作为该类的实例方法实现。
delegate void D();
class Test
{
int x;
void F()
{
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++)
{
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1
{
public Test __this;
public int y;
}
class __Locals2
{
public __Locals1 __locals1;
public int z;
public void __Method1()
{
Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}
将匿名函数转换为表达式树时,也可以使用此处应用的相同方法来捕获局部变量:对编译器生成的对象的引用可以存储在表达式树中,对局部变量的访问可以表示为对这些对象的字段访问。 此方法的优点是,它允许在委托树和表达式树之间共享“提升”局部变量。
信息性文本的结尾。
12.20 查询表达式
12.20.1 常规
查询表达式 为类似于关系查询和分层查询语言(如 SQL 和 XQuery)的查询提供语言集成的语法。
query_expression
: from_clause query_body
;
from_clause
: 'from' type? identifier 'in' expression
;
query_body
: query_body_clauses? select_or_group_clause query_continuation?
;
query_body_clauses
: query_body_clause
| query_body_clauses query_body_clause
;
query_body_clause
: from_clause
| let_clause
| where_clause
| join_clause
| join_into_clause
| orderby_clause
;
let_clause
: 'let' identifier '=' expression
;
where_clause
: 'where' boolean_expression
;
join_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression
;
join_into_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression 'into' identifier
;
orderby_clause
: 'orderby' orderings
;
orderings
: ordering (',' ordering)*
;
ordering
: expression ordering_direction?
;
ordering_direction
: 'ascending'
| 'descending'
;
select_or_group_clause
: select_clause
| group_clause
;
select_clause
: 'select' expression
;
group_clause
: 'group' expression 'by' expression
;
query_continuation
: 'into' identifier query_body
;
查询表达式以子from
句开头,以或子句结尾select
group
。 初始from
子句后跟零个或多个from
、let
或 where
join
orderby
子句。 每个from
子句都是一个生成器,它引入了一个范围变量,该变量范围位于序列的元素上。 每个 let
子句都引入了一个范围变量,该变量表示由以前的范围变量计算的值。 每个 where
子句都是从结果中排除项的筛选器。 每个 join
子句将源序列的指定键与另一个序列的键进行比较,从而生成匹配对。 每个 orderby
子句根据指定的条件对项重新排序。最终 select
或 group
子句根据范围变量指定结果的形状。 最后,子句可以通过将一个 into
查询的结果视为后续查询中的生成器来“连接”查询。
12.20.2 查询表达式中的歧义性
查询表达式使用许多上下文关键字(§6.4.4):ascending
、、by
、、equals
descending
、join
let
on
group
into
from
和。 orderby
select
where
为了避免使用这些标识符作为关键字和简单名称而出现的歧义,这些标识符在查询表达式中的任何位置都被视为关键字,除非它们以“”@
(§6.4.4)为前缀,在这种情况下,这些标识符被视为标识符。 为此,查询表达式是以“identifier”开头的任何表达式,后跟除“;
from
”、“”=
或“”,
之外的任何标记。
12.20.3 查询表达式转换
12.20.3.1 常规
C# 语言未指定查询表达式的执行语义。 相反,查询表达式将转换为遵循查询表达式模式的方法的调用(§12.20.4)。 具体而言,查询表达式将转换为名为Where
、、Select
、Join
SelectMany
、GroupJoin
、OrderBy
、、OrderByDescending
、ThenBy
、 ThenByDescending
GroupBy
和的方法Cast
的调用。 这些方法应具有特定的签名和返回类型,如 §12.20.4 中所述。 这些方法可以是要查询的对象实例方法或对象外部的扩展方法。 这些方法实现查询的实际执行。
从查询表达式到方法调用的转换是在执行任何类型绑定或重载解析之前发生的语法映射。 查询表达式转换后,生成的方法调用将作为常规方法调用进行处理,这反过来可能会发现编译时错误。 这些错误条件包括但不限于不存在的方法、错误的类型的参数以及类型推理失败的泛型方法。
通过重复应用以下翻译来处理查询表达式,直到无法进一步减少。 翻译按应用程序顺序列出:每个部分假定前面各节中的翻译已详尽执行,一旦用尽,则以后不会重新访问同一查询表达式的处理中的一个部分。
查询表达式包含对范围变量的赋值,或使用范围变量作为引用或输出参数的参数的编译时间错误。
某些翻译使用 *表示的透明标识符注入范围变量。 这些内容在 §12.20.3.8 中进一步介绍。
12.20.3.2 包含延续的查询表达式
查询表达式,其查询正文后面的延续
from «x1» in «e1» «b1» into «x2» «b2»
转换为
from «x2» in ( from «x1» in «e1» «b1» ) «b2»
以下部分中的翻译假定查询没有延续。
示例:示例:
from c in customers group c by c.Country into g select new { Country = g.Key, CustCount = g.Count() }
转换为:
from g in (from c in customers group c by c.Country) select new { Country = g.Key, CustCount = g.Count() }
其最终翻译为:
customers. GroupBy(c => c.Country). Select(g => new { Country = g.Key, CustCount = g.Count() })
end 示例
12.20.3.3 显式范围变量类型
from
显式指定范围变量类型的子句
from «T» «x» in «e»
转换为
from «x» in ( «e» ) . Cast < «T» > ( )
join
显式指定范围变量类型的子句
join «T» «x» in «e» on «k1» equals «k2»
转换为
join «x» in ( «e» ) . Cast < «T» > ( ) on «k1» equals «k2»
以下部分中的翻译假定查询没有显式范围变量类型。
示例:示例
from Customer c in customers where c.City == "London" select c
转换为
from c in (customers).Cast<Customer>() where c.City == "London" select c
其最终翻译为
customers. Cast<Customer>(). Where(c => c.City == "London")
end 示例
注意:显式范围变量类型可用于查询实现非泛型
IEnumerable
接口的集合,但不适用于泛型IEnumerable<T>
接口。 在上面的示例中,如果客户的类型ArrayList
为,则情况就是这种情况。 end note
12.20.3.4 退化查询表达式
窗体的查询表达式
from «x» in «e» select «x»
转换为
( «e» ) . Select ( «x» => «x» )
示例:示例
from c in customers select c
转换为
(customers).Select(c => c)
end 示例
退化的查询表达式是一个简单选择源元素的表达式。
注意:翻译(§12.20.3.6 和 §12.20.3.7)的后续阶段通过将转换步骤替换为源来删除其他翻译步骤引入的退化查询。 但是,请务必确保查询表达式的结果绝不是源对象本身。 否则,返回此类查询的结果可能会无意中向调用方公开私有数据(例如元素数组)。 因此,此步骤通过显式调用
Select
源来保护直接在源代码中编写的退化查询。 然后由其他查询运算符的Select
实现者决定,以确保这些方法永远不会返回源对象本身。 end note
12.20.3.5 From, let, where, join 和 orderby 子句
第二 from
个子句后跟子句的 select
查询表达式
from «x1» in «e1»
from «x2» in «e2»
select «v»
转换为
( «e1» ) . SelectMany( «x1» => «e2» , ( «x1» , «x2» ) => «v» )
示例:示例
from c in customers from o in c.Orders select new { c.Name, o.OrderID, o.Total }
转换为
(customers). SelectMany(c => c.Orders, (c,o) => new { c.Name, o.OrderID, o.Total } )
end 示例
包含第二 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
,编译器生成的标识符不可见且不可访问。end 示例
表达式 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
,编译器生成的标识符不可见且不可访问。end 示例
表达式 where
及其前面的 from
子句:
from «x» in «e»
where «f»
...
转换为
from «x» in ( «e» ) . Where ( «x» => «f» )
...
紧跟子select
句的子join
句
from «x1» in «e1»
join «x2» in «e2» on «k1» equals «k2»
select «v»
转换为
( «e1» ) . Join( «e2» , «x1» => «k1» , «x2» => «k2» , ( «x1» , «x2» ) => «v» )
示例:示例
from c in customersh join o in orders on c.CustomerID equals o.CustomerID select new { c.Name, o.OrderDate, o.Total }
转换为
(customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })
end 示例
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
编译器生成的标识符不可见且不可访问。end 示例
子 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)
end 示例
以下翻译假定每个查询表达式中没有let
、where
join
或orderby
子句,并且不超过每个查询表达式中的一个初始from
子句。
12.20.3.6 Select 子句
窗体的查询表达式
from «x» in «e» select «v»
转换为
( «e» ) . Select ( «x» => «v» )
除非是 «v»
标识符 «x»
,翻译只是
( «e» )
示例:示例
from c in customers.Where(c => c.City == "London") select c
只需转换为
(customers).Where(c => c.City == "London")
end 示例
12.20.3.7 Group 子句
A group
子句
from «x» in «e» group «v» by «k»
转换为
( «e» ) . GroupBy ( «x» => «k» , «x» => «v» )
除标识符«x»
外«v»
,翻译为
( «e» ) . GroupBy ( «x» => «k» )
示例:示例
from c in customers group c.Name by c.Country
转换为
(customers).GroupBy(c => c.Country, c => c.Name)
end 示例
12.20.3.8 透明标识符
某些转换会注入范围变量,其中包含由该变量表示的*
透明标识符。 透明标识符仅作为查询表达式转换过程中的中间步骤存在。
当查询翻译注入透明标识符时,进一步的翻译步骤会将透明标识符传播到匿名函数和匿名对象初始值设定项。 在这些上下文中,透明标识符具有以下行为:
- 当透明标识符作为匿名函数中的参数发生时,关联的匿名类型的成员将自动在匿名函数的正文范围内。
- 当具有透明标识符的成员处于范围内时,该成员的成员也位于范围内。
- 当透明标识符作为匿名对象初始值设定项中的成员声明符发生时,它会引入具有透明标识符的成员。
在上述翻译步骤中,透明标识符始终与匿名类型一起引入,目的是将多个范围变量捕获为单个对象的成员。 允许使用与匿名类型不同的机制将 C# 的实现组合在一起多个范围变量。 以下翻译示例假定使用匿名类型,并显示一个可能的透明标识符转换。
示例:示例
from c in customers from o in c.Orders orderby o.Total descending select new { c.Name, o.Total }
转换为
from * in (customers).SelectMany(c => c.Orders, (c,o) => new { c, o }) orderby o.Total descending select new { c.Name, o.Total }
这进一步转换为
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(* => o.Total) .Select(\* => new { c.Name, o.Total })
当清除透明标识符时,该标识符等效于
customers .SelectMany(c => c.Orders, (c,o) => new { c, o }) .OrderByDescending(x => x.o.Total) .Select(x => new { x.c.Name, x.o.Total })
其中
x
,编译器生成的标识符不可见且不可访问。示例
from c in customers join o in orders on c.CustomerID equals o.CustomerID join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
转换为
from * in (customers).Join( orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) join d in details on o.OrderID equals d.OrderID join p in products on d.ProductID equals p.ProductID select new { c.Name, o.OrderDate, p.ProductName }
这进一步减少到
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, * => o.OrderID, d => d.OrderID, (*, d) => new { *, d }) .Join(products, * => d.ProductID, p => p.ProductID, (*, p) => new { c.Name, o.OrderDate, p.ProductName })
其最终翻译为
customers .Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c, o }) .Join(details, x => x.o.OrderID, d => d.OrderID, (x, d) => new { x, d }) .Join(products, y => y.d.ProductID, p => p.ProductID, (y, p) => new { y.x.c.Name, y.x.o.OrderDate, p.ProductName })
其中
x
,以及y
编译器生成的标识符,这些标识符不可见且不可访问。 end 示例
12.20.4 查询表达式模式
查询表达式模式建立了一种方法模式,这些方法类型可以实现以支持查询表达式。
如果泛型类型的公共成员方法和可公开访问的扩展方法可以替换为以下类定义,则泛型类型 C<T>
支持查询表达式模式。 成员和可访问的外泄方法称为泛型类型的 C<T>
“形状”。 泛型类型用于说明参数和返回类型之间的适当关系,但也可以实现非泛型类型的模式。
delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);
class C
{
public C<T> Cast<T>() { ... }
}
class C<T> : C
{
public C<T> Where(Func<T,bool> predicate) { ... }
public C<U> Select<U>(Func<T,U> selector) { ... }
public C<V> SelectMany<U,V>(Func<T,C<U>> selector,
Func<T,U,V> resultSelector) { ... }
public C<V> Join<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,U,V> resultSelector) { ... }
public C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K> outerKeySelector,
Func<U,K> innerKeySelector, Func<T,C<U>,V> resultSelector) { ... }
public O<T> OrderBy<K>(Func<T,K> keySelector) { ... }
public O<T> OrderByDescending<K>(Func<T,K> keySelector) { ... }
public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector) { ... }
public C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
Func<T,E> elementSelector) { ... }
}
class O<T> : C<T>
{
public O<T> ThenBy<K>(Func<T,K> keySelector) { ... }
public O<T> ThenByDescending<K>(Func<T,K> keySelector) { ... }
}
class G<K,T> : C<T>
{
public K Key { get; }
}
上述方法使用泛型委托类型Func<T1, R>
Func<T1, T2, R>
,但它们同样可以使用参数和返回类型中具有相同关系的委托或表达式树类型。
注意:建议的关系
C<T>
,O<T>
确保ThenBy
和ThenByDescending
方法仅在或的结果OrderBy
OrderByDescending
上可用。 end note
注意:建议的结果
GroupBy
形状是序列序列,其中每个内部序列都有一个附加Key
属性。 end note
注意:由于查询表达式通过语法映射转换为方法调用,因此类型在实现任何或全部查询表达式模式的方式方面具有相当大的灵活性。 例如,模式的方法可以作为实例方法或扩展方法实现,因为两者具有相同的调用语法,并且方法可以请求委托或表达式树,因为匿名函数可转换为两者。 仅实现某些查询表达式模式的类型仅支持映射到类型支持的方法的查询表达式转换。 end note
注意:命名空间
System.Linq
为实现接口的任何类型提供查询表达式模式的实现System.Collections.Generic.IEnumerable<T>
。 end note
12.21 赋值运算符
12.21.1 常规
除了一个赋值运算符,所有赋值运算符都会向变量、属性、事件或索引器元素赋值。 异常 = ref
将变量引用(§9.5)分配给引用变量(§9.7)。
assignment
: unary_expression assignment_operator expression
;
assignment_operator
: '=' 'ref'? | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<='
| right_shift_assignment
;
赋值左操作数应是分类为变量的表达式,或者属性访问、索引器访问、事件访问或元组除外 = ref
。 声明表达式不能作为左操作数直接允许,但可能会作为析构赋值的计算步骤发生。
运算符=
称为简单赋值运算符。 它将右侧操作数的值或值分配给左操作数给定的变量、属性、索引器元素或元组元素。 简单赋值运算符的左操作数不应是事件访问(如 §15.8.2 中所述)。 简单赋值运算符在 §12.21.2 中介绍。
= ref
运算符称为 ref 赋值运算符。 它使右操作数(应为 variable_reference (§9.5),该操作数是左操作数指定的引用变量的引用。 ref 赋值运算符在 §12.21.3 中介绍。
除赋值运算符和运算符以外的=
赋值运算符称为复合赋值运算符。= ref
这些运算符对两个操作数执行指示的操作,然后将结果值分配给左操作数给定的变量、属性或索引器元素。 复合赋值运算符在 §12.21.4 中介绍。
+=
具有事件访问表达式作为左操作数的事件访问表达式的运算符-=
称为事件赋值运算符。 没有其他赋值运算符作为左操作数的事件访问有效。 事件赋值运算符在 §12.21.5 中介绍。
赋值运算符是右关联运算符,这意味着操作从右到左进行分组。
示例:将窗体
a = b = c
的表达式计算为a = (b = c)
. end 示例
12.21.2 简单分配
运算符 =
称为简单赋值运算符。
如果简单赋值左侧操作数为窗体E.P
或E[Ei]
具有编译时类型dynamic
的位置E
,则赋值将动态绑定(§12.3.3.3)。 在这种情况下,赋值表达式的编译时类型是 dynamic
,下面所述的解决方法将基于运行时类型的 E
运行时进行。 如果左侧操作数是至少有一个元素Ei
具有编译时类型的dynamic
窗体E[Ei]
,并且编译时类型E
不是数组,则生成的索引器访问是动态绑定的,但具有有限的编译时检查(§12.6.5)。
左操作数分类为元组的简单赋值也称为 析构赋值。 如果左侧操作数的任何元组元素具有元素名称,则会发生编译时错误。 如果左侧操作数的任何元组元素是 declaration_expression ,并且任何其他元素不是 declaration_expression 或简单放弃,则会发生编译时错误。
简单赋值x = y
的类型是赋值x
y
的类型,以递归方式确定,如下所示:
- 如果是
x
元组表达式(x1, ..., xn)
,并且可以y
解构为具有元素(§12.7)的n
元组表达式(y1, ..., yn)
,并且每个赋值都具有该类型Ti
,则赋xi
yi
值具有类型(T1, ..., Tn)
。 - 否则,如果
x
分类为变量,则变量不是readonly
,x
具有类型T
,并且y
具有隐式转换,T
则赋值具有类型T
。 - 否则,如果
x
分类为隐式类型化变量(即隐式类型声明表达式),并且y
具有类型T
,则变量的推断类型为T
,赋值具有类型T
。 - 否则,如果
x
分类为属性或索引器访问,则属性或索引器具有可访问的集访问器、x
具有类型T
,并且y
具有隐式转换,T
则赋值具有类型T
。 - 否则,分配无效且发生绑定时错误。
对类型T
为窗体x = y
的简单分配的运行时处理将作为类型T
赋x
y
值执行,其中包括以下递归步骤:
x
如果尚未评估,则评估它。- 如果
x
分类为变量,y
则进行求值,如果需要,则通过隐式转换(T
§10.2)转换为该变量。- 如果给定
x
的变量是reference_type的数组元素,则执行运行时检查,以确保计算得出y
的值与其中一x
个元素的数组实例兼容。 如果y
为null
,或者隐式引用转换(§10.2.8)从所y
引用的实例的类型到包含x
数组实例的实际元素类型,则检查成功。 否则,将会引发System.ArrayTypeMismatchException
。 - 求值和转换
y
产生的值存储在计算结果给出x
的位置,并生成为赋值的结果。
- 如果给定
- 如果
x
分类为属性或索引器访问:y
是计算的,如果需要,则通过隐式转换(§10.2)转换为T
。- 使用计算和转换
y
作为其值参数生成的值调用 的 set 访问器x
。 - 计算和转换
y
产生的值作为赋值的结果生成。
- 如果
x
分类为具有 arityn
的元组(x1, ..., xn)
:y
与元素解构n
为元组表达式e
。- 通过转换为
e
T
隐式元组转换来创建结果t
元组。 - 对于每个
xi
从左到右的顺序,将执行赋值xi
t.Itemi
,但不会再次评估。xi
t
作为工作分配的结果生成。
注意:如果编译时间类型
x
为,dynamic
并且有从编译时间类型y
到dynamic
隐式转换,则不需要运行时解析。 end note
注意:数组共同方差规则(§17.6)允许数组类型的值作为对数组类型的
A[]
B[]
实例的引用,前提是存在从 <a0/> 到 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[]
中。end note
在struct_type中声明的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式应归类为变量。 如果实例表达式被分类为值,则会发生绑定时错误。
注意:由于 §12.8.7,同一规则也适用于字段。 end note
示例:给定声明:
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
变量和r
变量。r.B
r.A
p.Y
但是,在示例中Rectangle r = new Rectangle(); r.A.X = 10; r.A.Y = 10; r.B.X = 100; r.B.Y = 100;
赋值都是无效的,因为
r.A
不是r.B
变量。end 示例
12.21.3 Ref 赋值
运算符 = ref
称为 ref 赋值 运算符。
左操作数应是绑定到引用变量(§9.7)、引用参数(而非 this
)、输出参数或输入参数的表达式。 右操作数应是生成一个variable_reference指定与左操作数相同的类型的值的表达式。
如果左操作数的 ref-safe-context (§9.7.2) 大于右操作数的 ref-safe-context,则这是编译时错误。
在 ref 赋值点,应明确分配正确的操作数。
当左操作数绑定到输出参数时,如果在 ref 赋值运算符的开头未明确分配该输出参数,则会出现错误。
如果左操作数是可写的 ref(即,它指定本地或输入参数以外的 ref readonly
任何参数),则右操作数应为可 写variable_reference。 如果右操作数变量可写,则左侧操作数可以是可写或只读 ref。
该操作使左操作数成为右操作数变量的别名。 即使右操作数变量可写,别名也可能是只读的。
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 }
end 示例
注意:使用
= 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
不是该语句的一部分,即使它可能一目了然。 end note
12.21.4 复合赋值
如果复合赋值左操作数为窗体E.P
或E[Ei]
具有编译时类型dynamic
的位置E
,则赋值是动态绑定的(§12.3.3)。 在这种情况下,赋值表达式的编译时类型是 dynamic
,下面所述的解决方法将基于运行时类型的 E
运行时进行。 如果左侧操作数是至少有一个元素Ei
具有编译时类型的dynamic
窗体E[Ei]
,并且编译时类型E
不是数组,则生成的索引器访问是动态绑定的,但具有有限的编译时检查(§12.6.5)。
表单 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
调用一次。 end 示例
当复合赋值左侧操作数是属性访问或索引器访问时,属性或索引器应同时具有 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
每个错误的直观原因是相应的简单赋值也是一个错误。
end 示例
注意:这也意味着复合赋值操作支持提升运算符。 由于复合赋值
x «op»= y
被计算为或x = x «op» y
x = (T)(x «op» y)
计算规则隐式覆盖提升运算符。 end note
12.21.5 事件分配
如果将运算符的 a += or -=
左操作数分类为事件访问,则表达式的计算方式如下:
- 计算事件访问的实例表达式(如果有)。
- 计算或运算符的
+=
右操作数,如果需要,通过隐式转换(§10.2)转换为左操作数-=
的类型。 - 调用事件的事件访问器,参数列表由上一步中计算的值组成。 如果运算符为
+=
,则调用 add 访问器;如果该运算符是-=
,则调用 remove 访问器。
事件赋值表达式不生成值。 因此,事件赋值表达式仅在statement_expression(§13.7)的上下文中有效。
12.22 表达式
表达式是non_assignment_expression或赋值。
expression
: non_assignment_expression
| assignment
;
non_assignment_expression
: declaration_expression
| conditional_expression
| lambda_expression
| query_expression
;
12.23 常量表达式
常量表达式是应在编译时完全计算的表达式。
constant_expression
: expression
;
常量表达式应具有值 null
或下列类型之一:
sbyte
、、byte
、ushort
short
、int
、、long
uint
、ulong
bool
float
char
double
decimal
;string
- 枚举类型;或
- 引用类型的默认值表达式 (§12.8.21)。
常量表达式中只允许以下构造:
- 文本(包括
null
文本)。 - 对
const
类和结构类型的成员的引用。 - 对枚举类型成员的引用。
- 对本地常量的引用。
- 圆括号子表达式,它们本身是常量表达式。
- 强制转换表达式。
checked
和unchecked
表达式。nameof
表达 式。- 预定义
+
的 、-
(!
逻辑求反)和~
一元运算符。 - 预定义的、、、、
/
>>
^
*
|
%
&&
&
==
!=
||
<<
、<
、>
、<=
和>=
二元运算符。-
+
- 条件
?:
运算符。 !
null 表示运算符 (§12.8.9)。sizeof
如果非托管类型是 §23.6.9 中指定的类型之一,该sizeof
类型返回常量值。- 如果类型是上面列出的类型之一,则默认值表达式。
常量表达式中允许以下转换:
- 标识转换
- 数值转换
- 枚举转换
- 常量表达式转换
- 隐式和显式引用转换,前提是转换的源是计算结果为
null
值的常量表达式。
注意:常量表达式中不允许其他转换,包括装箱、取消装箱和非值的隐式引用转换
null
。 end note
示例:在以下代码中
class C { const object i = 5; // error: boxing conversion not permitted const object str = "hello"; // error: implicit reference conversion }
初始化
i
是一个错误,因为需要装箱转换。 初始化str
是一个错误,因为需要从非null
值进行隐式引用转换。end 示例
每当表达式满足上面列出的要求时,都会在编译时计算表达式。 即使表达式是包含非常量构造的较大表达式的子表达式,也是如此。
常量表达式的编译时计算使用与非常量表达式的运行时计算相同的规则,除非运行时计算会引发异常,编译时计算会导致编译时错误发生。
除非在上下文中 unchecked
显式放置常量表达式,否则在表达式编译时计算期间在整型算术运算和转换中发生的溢出始终会导致编译时错误(§12.8.20)。
常量表达式在下面列出的上下文中是必需的,这在语法中使用 constant_expression表示。 在这些上下文中,如果在编译时无法完全计算表达式,则会发生编译时错误。
- 常量声明 (§15.4)
- 枚举成员声明 (§19.4)
- 参数列表的默认参数(§15.6.2)
case
语句的switch
标签(§13.8.3)。goto case
语句 (§13.10.4)- 包含初始值设定项的数组创建表达式(§12.8.17.5)中的维度长度。
- 属性 (§22)
- 在constant_pattern(§11.2.3)
隐式常量表达式转换(§10.2.11)允许将类型的int
常量表达式转换为 sbyte
、byte
、、short
ushort
、 uint
或ulong
常量表达式,前提是常量表达式的值在目标类型的范围内。
12.24 布尔表达式
boolean_expression是一个表达式,它生成类型bool
的结果;直接或通过在特定上下文中应用operator true
的结果,如下所述:
boolean_expression
: expression
;
if_statement(§13.8.2)、while_statement(§13.9.2)、do_statement(§13.9.3)或for_statement(§13.9.4)的控制条件表达式是boolean_expression。 运算符(§12.18)的控制?:
条件表达式遵循与boolean_expression相同的规则,但由于运算符优先级的原因被归类为null_coalescing_expression。
需要boolean_expressionE
才能生成类型bool
值,如下所示:
- 如果 E 可隐式转换为
bool
在运行时应用隐式转换。 - 否则,一元运算符重载解析(§12.4.4)用于查找启用
E
的唯一最佳实现,并在运行时应用该实现operator true
。 - 如果未找到此类运算符,则会发生绑定时错误。