C# Language Specification 1.2 之七 表达式
1. 表达式
表达式是一个运算符和操作数的序列。本章定义语法、操作数和运算符的计算顺序以及表达式的含义。
1.1 表达式的分类
一个表达式可归类为下列类别之一:
· 值。每个值都有关联的类型。
· 变量。每个变量都有关联的类型,称为该变量的已声明类型。
· 命名空间。这类表达式只能出现在 member-access(第 7.5.4 节)的左边。在任何其他上下文中,归类为命名空间的表达式将导致编译时错误。
· 类型。这类表达式只能出现在 member-access(第 7.5.4 节)的左边,或作为 as 运算符(第 7.9.10 节)、is 运算符(第 7.9.9 节)或 typeof 运算符(第 7.5.11 节)的操作数。在任何其他上下文中,归类为类型的表达式将导致编译时错误。
· 方法组。它是一组重载方法,是成员查找(第 7.3 节)的结果。方法组可以有关联的实例表达式。当调用实例方法时,实例表达式的计算结果成为由 this(第 7.5.7 节)表示的实例。只能在 invocation-expression(第 7.5.5 节)或 delegate-creation-expression(第 7.5.10.3 节)中使用方法组。在任何其他上下文中,归类为方法组的表达式将导致编译时错误。
· 属性访问。每个属性访问都有关联的类型,即该属性的类型。此外,属性访问可以有关联的实例表达式。当调用实例属性访问的访问器(get 或 set 块)时,实例表达式的计算结果成为由 this(第 7.5.7 节)表示的实例。
· 事件访问。每个事件访问都有关联的类型,即该事件的类型。此外,事件访问还可以有关联的实例表达式。事件访问可作为 += 和 -= 运算符(第7.13.3)的左操作数出现。在任何其他上下文中,归类为事件访问的表达式将导致编译时错误。
· 索引器访问。每个索引器访问都有关联的类型,即该索引器的元素类型。此外,索引器访问还可以有关联的实例表达式和关联的参数列表。当调用索引器访问的访问器(get 或 set 块)时,实例表达式的计算结果成为由 this(第 7.5.7 节)表示的实例,而实参列表的计算结果成为调用的形参列表。
· Nothing。这出现在当表达式是调用一个具有 void 返回类型的方法时。Nothing 类别的表达式仅在 statement-expression(第 8.6 节)的上下文中有效。
表达式的最终结果绝不会是一个命名空间、类型、方法组或事件访问。恰如以上所述,这些类别的表达式是只能在特定上下文中使用的中间构造。
通过执行 get-accessor 或 set-accessor 的调用,属性访问或索引器访问总是被重新归类为值。特定访问器是由属性或索引器访问的上下文确定的:如果访问是赋值的目标,则调用 set-accessor 以赋新值(第 7.13.1 节)。否则调用 get-accessor 访问器以获取当前值(第 7.1.1 节)。
1.1.1 表达式的值
大多数含有表达式的构造最后都要求表达式表示一个值 (value)。在此情况下,如果实际的表达式表示命名空间、类型、方法组或 Nothing,则将发生编译时错误。但是,如果表达式表示属性访问、索引器访问或变量,则将它们隐式替换为相应的属性、索引器或变量的值:
· 变量的值只是当前存储在该变量所标识的存储位置的值。在可以获取变量的值之前,变量必须被视为已明确赋值(第 5.3 节),否则将发生编译时错误。
· 一个属性访问表达式的值通过调用属性的 get-accessor 来获取。如果属性没有 get-accessor,则发生编译时错误。否则将执行一个函数成员调用(第 7.4.3 节),调用的结果成为属性访问表达式的值。
· 一个索引器访问表达式的值通过调用索引器的 get-accessor 来获取。如果索引器没有 get-accessor,则发生编译时错误。否则将使用与索引器访问表达式关联的参数列表来执行函数成员调用(第 7.4.3 节),调用的结果成为索引器访问表达式的值。
1.2 运算符
表达式由操作数 (operand) 和运算符 (operator) 构成。表达式的运算符指示对操作数进行什么样的运算。运算符的示例包括 +、-、*、/ 和 new。操作数的示例包括文本 (literal)、字段、局部变量和表达式。
有三类运算符:
· 一元运算符。一元运算符带一个操作数并使用前缀表示法(如 –x)或后缀表示法(如 x++)。
· 二元运算符。二元运算符带两个操作数并且全都使用中缀表示法(如 x + y)。
· 三元运算符。只有一个三元运算符 ?: 存在,它带三个操作数并使用中缀表示法 (c? x: y)。
表达式中运算符的计算顺序由运算符的优先级 (precedence) 和关联性 (associativity)(第 7.2.1 节)确定。
表达式中的操作数从左到右进行计算。例如,在 F(i) + G(i++) * H(i) 中,F 方法是使用 i 的旧值调用的,然后 G 方法也是使用 i 的旧值进行调用,最后 H 方法使用 i 的新值调用。这与运算符的优先级无关。
某些运算符可以重载 (overloaded)。运算符重载允许指定使用用户定义的运算符来执行某些运算,这些运算的操作数中至少有一个,甚至两个都属于用户定义的类或结构类型(第 7.2.2 节)。
1.2.1 运算符的优先级和顺序关联性
当表达式包含多个运算符时,运算符的优先级 (precedence) 控制各运算符的计算顺序。例如,表达式 x + y * z 按 x + (y * z) 计算,因为 * 运算符的优先级比 + 运算符高。运算符的优先级由运算符的关联语法产生式的定义确定。例如,一个 additive-expression 由以 + 或 - 运算符分隔的 multiplicative-expression 组成,因此给 + 和 - 运算符赋予的优先级比 *、/ 和 % 运算符低。
下表按照从最高到最低的优先级顺序概括了所有的运算符:
章节 |
类别 |
运算符 |
7.5 |
基本 |
x.y f(x) a[x] x++ x-- new typeof checked unchecked |
7.6 |
一元 |
+ - ! ~ ++x --x (T)x |
7.7 |
乘除 |
* / % |
7.7 |
加减 |
+ - |
7.8 |
移位 |
<< >> |
7.9 |
关系和类型检测 |
< > <= >= is as |
7.9 |
相等 |
== != |
7.10 |
逻辑 AND |
& |
7.10 |
逻辑 XOR |
^ |
7.10 |
逻辑 OR |
| |
7.11 |
条件 AND |
&& |
7.11 |
条件 OR |
|| |
7.12 |
条件 |
?: |
7.13 |
赋值 |
= *= /= %= += -= <<= >>= &= ^= |= |
当操作数出现在具有相同优先级的两个运算符之间时,运算符的顺序关联性控制运算的执行顺序:
· 除了赋值运算符外,所有的二元运算符都向左顺序关联 (left-associative),意思是从左向右执行运算。例如,x + y + z 按 (x + y) + z 计算。
· 赋值运算符和条件运算符 (?:) 向右顺序关联 (right-associative),意思是从右向左执行运算。例如,x = y = z 按 x = (y = z) 计算。
优先级和顺序关联性都可以用括号控制。例如,x + y * z 先将 y 乘以 z,然后将结果与 x 相加,而 (x + y) * z 先将 x 与 y 相加,然后再将结果乘以 z。
1.2.2 运算符重载
所有一元和二元运算符都具有可自动用于任何表达式的预定义实现。除了预定义实现外,还可通过在类或结构(第 10.9 节)中设置 operator 声明来引入用户定义的实现。用户定义的运算符实现的优先级总是高于预定义运算符实现的优先级:仅当没有适用的用户定义运算符实现存在时,才会考虑预定义的运算符实现,如第 7.2.3 节和第 7.2.4 节中所述。
可重载的一元运算符 (overloadable unary operator) 有:
+ - ! ~ ++ -- true false
虽然不在表达式中显式使用 true 和 false(并且因此而未包括在第 7.2.1 节的优先级表中),但仍将它们视为运算符,因为它们在多种表达式上下文中被调用:布尔表达式(第 7.16 节)和涉及条件运算符(第 7.12 节)以及条件逻辑运算符(第 7.11 节)的表达式。
可重载的二元运算符 (overloadable binary operator) 有:
+ - * / % & | ^ << >> == != > < >= <=
只有以上所列的运算符可以重载。具体而言,不可能重载成员访问、方法调用或 =、&&、||、?:、checked、unchecked、new、typeof、as 和 is 运算符。
当重载一个二元运算符时,也会隐式重载相应的赋值运算符(若有)。例如,运算符 * 的重载也是运算符 *= 的重载。第 7.13 节对此有进一步描述。请注意,赋值运算符本身 (=) 不能重载。赋值总是简单地将值按位复制到变量中。
强制转换运算(如 (T)x)通过提供用户定义的转换(第 6.4 节)来重载。
元素访问(如 a[x])不被视为可重载的运算符。但是,可通过索引器(第 10.8 节)支持用户定义的索引。
在表达式中,使用运算符表示法来引用运算符,而在声明中,使用函数表示法来引用运算符。下表显示了一元运算符和二元运算符的运算符表示法和函数表示法之间的关系。在第一项中,op 表示任何可重载的一元前缀运算符。在第二项中,op 表示 ++ 和 -- 一元后缀运算符。在第三项中,op 表示任何可重载的二元运算符。
运算符表示法 |
函数表示法 |
op x |
operator op(x) |
x op |
operator op(x) |
x op y |
operator op(x, y) |
用户定义的运算符声明总是要求至少一个参数为包含运算符声明的类或结构类型。因此,用户定义的运算符不可能具有与预定义运算符相同的签名。
用户定义的运算符声明不能修改运算符的语法、优先级或顺序关联性。例如,/ 运算符总是一个二元运算符,总是具有在第 7.2.1 节中指定的优先级,并且总是向左顺序关联。
虽然用户定义的运算符可以执行它想执行的任何计算,但是强烈建议不要采用产生的结果与直觉预期不同的实现。例如,operator == 的实现应比较两个操作数是否相等,然后返回一个适当的 bool 结果。
在从第 7.5 节到第 7.13 节的关于各运算符的说明中,运算符的预定义实现以及应用于各运算符的任何其他规则都有规定。在这些说明中使用了“一元运算符重载决策”(unary operator overload resolution)、“二元运算符重载决策”(binary operator overload resolution) 和“数值提升”(numeric promotion) 这样的术语,在后面的章节中可以找到它们的定义。
1.2.3 一元运算符重载决策
op x 或 x op 形式的运算(其中 op 是可重载的一元运算符,x 是 X 类型的表达式)按下面这样处理:
· 对于由 X 为运算 operator op(x) 提供的候选的用户定义运算符的集合,应根据第 7.2.5 节中的规则来确定。
· 如果候选的用户定义运算符集合不为空,则它就会成为运算的候选运算符集合。否则,预定义一元 operator op 实现成为关于该运算的候选运算符集合。关于给定运算符的预定义实现,在有关运算符的说明(第 7.5 节和第 7.6 节)中指定。
· 第 7.4.2 节中的重载决策规则应用于候选运算符集合,以选择一个关于参数列表 (x) 的最好的运算符,此运算符将成为重载决策过程的结果。如果重载决策未能选出单个最佳运算符,则发生编译时错误。
1.2.4 二元运算符重载决策
x op y 形式的运算(其中 op 是可重载的二元运算符,x 是 X 类型的表达式,y 是 Y 类型的表达式)按下面这样处理:
· 确定 X 和 Y 为运算 operator op(x, y) 提供的候选用户定义运算符集合。该集合包括由 X 提供的候选运算符和由 Y 提供的候选运算符的联合,每个候选运算符使用第 7.2.5 节中的规则确定。如果 X 和 Y 为同一类型,或者 X 和 Y 派生自一个公共基类型,则两者共有的候选运算符只在该并集中出现一次。
· 如果候选的用户定义运算符集合不为空,则它就会成为运算的候选运算符集合。否则,预定义二元 operator op 实现将成为该运算的候选运算符集合。关于给定运算符的预定义实现,在有关运算符的说明(第 7.7 节到第 7.13 节)中指定。
· 第 7.4.2 节中的重载决策规则应用于候选运算符集合,以选择一个关于参数列表 (x, y) 的最好的运算符,此运算符将成为重载决策过程的结果。如果重载决策未能选出单个最佳运算符,则发生编译时错误。
1.2.5 候选用户定义运算符
给定一个 T 类型和运算 operator op(A),其中 op 是可重载的运算符,A 是参数列表,对 T 为 operator op(A) 提供的候选用户定义运算符集合按下面这样确定:
· 对于 T 中的所有 operator op 声明,如果关于参数列表 A 至少有一个运算符是适用的(第 7.4.2.1 节),则候选运算符集合由 T 中所有适用的 operator op 声明组成。
· 否则,如果 T 为 object,则候选运算符集合为空。
· 否则,由 T 提供的候选运算符集合是 T 的直接基类提供的候选运算符集合。
1.2.6 数值提升
数值提升包括自动为预定义一元和二元数值运算符的操作数执行某些隐式转换。数值提升不是一个独特的机制,而是一种将重载决策应用于预定义运算符所产生的效果。数值提升尤其不影响用户定义运算符的计算,尽管可以实现用户定义运算符以表现类似的效果。
作为数值提升的示例,请看二元运算符 * 的预定义实现:
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);
当重载决策规则(第 7.4.2 节)应用于此运算符集合时,这些运算符中第一个能满足下述条件的运算符会被选中:存在一个从给定的操作数的类型到它声明的类型的隐式转换。例如,对于运算 b * s(其中 b 为 byte,s 为 short),重载决策选择 operator *(int, int) 作为最佳运算符。因此,效果是 b 和 s 转换为 int,并且结果的类型为 int。同样,对于 i * d 运算(其中 i 为 int,d 为 double),重载决策选择 operator *(double, double) 作为最佳运算符。
1.2.6.1 一元数值提升
一元数值提升是针对预定义的 +、– 和 ~ 一元运算符的操作数发生的。一元数值提升仅包括将 sbyte、byte、short、ushort 或 char 类型的操作数转换为 int 类型。此外,对于 – 一元运算符,一元数值提升将 uint 类型的操作数转换为 long 类型。
1.2.6.2 二元数值提升
二元数值提升是针对预定义的 +、–、*、/、%、&、|、^、==、!=、>、<、>= 和 <= 二元运算符的操作数发生的。二元数值提升隐式地将两个操作数都转换为一个公共类型,如果涉及的是非关系运算符,则此公共类型还成为运算的结果类型。二元数值提升应按下列规则进行(以它们在此出现的顺序):
· 如果有一个操作数的类型为 decimal,则另一个操作数转换为 decimal 类型;否则,如果另一个操作数的类型为 float 或 double,则发生编译时错误。
· 否则,如果有一个操作数的类型为 double,则另一个操作数转换为 double 类型。
· 否则,如果有一个操作数的类型为 float,则另一个操作数转换为 float 类型。
· 否则,如果有一个操作数的类型为 ulong,则另一个操作数转换为 ulong 类型;否则,如果另一个操作数的类型为 sbyte、short、int 或 long,则发生编译时错误。
· 否则,如果有一个操作数的类型为 long,则另一个操作数转换为 long 类型。
· 否则,如果有一个操作数的类型为 uint 而另一个操作数的类型为 sbyte、short 或 int,则两个操作数都转换为 long 类型。
· 否则,如果有一个操作数的类型为 uint,则另一个操作数转换为 uint 类型。
· 否则,两个操作数都转换为 int 类型。
请注意,第一个规则不允许将 decimal 类型与 double 和 float 类型混用。该规则遵循这样的事实:在 decimal 类型与 double 和 float 类型之间不存在隐式转换。
还需要注意的是,当一个操作数为有符号的整型时,另一个操作数的类型不可能为 ulong 类型。原因是不存在一个既可以表示 ulong 的全部范围,又能表示有符号整数的整型类型。
在以上两种情况下,都可以使用强制转换表达式显式地将一个操作数转换为与另一个操作数兼容的类型。
在下面的示例中
decimal AddPercent(decimal x, double percent) {
return x * (1.0 + percent / 100.0);
}
由于 decimal 类型不能与 double 类型相乘,因此发生编译时错误。通过将第二个操作数显式转换为 decimal 消除此错误,如下所示:
decimal AddPercent(decimal x, double percent) {
return x * (decimal)(1.0 + percent / 100.0);
}
1.3 成员查找
成员查找是确定类型上下文中的名称含义的过程。成员查找可能成为在表达式中计算 simple-name(第 7.5.2 节)或 member-access(第 7.5.4 节)过程的组成部分。
T 类型中的名称 N 的成员查找按下面这样处理:
· 首先,构造一个在 T 和 T 的基类型(第 3.5 节)中声明的名为 N 的所有可访问(第 7.3.1 节)成员的集合。包含 override 修饰符的声明不包括在此集合中。如果名为 N 且可访问的成员不存在,则此查找不产生任何匹配,并且不对下面的步骤进行计算。
· 然后,从该集合中移除被其他成员隐藏的成员。对于该集合中的每个成员 S.M(其中 S 是声明了成员 M 的类型),应用下面的规则:
o 如果 M 是一个常量、字段、属性、事件、类型或枚举成员,则从集合中移除在 S 的基类型中声明的所有成员。
o 如果 M 是一个方法,则从集合中移除在 S 的基类型中声明的所有非方法成员,并从集合中移除与在 S 的基类型中声明的 M 具有相同签名的所有方法。
· 最后,移除了隐藏成员后,按下述规则确定查找的结果:
o 如果集合由一个非方法成员组成,则此成员为查找的结果。
o 否则,如果集合只包含方法,则这组方法为查找的结果。
o 否则,查找失败(无明确的结果),并发生编译时错误(只有对于具有多个直接基接口的接口中的成员查找才会出现这种情况)。
在不是接口的类型和严格单一继承的接口(继承链中的每个接口都只有零个或一个直接基接口)类型中,进行成员查找的规则可以简单地归结为:派生成员隐藏具有相同名称或签名的基成员。这种单一继承查找决不会失败(一定有明确的结果)。有关多重继承接口中的成员查找可能引起的多义性的介绍详见第 13.2.5 节。
1.3.1 基类型
出于成员查找的目的,类型 T 被视为具有下列基类型:
· 如果 T 为 object,则 T 没有基类型。
· 如果 T 为 enum-type,则 T 的基类型为类类型 System.Enum、System.ValueType 和 object。
· 如果 T 为 struct-type,则 T 的基类型为类类型 System.ValueType 和 object。
· 如果 T 为 class-type,则 T 的基类型为 T 的基类,其中包括类类型 object。
· 如果 T 为 interface-type,则 T 的基类型为 T 的基接口和类类型 object。
· 如果 T 为 array-type,则 T 的基类型为类类型 System.Array 和 object。
· 如果 T 为 delegate-type,则 T 的基类型为类类型 System.Delegate 和 object。
1.4 函数成员
函数成员是包含可执行语句的成员。函数成员总是类型的成员,不能是命名空间的成员。C# 定义了以下类别的函数成员:
· 方法
· 属性
· 事件
· 索引器
· 用户定义运算符
· 实例构造函数
· 静态构造函数
· 析构函数
除了析构函数和静态构造函数(它们不能被显式调用),函数成员中包含的语句通过函数成员调用执行。编写函数成员调用的实际语法取决于具体的函数成员类别。
函数成员调用中所带的实参列表(第 7.4.1 节)提供了函数成员形参的实际值或变量引用。
调用方法、索引器、运算符和实例构造函数时,使用重载决策来确定要调用的候选函数成员集。有关此过程的介绍详见第 7.4.2 节。
在编译时(可能通过重载决策)确定了具体的函数成员后,有关运行时调用函数成员的实际过程的介绍详见第 7.4.3 节。
下表概述了在涉及六个可被显式调用的函数成员类别的构造中发生的处理过程。在下表中,e、x、y 和 value 代表变量或值类别的表达式,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 不是静态的,则用 this 来表达对应的实例。 |
E -= value |
调用包含类或结构中的事件 E 的 remove 访问器。如果 E 不是静态的,则用 this 来表达对应的实例。 |
|
T.E += value |
调用类或结构 T 中事件 E 的 add 访问器。如果 E 不是静态的,则发生编译时错误。 |
|
T.E -= value |
调用类或结构 T 中事件 E 的 remove 访问器。如果 E 不是静态的,则发生编译时错误。 |
|
e.E += value |
用实例表达式 e 调用由 e 的类型给定的类、结构或接口中事件 E 的 add 访问器。如果 E 是静态的,则发生编译时错误。 |
|
e.E -= value |
用实例表达式 e 调用由 e 的类型给定的类、结构或接口中事件 E 的 remove 访问器。如果 E 是静态的,则发生编译时错误。 |
|
索引器访问 |
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) 调用该实例构造函数。 |
1.4.1 参数列表
每个函数成员调用均包括一个参数列表,该列表列出了函数成员参数的实际值或变量引用。如何指定函数成员调用的参数列表的语法取决于函数成员类别:
· 对于实例构造函数、方法和委托,参数被指定为 argument-list,如下所述。
· 对于属性,当调用 get 访问器时,参数列表是空的;而当调用 set 访问器时,参数列表由指定为赋值运算符的右操作数的表达式组成。
· 对于事件,参数列表由指定为 += 或 -= 运算符的右操作数的表达式组成。
· 对于索引器,参数列表由在索引器访问中的方括号之间指定的表达式组成。当调用 set 访问器时,参数列表还需附加上一个表达式,该表达式被指定为赋值运算符的右操作数。
· 对于用户定义的运算符,参数列表由一元运算符的单个操作数或二元运算符的两个操作数组成。
对于属性(第 10.6 节)、事件(第 10.7 节)和用户定义运算符(第 10.9 节),它们的参数总是作为值参数(第 10.5.1.1 节)来传递。索引器(第 10.8 节)的参数总是作为值参数(第 17.5.1.1 节)或参数数组(第 10.5.1.4 节)来传递。这些函数成员类别不支持引用参数和输出参数。
实例构造函数、方法或委托调用的参数按如下的 argument-list 形式指定:
argument-list:
argument
argument-list , argument
argument:
expression
ref variable-reference
out variable-reference
argument-list 由一个或多个 argument 组成,各参数之间用逗号隔开。每个参数都可以采用下列形式之一:
· expression,指示将该参数作为值参数(第 10.5.1.1 节)传递。
· 后跟 variable-reference(第 5.4 节)的关键字 ref,指示将参数作为引用参数(第 10.5.1.2 节)传递。变量在可以作为引用参数传递之前,必须先明确赋值(第 5.3 节)。后跟 variable-reference(第 5.4 节)的关键字 out,指示将参数作为输出参数(第 10.5.1.3 节)传递。在将变量作为输出参数传递的函数成员调用之后,该变量被视为已明确赋值(第 5.3 节)。
在一个函数成员调用(第 7.4.3 节)的运行时处理期间,将按顺序从左到右计算参数列表的表达式或变量引用,具体规则如下:
· 对于值参数,计算参数表达式并执行到相应的参数类型的隐式转换(第 6.1 节)。结果值在函数成员调用中成为该值参数的初始值。
· 对于引用参数或输出参数,计算对应的变量引用,所得的存储位置在函数成员调用中成为该参数表示的存储位置。如果作为引用参数或输出参数给定的变量引用是一个 reference-type 的数组元素,则执行一个运行时检查以确保该数组的元素类型与参数的类型相同。如果检查失败,则引发 System.ArrayTypeMismatchException。
方法、索引器和实例构造函数可以将其最右边的参数声明为参数数组(第 10.5.1.4 节)。是以正常形式还是以展开形式调用这类函数成员取决于哪种形式适用(第 7.4.2.1 节):
· 当以正常形式调用带有参数数组的函数成员时,为参数数组给定的参数必须是属于某个类型的单个表达式,而该类型可隐式转换(第 6.1 节)为参数数组类型。在此情况下,参数数组的作用与值参数完全一样。
· 当以展开形式调用带有参数数组的函数成员时,调用必须为参数数组指定零个或更多参数,其中每个参数都是某个类型的表达式,而该类型可隐式转换(第 6.1 节)为参数数组的元素类型。在此情况下,调用会创建一个该参数数组类型的实例,其所含的元素个数等于给定的参数个数,再用给定的参数值初始化此数组实例的每个元素,然后将新创建的数组实例用作实参。
参数列表的表达式总是按其书写的顺序来计算。因此,示例
class Test
{
static void F(int x, int y, int z) {
System.Console.WriteLine("x = {0}, y = {1}, z = {2}", x, y, z);
}
static void Main() {
int i = 0;
F(i++, i++, i++);
}
}
产生输出
x = 0, y = 1, z = 2
如果存在从 B 到 A 的隐式引用转换,则数组协变规则(第 12.5 节)允许数组类型 A[] 的值成为对数组类型 B[] 的实例的引用。由于这些规则,在将 reference-type 的数组元素作为引用参数或输出参数传递时,需要执行运行时检查以确保该数组的实际元素类型与参数的类型完全一致。在下面的示例中
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。
当以展开形式调用带有参数数组的函数成员时,其调用处理过程完全类似于如下过程:将带有数组初始值设定项(第 7.5.10.2 节)的数组创建表达式插入到展开的参数所在之处。例如,已知下面的声明
void F(int x, int y, params object[] args);
以下方法的展开形式的调用
F(10, 20);
F(10, 20, 30, 40);
F(10, 20, 1, "hello", 3.0);
完全对应于
F(10, 20, new object[] {});
F(10, 20, new object[] {30, 40});
F(10, 20, new object[] {1, "hello", 3.0});
请特别注意,当为参数数组指定的参数的个数为零时,将创建一个空数组。
1.4.2 重载决策
重载决策是一种编译时机制,用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。在 C# 内,重载决策在下列不同的上下文中选择一个应调用的函数成员:
· 要调用的方法的名称出现在 invocation-expression(第 7.5.5 节)中。
· 要调用的实例构造函数的名称出现在 object-creation-expression(第 7.5.10.1 节)中。
· 对一个索引器访问器的调用出现在 element-access(第 7.5.6 节)中。
· 要调用的预定义运算符或用户定义的运算符出现在表达式(第 7.2.3 节和第 7.2.4 节)中。
这些上下文中的每一个都以自己的唯一方式定义候选函数成员集和参数列表,上面列出的章节对此进行了详细说明。例如,方法调用的候选集不包括标记为 override(第 7.3 节)的方法,而且如果派生类中的任何方法可用(第 7.5.5.1 节),则基类中的方法不是候选方法。
一旦确定了候选函数成员和参数列表,对最佳函数成员的选择在所有情况下都相同,都遵循下列规则:
· 如果给定了适用的候选函数成员集,则在其中选出最佳函数成员。如果该集合只包含一个函数成员,则该函数成员为最佳函数成员。否则,最佳函数成员的选择依据是:各成员对给定的参数列表的匹配程度。比所有其他函数成员匹配得更好的那个函数成员就是最佳函数成员,但有一个前提:必须使用第 7.4.2.2 节中的规则将每个函数成员与所有其他函数成员进行比较。如果不是正好有一个函数成员比所有其他函数成员都好,则函数成员调用不明确并发生编译时错误。
下面几节定义有关术语“适用的函数成员”(applicable function member) 和“更好的函数成员”(better function member) 的准确含义。
1.4.2.1 适用函数成员
当所有下列条件都为真时,就称函数成员对于参数列表 A 是一个适用的函数成员 (applicable function member):
· A 中的参数数目与函数成员声明中的参数数目相同。
· 对于 A 中的每个参数,实参的参数传递模式(即值、ref 或 out)与相应参数的参数传递模式相同,而且
o 对于值参数或参数数组,存在从实参类型到相应形参的类型的隐式转换(第 6.1 节),或者
o 对于 ref 或 out 参数,实参的类型与相应形参的类型相同。ref 或 out 参数毕竟只是传递的实参的别名。
对于包含参数数组的函数成员,如果按上述规则判定该函数成员是适用的,则称它以正常形式 (normal form) 适用。如果包含参数数组的函数成员以正常形式不适用,则该函数成员可能以展开形式 (expanded form) 适用:
· 构造展开形式的方法是:用形参数组的元素类型的零个或更多值参数替换函数成员声明中的形参数组,使实参列表 A 中的实参数目匹配总的形参数目。如果 A 中的实参比函数成员声明中的固定形参的数目少,则该函数成员的展开形式无法构造,因而可判定该函数成员不适用。
· 如果声明函数成员的类、结构或接口已经包含另一个与展开形式具有相同签名的适用函数成员,则展开形式不适用。
· 否则,如果对于 A 中的每个实参,它的实参传递模式与相应形参的形参传递模式相同,并且下列条件成立,则称该成员函数以展开形式适用:
o 对于固定值参数或展开操作所创建的值参数,存在从实参的类型到相应形参的类型的隐式转换(第 6.1 节),或者
o 对于 ref 或 out 参数,实参的类型与相应形参的类型相同。
1.4.2.2 更好的函数成员
给定一个带有参数类型集 { A1, A2, ..., AN } 的参数列表 A 和带有参数类型 { P1, P2, ..., PN } 和 { Q1, Q2, ..., QN } 的两个可应用的函数成员 MP 和 MQ,则在以下情况中,MP 定义为比 MQ 更好的函数成员 (better function member):
· 对于每个参数,从 AX 到 PX 的隐式转换都不比从 AX 到 QX 的隐式转换差,并且
· 对于至少一个参数,从 AX 到 PX 的转换比从 AX 到 QX 的转换更好。
当执行此计算时,如果 MP 或 MQ 以展开形式适用,则 PX 或 QX 所代表的是展开形式的参数列表中的参数。
1.4.2.3 更好的转换
假设有一个从类型 S 转换到类型 T1 的隐式转换 C1,和一个从类型 S 转换到类型 T2 的隐式转换 C2,将按下列规则确定这两个转换中哪个是更好的转换 (better conversion):
· 如果 T1 和 T2 是相同类型,则两个转换都不是更好的转换。
· 如果 S 为 T1,则 C1 为更好的转换。
· 如果 S 为 T2,则 C2 为更好的转换。
· 如果存在从 T1 到 T2 的隐式转换,且不存在从 T2 到 T1 的隐式转换,则 C1 为更好的转换。
· 如果存在从 T2 到 T1 的隐式转换,且不存在从 T1 到 T2 的隐式转换,则 C2 为更好的转换。
· 如果 T1 为 sbyte 而 T2 为 byte、ushort、uint 或 ulong,则 C1 为更好的转换。
· 如果 T2 为 sbyte 而 T1 为 byte、ushort、uint 或 ulong,则 C2 为更好的转换。
· 如果 T1 为 short 而 T2 为 ushort、uint 或 ulong,则 C1 为更好的转换。
· 如果 T2 为 short 而 T1 为 ushort、uint 或 ulong,则 C2 为更好的转换。
· 如果 T1 为 int 而 T2 为 uint 或 ulong,则 C1 为更好的转换。
· 如果 T2 为 int 而 T1 为 uint 或 ulong,则 C2 为更好的转换。
· 如果 T1 为 long 而 T2 为 ulong,则 C1 为更好的转换。
· 如果 T2 为 long 而 T1 为 ulong,则 C2 为更好的转换。
· 否则,两个转换都不是更好的转换。
如果按上述规则,确定了隐式转换 C1 是比隐式转换 C2 更好的转换,则 C2 是比 C1 更差的转换 (worse conversion)。
1.4.3 函数成员调用
本节描述在运行时发生的调用一个特定的函数成员的进程。这里假定这个要调用的特定成员,已在编译时进程确定了(可能采用重载决策从一组候选函数成员中选出)。
为了描述调用进程,将函数成员分成两类:
· 静态函数成员。包括实例构造函数、静态方法、静态属性访问器和用户定义的运算符。静态函数成员总是非虚的。
· 实例函数成员。包括实例方法、实例属性访问器和索引器访问器。实例函数成员不是非虚的就是虚的,并且总是在特定的实例上调用。该实例由实例表达式计算,并且在函数成员内可以以 this(第 7.5.7 节)的形式对它进行访问。
函数成员调用的运行时处理包括以下步骤(其中 M 是函数成员,如果 M 是实例成员,则 E 是实例表达式):
· 如果 M 是静态函数成员,则:
o 参数列表按照第 7.4.1 节中的说明进行计算。
o 调用 M。
· 如果 M 是在 value-type 中声明的实例函数成员,则:
o 计算 E。如果该计算导致异常,则不执行进一步的操作。
o 如果 E 没有被归类为一个变量,则创建一个与 E 同类型的临时局部变量,并将 E 的值赋给该变量。这样,E 就被重新归类为对该临时局部变量的一个引用。该临时变量在 M 中可以以 this 的形式被访问,但不能以任何其他形式访问。因此,仅当 E 是真正的变量时,调用方才可能观察到 M 对 this 所做的更改。
o 参数列表按照第 7.4.1 节中的说明进行计算。
o 调用 M。E 引用的变量成为 this 引用的变量。
· 如果 M 是在 reference-type 中声明的实例函数成员,则:
o 计算 E。如果该计算导致异常,则不执行进一步的操作。
o 参数列表按照第 7.4.1 节中的说明进行计算。
o 如果 E 的类型为 value-type,则执行装箱转换(第 4.3.1 节)以将 E 转换为 object 类型,并且在下列步骤中,E 被视为 object 类型。这种情况下,M 只能是 System.Object 的成员。
o 检查 E 的值是否有效。如果 E 的值为 null,则引发 System.NullReferenceException,并且不再执行进一步的操作。
o 要调用的函数成员实现按以下规则确定:
· 如果 E 的编译时类型是接口,则调用的函数成员是 M 的实现,此实现是由 E 引用的实例在运行时所属的类型提供的。确定此函数成员时,应用接口映射规则(第 13.4.2 节)确定由 E 引用的实例运行时类型提供的 M 实现。
· 否则,如果 M 是虚函数成员,则调用的函数成员是由 E 引用的实例运行时类型提供的 M 实现。确定此函数成员时,应用“确定 M 的派生程度最大的实现”的规则(第 10.5.3 节)(相对于 E 引用的实例的运行时类型)。
· 否则,M 是非虚函数成员,调用的函数成员是 M 本身。
o 调用在上一步中确定的函数成员实现。E 引用的对象成为 this 引用的对象。
1.4.3.1 已装箱实例上的调用
在下列情况中,一个在 value-type 中实现的函数成员,可以通过该 value-type 的已装箱的实例来调用:
· 当该函数成员是从 object 类型继承的,且具有 override 修饰符,并通过 object 类型的实例表达式被调用时。
· 当函数成员是接口函数成员的实现并且通过 interface-type 的实例表达式被调用时。
· 当函数成员通过委托被调用时。
在这些情况中,已装箱实例被视为包含 value-type 的一个变量,并且此变量在函数成员调用中成为 this 引用的变量。具体而言,这表示当调用已装箱实例的函数成员时,该函数成员可以修改已装箱实例中包含的值。
1.5 基本表达式
基本表达式包括最简单的表达式形式。
primary-expression:
primary-no-array-creation-expression
array-creation-expression
primary-no-array-creation-expression:
literal
simple-name
parenthesized-expression
member-access
invocation-expression
element-access
this-access
base-access
post-increment-expression
post-decrement-expression
object-creation-expression
delegate-creation-expression
typeof-expression
checked-expression
unchecked-expression
基本表达式分为 array-creation-expressions 和 primary-no-array-creation-expression。采用这种方式处理数组创建表达式(不允许它和其他简单的表达式并列),使语法能够禁止可能的代码混乱,如
object o = new int[3][1];
被另外解释为
object o = (new int[3])[1];
1.5.1 文本
由 literal(第 2.4.4 节)组成的 primary-expression 属于值类别。
1.5.2 简单名称
simple-name 由单个标识符组成。
simple-name:
identifier
simple-name 按下面这样计算和分类:
· 如果 simple-name 在一个 block 内出现,而且该 block(或封闭块)的局部变量声明空间(第 3.3 节)内含有一个以给定的名称命名的局部变量或参数,则该 simple-name 引用该局部变量或参数并归为变量类别。
· 否则,对于每个类型 T(此 T 从该简称的直接封闭类、结构或枚举声明开始,从里到外,遍历所有外部的封闭类或结构声明),在 T 中对 simple-name 实施“成员查找”,如果产生一个匹配,则按下述内容进行:
o 如果 T 为直接封闭类或结构类型,且该成员查找标识了一个或更多方法,则结果是一个具有关联实例表达式 this 的方法组。
o 如果 T 为直接封闭类或结构类型,查找标识出了一个实例成员,并且引用在实例构造函数、实例方法或实例访问器的 block 中发生,则结果与 this.E(其中 E 为 simple-name)形式的成员访问(第 7.5.4 节)相同。
o 否则,结果与 T.E 形式(其中 E 为 simple-name)的成员访问(第 7.5.4 节)相同。在此情况下,会因使用 simple-name 引用了一个实例成员而导致一个编译时错误。
· 否则,从出现 simple-name 的命名空间开始,然后是该命名空间外部的每个封闭命名空间(若有),最后以全局命名空间结束,按下列步骤进行计算,直到找到实体:
o 如果命名空间包含有给定名称的命名空间成员,则 simple-name 引用该成员,并根据该成员归为命名空间或类型类别。
o 否则,如果该命名空间具有一个相应的命名空间声明,将 simple-name 出现的位置包含在其中,则:
· 如果该命名空间声明包含一个 using-alias-directive,它使给定的简称与导入的命名空间或类型相关联,则 simple-name 引用此命名空间或类型。
· 否则,如果该命名空间声明中包含多个 using-namespace-directive,而它们导入的命名空间只包含一个具有给定名称的类型,则 simple-name 引用此类型。
· 否则,如果该命名空间声明中包含多个 using-namespace-directive,而它们导入的命名空间包含有多个具有给定名称的类型,则 simple-name 不明确并发生编译时错误。
· 否则,由 simple-name 给定的名称被认为是未经定义的,并导致编译时错误。
1.5.2.1 块中的固定含义
对于表达式或声明符中以 simple-name 形式给定的标识符的每个匹配项,直接封闭 block(第 8.2 节)或 switch-block(第 8.7.2 节)内的表达式或声明符中同一标识符的每个其他匹配项都必须引用相同的实体。此规则确保一个名称的含义在一个块中总是相同。
在以下示例中:
class Test
{
double x;
void F(bool b) {
x = 1.0;
if (b) {
int x;
x = 1;
}
}
}
将产生编译时错误,这是因为 x 引用外部块(其范围包括 if 语句中的嵌套块)中的不同实体。相反,示例
class Test
{
double x;
void F(bool b) {
if (b) {
x = 1.0;
}
else {
int x;
x = 1;
}
}
}
是允许的,这是因为在外部块中从未使用过名称 x。
注意固定含义的规则仅适用于简称。同一标识符在作为简称时有一种意义,在作为一个成员访问(第 7.5.4 节)的右操作数时具有另一种意义,这是完全合法的。例如:
struct Point
{
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
上面的示例阐释了一个将字段名用作实例构造函数中的参数名的通用模式。在该示例中,简称 x 和 y 引用参数,但这并不妨碍成员访问表达式 this.x 和 this.y 访问字段。
1.5.3 带括号的表达式
parenthesized-expression 由一个用括号括起来的 expression 组成。
parenthesized-expression:
( expression )
通过计算括号内的 expression 计算 parenthesized-expression。如果括号内的 expression 表示命名空间、类型或方法组,则发生编译时错误。否则,parenthesized-expression 的结果为所含 expression 的计算结果。
1.5.4 成员访问
member-access 包括一个 primary-expression 或 predefined-type,后跟“.”标记,再跟一个 identifier。
member-access:
primary-expression . identifier
predefined-type . identifier
predefined-type: 下列之一
bool byte char decimal double float int long
object sbyte short string uint ulong ushort
E.I 形式(其中 E 是一个 primary-expression 或 predefined-type,I 是 identifier)的 member-access 按下面这样计算和分类:
· 如果 E 是命名空间,而 I 是该命名空间的可访问成员的名称,则结果为该成员,并且根据该成员归为命名空间或类型类别。
· 如果 E 是一个 predefined-type 或一个被归类为类型的 primary-expression,并且 E 中的 I 成员查找(第 7.3 节)产生一个匹配,则 E.I 按下面这样计算和分类:
o 如果 I 标识类型,则结果为该类型。
o 如果 I 标识一个或多个方法,则结果为一个没有关联的实例表达式的方法组。
o 如果 I 标识一个 static 属性,则结果为一个没有关联的实例表达式的属性访问。
o 如果 I 标识一个 static 字段,则:
· 如果该字段为 readonly 并且引用发生在声明该字段的类或结构的静态构造函数外,则结果为值,即 E 中静态字段 I 的值。
· 否则,结果为变量,即 E 中的静态字段 I。
o 如果 I 标识一个 static 事件,则:
· 如果引用发生在声明了该事件的类或结构内,并且事件不是用 event-accessor-declarations(第 10.7 节)声明的,则完全将 I 视为静态字段来处理 E.I。
· 否则,结果为没有关联的实例表达式的事件访问。
o 如果 I 标识一个常量,则结果为值,即该常量的值。
o 如果 I 标识枚举成员,则结果为值,即该枚举成员的值。
o 否则,E.I 是无效成员引用,并且发生编译时错误。
· 如果 E 为属性访问、索引器访问、变量或值,它们都属于类型 T,并且 T 中的 I 成员查找(第 7.3 节)产生一个匹配,则 E.I 按下面这样计算和分类:
o 首先,如果 E 为属性访问或索引器访问,则获取属性访问或索引器访问的值(第 7.1.1 节),并且将 E 重新划为值类别。
o 如果 I 标识一个或多个方法,则结果为具有 E 的关联实例表达式的方法组。
o 如果 I 标识实例属性,则结果为具有 E 的关联实例表达式的属性访问。
o 如果 T 为 class-type 并且 I 标识此 class-type 的一个实例字段,则:
· 如果 E 的值为 null,则引发 System.NullReferenceException。
· 否则,如果字段为 readonly 并且引用发生在声明字段的类的实例构造函数外,则结果为值,即 E 引用的对象中字段 I 的值。
· 否则,结果为变量,即 E 引用的对象中的字段 I。
o 如果 T 为 struct-type 并且 I 标识此 struct-type 的实例字段,则:
· 如果 E 为值,或者如果字段为 readonly 并且引用发生在声明字段的结构的实例构造函数外,则结果为值,即 E 给定的结构实例中字段 I 的值。
· 否则,结果为变量,即 E 给定的结构实例中的字段 I。
o 如果 I 标识实例事件,则:
· 如果引用发生在声明事件的类或结构内,并且事件不是用 event-accessor-declarations(第 10.7 节)声明的,则完全将 I 视为实例字段来处理 E.I。
· 否则,结果为具有 E 的关联实例表达式的事件访问。
· 否则,E.I 是无效成员引用,并且发生编译时错误。
1.5.4.1 相同的简称和类型名称
在 E.I 形式的成员访问中,如果 E 为单个标识符,E 可能有两种含义:作为 simple-name(第 7.5.2 节)的 E,作为 type-name(第 3.8 节)的 E。只要前者所标识的对象实体(无论是常量、字段、属性、局部变量或参数)所属的类型就是以后者命名的类型,则 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() {...}
}
class A
{
public Color Color; // Field Color of type Color
void F() {
Color = Color.Black; // References Color.Black static member
Color = Color.Complement(); // Invokes Complement() on Color field
}
static void G() {
Color c = Color.White; // References Color.White static member
}
}
在类 A 中,引用 Color 类型的 Color 标识符的那些匹配项带下划线,而引用 Color 字段的那些匹配项不带下划线。
1.5.5 调用表达式
invocation-expression 用于调用方法。
invocation-expression:
primary-expression ( argument-listopt )
invocation-expression 的 primary-expression 必须是方法组或 delegate-type 的值。如果 primary-expression 是方法组,则 invocation-expression 为方法调用(第 7.5.5.1 节)。如果 primary-expression 是 delegate-type 的值,则 invocation-expression 为委托调用(第 7.5.5.2 节)。如果 primary-expression 既不是方法组也不是 delegate-type 的值,则发生编译时错误。
可选的 argument-list(第 7.4.1 节)列出的值或变量引用在调用时传递给方法中的参数。
invocation-expression 的计算结果按下面这样分类:
· 如果 invocation-expression 调用的方法或委托返回 void,则结果为 Nothing。Nothing 类别的表达式不能是任何运算符的操作数,并且只能在 statement-expression(第 8.6 节)的上下文中使用。
· 否则,结果是由方法或委托返回的类型的值。
1.5.5.1 方法调用
对于方法调用,invocation-expression 的 primary-expression 必须是方法组。方法组标识要调用的一个方法,或者标识从中选择要调用的特定方法的一个重载方法集。在后一种情况中,具体调用那个方法取决于 argument-list 中的参数的类型所提供的上下文。
M(A) 形式(其中 M 是方法组,A 是可选的 argument-list)的方法调用的编译时处理包括以下步骤:
· 构造方法调用的候选方法集。从与 M 关联的、由以前的成员查找(第 7.3 节)找到的方法集合开始,将该集合缩减为对于参数列表 A 适用的那些方法。集合缩减操作包括将下列规则应用于集合中的每个 T.N 方法(其中 T 是声明方法 N 的类型):
o 如果 N 对于 A(第 7.4.2.1 节)不适用,则 N 从集合中移除。
o 如果对于 A(第 7.4.2.1 节),N 适用,则从集合中移除在 T 的基类型中声明的所有方法。
· 如果得到的候选方法集为空,则不存在适用的方法,并发生编译时错误。
· 使用第 7.4.2 节中的重载决策规则确定候选方法集中的最佳方法。如果无法确定单个最佳方法,则该方法调用是不明确的,并发生编译时错误。
· 如果存在最佳方法,则该方法的调用在方法组的上下文中进行验证:如果最佳方法是静态方法,则方法组必须是由一个 simple-name 或关于类型的一个 member-access 产生的。如果最佳方法为实例方法,则方法组必须由 simple-name、通过变量或值的 member-access 或 base-access 产生。如果这两个要求都不满足,则发生编译时错误。
通过以上步骤在编译时选定并验证了方法后,将根据第 7.4.3 节中说明的函数成员调用规则处理实际的运行时调用。
上述决策规则的直觉效果如下:为找到方法调用所调用的特定方法,从方法调用指示的类型开始,在继承链中一直向上查找,直到至少找到一个适用的、可访问的、非重写的方法声明。然后对该类型中声明的适用的、可访问的、非重写的方法集执行重载决策,并调用由此选定的方法。
1.5.5.2 委托调用
对于委托调用,invocation-expression 的 primary-expression 必须是 delegate-type 的值。另外,将 delegate-type 视为与 delegate-type 具有相同的参数列表的函数成员,delegate-type 对于 invocation-expression 的 argument-list 必须是适用的(第 7.4.2.1 节)。
D(A) 形式(其中 D 是 delegate-type 的 primary-expression,A 是可选的 argument-list)的委托调用的运行时处理包括以下步骤:
· 计算 D。如果此计算导致异常,则不执行进一步的操作。
· 检查 D 的值是否有效。如果 D 的值为 null,则引发 System.NullReferenceException,并且不再执行进一步的操作。
· 否则,D 是一个对委托实例的引用。对该委托的调用列表中的每个可调用实体,执行函数成员调用(第 7.4.3 节)。对于由实例和实例方法组成的可调用实体,用于调用的实例是包含在可调用实体中的实例。
1.5.6 元素访问
一个 element-access 由一个 primary-no-array-creation-expression,后面依次跟着“[”标记、expression-list 和“]”标记组成。expression-list 由一个或多个用逗号分隔的 expression 组成。
element-access:
primary-no-array-creation-expression [ expression-list ]
expression-list:
expression
expression-list , expression
如果 element-access 的 primary-no-array-creation-expression 是一个 array-type 的值,则该 element-access 是一个数组访问(第 7.5.6.1 节)。否则,该 primary-no-array-creation-expression 必须是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下,element-access 为索引器访问(第 7.5.6.2 节)。
1.5.6.1 数组访问
对于数组访问,element-access 的 primary-no-array-creation-expression 必须是 array-type 的值。expression-list 中的表达式数目必须与 array-type 的秩相同,并且每个表达式都必须是 int、uint、long、ulong 等类型,或者是可以隐式转换为这些类型中的一个或多个类型的类型。
数组访问的计算结果是数组的元素类型的变量,即由 expression-list 中表达式的值选定的数组元素。
P[A] 形式(其中 P 是 array-type 的 primary-no-array-creation-expression,A 是 expression-list)的数组访问运行时处理包括以下步骤:
· 计算 P。如果此计算导致异常,则不执行进一步的操作。
· expression-list 的索引表达式按从左到右的顺序计算。计算每个索引表达式后,执行到下列类型之一的隐式转换(第 6.1 节):int、uint、long、ulong。选择此列表中第一个存在相应隐式转换的类型。例如,如果索引表达式是 short 类型,则执行到 int 的隐式转换,这是因为可以执行从 short 到 int 和从 short 到 long 的隐式转换。如果计算索引表达式或后面的隐式转换时导致异常,则不再进一步计算索引表达式,并且不再执行进一步的操作。
· 检查 P 的值是否有效。如果 P 的值为 null,则引发 System.NullReferenceException,并且不再执行进一步的操作。
· 针对由 P 引用的数组实例的每个维度的实际界限,检查 expression-list 中每个表达式的值。如果一个或多个值超出了范围,则引发 System.IndexOutOfRangeException,并且不再执行进一步的操作。
· 计算由索引表达式给定的数组元素的位置,此位置将成为数组访问的结果。
1.5.6.2 索引器访问
对于索引器访问,element-access 的 primary-no-array-creation-expression 必须是类、结构或接口类型的变量或值,并且此类型必须实现了一个或多个对于 element-access 的 expression-list 适用的索引器。
P[A] 形式(其中 P 是类、结构或接口类型 T 的一个 primary-no-array-creation-expression,A 是 expression-list)的索引器访问编译时处理包括以下步骤:
· 构造由 T 提供的索引器集。该集合由 T 或 T 的基类型中的所有符合下列条件的索引器组成:它们不是经 override 声明的,并且在当前上下文(第 3.5 节)中可以访问。
· 将该集合缩减为那些适用的并且不被其他索引器隐藏的索引器。对该集合中的每个索引器 S.I(其中 S 为声明索引器 I 的类型)应用下列规则:
o 如果 I 对于 A(第 7.4.2.1 节)不适用,则 I 从集合中移除。
o 如果对于 A(第 7.4.2.1 节),I 适用,则从该集合中移除在 S 的基类型中声明的所有索引器。
· 如果结果候选索引器集为空,则不存在适用的索引器,并发生编译时错误。
· 使用第 7.4.2 节中的重载决策规则确定候选索引器集中的最佳索引器。如果无法确定单个最佳索引器,则该索引器访问是不明确的,并发生编译时错误。
· expression-list 的索引表达式按从左到右的顺序计算。索引器访问的处理结果是属于索引器访问类别的表达式。索引器访问表达式引用在上一步骤中确定的索引器,并具有 P 的关联实例表达式和 A 的关联参数列表。
根据索引器访问的使用上下文,索引器访问导致调用该索引器的 get-accessor 或 set-accessor。如果索引器访问是赋值的目标,则调用 set-accessor 以赋新值(第 7.13.1 节)。在其他所有情况中,调用 get-accessor 以获取当前值(第 7.1.1 节)。
1.5.7 this 访问
this-access 由保留字 this 组成。
this-access:
this
this-access 只能在实例构造函数、实例方法或实例访问器的 block 中使用。它具有下列含义之一:
· 当 this 在类的实例构造函数内的 primary-expression 中使用时,它属于值类别。此时,该值的类型是出现该表达式的类,并且它的值就是对所构造的对象的引用。
· 当 this 在类的实例方法或实例访问器内的 primary-expression 中使用时,它属于值类别。此时,该值的类型是出现该表达式的类,并且它的值就是对为其调用方法或访问器的对象的引用。
· 当 this 在结构的实例构造函数内的 primary-expression 中使用时,它属于变量类别。该变量的类型是此表达式所在的结构,并且该变量表示的正是所构造的结构。结构实例构造函数的 this 变量的行为与结构类型的 out 参数完全一样,具体而言,这表示该变量在实例构造函数的每个执行路径中必须已明确赋值。
· 当 this 在结构的实例方法或实例访问器内的 primary-expression 中使用时,它属于变量类别。该变量的类型是此表达式所在的结构,并且该变量表示的是为其调用方法或访问器的结构。在结构实例方法中,this 变量的行为与结构类型的 ref 参数完全一样。
在以上列出的上下文以外的上下文内的 primary-expression 中使用 this 是编译时错误。具体说就是不能在静态方法、静态属性访问器中或字段声明的 variable-initializer 中引用 this。
1.5.8 base 访问
base-access 由保留字 base,后跟一个“.”标记和一个标识符或一个用方括号括起来的 expression-list 组成:
base-access:
base . identifier
base [ expression-list ]
base-access 用于访问被当前类或结构中名称相似的成员隐藏的基类成员。base-access 只能在实例构造函数、实例方法或实例访问器的 block 中使用。当 base.I 出现在类或结构中时,I 必须表示该类或结构的基类的一个成员。同样,当 base[E] 出现在一个类中时,该类的基类中必须存在适用的索引器。
在编译时,base.I 和 base[E] 形式的 base-access 表达式完全等价于 ((B)this).I 和 ((B)this)[E](其中 B 是所涉及的类或结构的基类)。因此,base.I 和 base[E] 对应于 this.I 和 this[E],但 this 被视为基类的实例。
当某个 base-access 引用虚函数成员(方法、属性或索引器)时,确定在运行时(第 7.4.3 节)调用哪个函数成员的规则有一些更改。确定调用哪一个函数成员的方法是,查找该函数成员相对于 B(而不是相对于 this 的运行时类型,在非 base 访问中通常如此)的派生程度最大的实现(第 10.5.3 节)。因此,在 virtual 函数成员的 override 中,可以使用 base-access 调用该函数成员的被继承了的实现。如果 base-access 引用的函数成员是抽象的,则发生编译时错误。
1.5.9 后缀增量和后缀减量运算符
post-increment-expression:
primary-expression ++
post-decrement-expression:
primary-expression --
后缀增量或后缀减量运算符的操作数必须是属于变量、属性访问或索引器访问类别的表达式。该运算的结果是与操作数类型相同的值。
如果后缀增量或后缀减量运算的操作数为属性或索引器访问,则该属性或索引器必须同时具有 get 和 set 访问器。如果不是这样,则发生编译时错误。
一元运算符重载决策(第 7.2.3 节)被用于选择一个特定的运算符实现。对于下列类型存在着预定义的 ++ 和 -- 运算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 和任何枚举类型。预定义 ++ 运算符返回的结果值为操作数加上 1,预定义 -- 运算符返回的结果值为操作数减去 1。在 checked 上下文中,如果此加法或减法运算的结果在结果类型的范围之外,且结果类型为整型或枚举类型,则会引发 System.OverflowException。
x++ 或 x-- 形式的后缀增量或后缀减量运算的运行时处理包括以下步骤:
· 如果 x 属于变量:
o 计算 x 以产生变量。
o 保存 x 的值。
o 调用选定的运算符,将 x 的保存值作为参数。
o 运算符返回的值存储在由 x 的计算结果给定的位置中。
o x 的保存值成为运算结果。
· 如果 x 属于属性或索引器访问:
o 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),结果用于后面的 get 和 set 访问器调用。
o 调用 x 的 get 访问器并保存返回的值。
o 调用选定的运算符,将 x 的保存值作为参数。
o 调用 x 的 set 访问器,将运算符返回的值作为 value 参数。
o x 的保存值成为运算结果。
++ 和 -- 运算符也支持前缀表示法(第 7.6.5 节)。x++ 或 x-- 的结果是运算“之前”x 的值,而 ++x 或 --x 的结果是运算“之后”x 的值。在任何一种情况下,运算后 x 本身都具有相同的值。
operator ++ 或 operator -- 的实现既可以用后缀表示法调用,也可以用前缀表示法调用。但是,不能让这两种表示法分别去调用该运算符的不同的实现。
1.5.10 new 运算符
new 运算符用于创建类型的新实例。
有三种形式的 new 表达式:
· 对象创建表达式用于创建类类型和值类型的新实例。
· 数组创建表达式用于创建数组类型的新实例。
· 委托创建表达式用于创建委托类型的新实例。
new 运算符表示创建类型的一个实例,但并非暗示要为它动态分配内存。具体而言,值类型的实例不要求在表示它的变量以外有额外的内存,因而,在使用 new 创建值类型的实例时不发生动态分配。
1.5.10.1 对象创建表达式
object-creation-expression 用于创建 class-type 或 value-type 的新实例。
object-creation-expression:
new type ( argument-listopt )
object-creation-expression 的 type 必须是 class-type 或 value-type。该 type 不能是 abstract class-type。
仅当 type 为 class-type 或 struct-type 时才允许使用可选的 argument-list(第 7.4.1 节)。
new T(A) 形式(其中 T 是 class-type 或 value-type,A 是可选 argument-list)的 object-creation-expression 的编译时处理包括以下步骤:
· 如果 T 是 value-type 且 A 不存在:
o object-creation-expression 是默认构造函数调用。object-creation-expression 的结果是 T 类型的一个值,即在第 4.1.1 节中定义的 T 的默认值。
· 否则,如果 T 是 class-type 或 struct-type:
o 如果 T 是 abstract class-type,则会发生编译时错误。
o 使用第 7.4.2 节中的重载决策规则确定要调用的实例构造函数。候选实例构造函数集由 T 中声明的适用于 A(第 7.4.2.1 节)的所有可访问实例构造函数组成。如果候选实例构造函数集合为空,或者无法标识单个最佳实例构造函数,则发生编译时错误。
o object-creation-expression 的结果是 T 类型的值,即由调用在上面的步骤中确定的实例构造函数所产生的值。
· 否则,object-creation-expression 无效,并发生编译时错误。
new T(A) 形式(其中 T 是 class-type 或 struct-type,A 是可选 argument-list)的 object-creation-expression 的运行时处理包括以下步骤:
· 如果 T 是 class-type:
o 为 T 类的一个新实例分配存储位置。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
o 新实例的所有字段初始化为它们的默认值(第 5.2 节)。
o 根据函数成员调用(第 7.4.3 节)的规则来调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,因而,可以从实例构造函数中用 this 来访问将该实例。
· 如果 T 是 struct-type:
o 通过分配一个临时局部变量来创建类型 T 的实例。由于要求 struct-type 的实例构造函数为所创建的实例的每个字段明确赋值,因此不需要初始化此临时变量。
o 根据函数成员调用(第 7.4.3 节)的规则来调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,因而,可以从实例构造函数中用 this 来访问将该实例。
1.5.10.2 数组创建表达式
array-creation-expression 用于创建 array-type 的新实例。
array-creation-expression:
new non-array-type [ expression-list ] rank-specifiersopt array-initializeropt
new array-type array-initializer
第一种形式的数组创建表达式分配一个数组实例,其类型是从表达式列表中删除每个表达式所得到的类型。例如,数组创建表达式 new int[10, 20] 产生 int[,] 类型的数组实例,数组创建表达式 new int[10][,] 产生 int[][,] 类型的数组。表达式列表中的每个表达式必须属于 int、uint、long 或 ulong 类型,或者属于可以隐式转换为一种或多种这些类型的类型。每个表达式的值确定新分配的数组实例中相应维度的长度。由于数组维度的长度必须非负,因此,当表达式列表中出现带有负值的 constant-expression 时,将出现一个编译时错误。
除了在不安全的上下文(第 18.1 节)中外,数组的布局是未指定的。
如果第一种形式的数组创建表达式包含数组初始值设定项,则表达式列表中的每个表达式必须是常量,并且表达式列表指定的秩和维度长度必须匹配数组初始值设定项的秩和维度长度。
在第二种形式的数组创建表达式中,指定数组类型的秩必须匹配数组初始值设定项的秩。各维度长度从数组初始值设定项的每个对应嵌套层数中的元素数推断出。因此,表达式
new int[,] {{0, 1}, {2, 3}, {4, 5}}
完全对应于
new int[3, 2] {{0, 1}, {2, 3}, {4, 5}}
数组初始值设定项的介绍详见第 12.6 节。
数组创建表达式的计算结果属于值类别,即对新分配的数组实例的一个引用。数组创建表达式的运行时处理包括以下步骤:
· expression-list 的维度长度表达式按从左到右的顺序计算。计算每个表达式后,执行到下列类型之一的隐式转换(第 6.1 节):int、uint、long、ulong。选择此列表中第一个存在相应隐式转换的类型。如果表达式计算或后面的隐式转换导致异常,则不计算其他表达式,并且不执行其他步骤。
· 维度长度的计算值按下面这样验证。如果一个或多个值小于零,则引发 System.OverflowException 并且不执行进一步的步骤。
· 分配具有给定维度长度的数组实例。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
· 将新数组实例的所有元素初始化为它们的默认值(第 5.2 节)。
· 如果数组创建表达式包含数组初始值设定项,则计算数组初始值设定项中的每个表达式的值,并将该值赋值给它的相应数组元素。计算和赋值按数组初始值设定项中各表达式的写入顺序执行,换言之,按递增的索引顺序初始化元素,最右边的维度首先增加。如果给定表达式的计算或其面向相应数组元素的赋值导致异常,则不初始化其他元素(剩余的元素将因此具有它们的默认值)。
数组创建表达式允许实例化一个数组,并且它的元素也属于数组类型,但必须手动初始化这类数组的元素。例如,语句
int[][] a = new int[100][];
创建一个包含 100 个 int[] 类型的元素的一维数组。每个元素的初始值为 null。想让数组创建表达式同时也实例化它所指定的子数组是不可能的,因而,语句
int[][] a = new int[100][5]; // Error
会导致编译时错误。实例化子数组必须改为手动执行,如下所示
int[][] a = new int[100][];
for (int i = 0; i < 100; i++) a[i] = new int[5];
当数组的数组具有“矩形”形状时,即当子数组全都具有相同的长度时,使用多维数组更有效。在上面的示例中,实例化一个数组的数组时,实际上创建了 101 个对象(1 个外部数组和 100 个子数组)。相反,
int[,] = new int[100, 5];
只创建单个对象(即一个二维数组)并在单个语句中完成分配。
1.5.10.3 委托创建表达式
delegate-creation-expression 用于创建 delegate-type 的新实例。
delegate-creation-expression:
new delegate-type ( expression )
委托创建表达式的参数必须是方法组(第 7.1 节)或 delegate-type 的值。如果参数是方法组,则它标识方法和(对于实例方法)为其创建委托的对象。如果参数是 delegate-type 的值,则它标识为其创建副本的委托实例。
new D(E) 形式(其中 D 是 delegate-type,E 是 expression)的 delegate-creation-expression 的编译时处理包括以下步骤:
· 如果 E 是方法组:
o 由 E 标识的方法集必须正好包括一个与 D 兼容(第 15.1 节)的方法,并且此方法成为新创建的委托引用的方法。如果不存在匹配的方法,或者如果存在多个匹配的方法,则发生编译时错误。如果选定的方法是实例方法,则与 E 关联的实例表达式确定委托的目标对象。
o 与在方法调用中一样,选定的方法必须与方法组的上下文兼容:如果该方法是静态方法,则方法组必须是由一个 simple-name 或一个通过类型的 member-access 产生的。如果该方法是实例方法,则方法组必须是由一个 simple-name 或一个通过变量或值的 member-access 产生的。如果选定的方法不匹配方法组的上下文,则发生编译时错误。
o 结果是类型 D 的值,即一个引用选定方法和目标对象的新创建的委托。
· 否则,如果 E 是 delegate-type 的值:
o D 和 E 必须兼容(第 15.1 节);否则将发生编译时错误。
o 结果是类型 D 的值,即一个引用与 E 具有相同调用列表的新创建的委托。
· 否则,委托创建表达式无效,并发生编译时错误。
new D(E) 形式(其中 D 是 delegate-type,E 是 expression)的 delegate-creation-expression 的运行时处理包括以下步骤:
· 如果 E 是方法组:
o 如果在编译时选择的方法是静态方法,则委托的目标对象为 null。否则,选定的方法是实例方法,并且从 E 的关联实例表达式确定委托的目标对象:
· 计算实例表达式。如果此计算导致异常,则不执行进一步的操作。
· 如果实例表达式为 reference-type,则由实例表达式计算的值成为目标对象。如果目标对象为 null,则引发 System.NullReferenceException 并且不执行进一步的步骤。
· 如果实例表达式为 value-type,则执行装箱操作(第 4.3.1 节)以将值转换为对象,并且此对象成为目标对象。
o 为委托类型 D 的一个新实例分配存储位置。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
o 用对在编译时确定的方法的引用和对上面计算的目标对象的引用初始化新委托实例。
· 如果 E 是 delegate-type 的值:
o 计算 E。如果此计算导致异常,则不执行进一步的操作。
o 如果 E 的值为 null,则引发 System.NullReferenceException,并且不再执行进一步的操作。
o 为委托类型 D 的一个新实例分配存储位置。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
o 用与 E 给定的委托实例相同的调用列表初始化新委托实例。
委托的调用列表在实例化委托时确定并在委托的整个生存期期间保持不变。换句话说,一旦创建了委托,就不可能更改它的可调用目标实体。当组合两个委托或从一个委托中移除另一个委托(第 15.1 节)时,将产生新委托;现有委托的内容不更改。
不可能创建引用属性、索引器、用户定义的运算符、实例构造函数、析构函数或静态构造函数的委托。
如上所述,当从方法组创建一个委托时,需根据该委托的形参表和返回类型来确定要选择的重载方法。在下面的示例中
delegate double DoubleFunc(double x);
class A
{
DoubleFunc f = new DoubleFunc(Square);
static float Square(float x) {
return x * x;
}
static double Square(double x) {
return x * x;
}
}
A.f 字段将由引用第二个 Square 方法的委托初始化,因为该方法与 DoubleFunc 的形参表和返回类型完全匹配。如果第二个 Square 方法不存在,则将发生编译时错误。
1.5.11 typeof 运算符
typeof 运算符用于获取类型的 System.Type 对象。
typeof-expression:
typeof ( type )
typeof ( void )
typeof-expression 的第一种形式由 typeof 关键字,后跟带括号的 type 组成。这种形式的表达式的结果是与给定的类型对应的 System.Type 对象。任何给定的类型都只有一个 System.Type 对象。这意味着对于类型 T,typeof(T) == typeof(T) 总是为 true。
typeof-expression 的第二种形式由 typeof 关键字,后跟带括号的 void 关键字组成。这种形式的表达式的结果是一个表示“类型不存在”的 System.Type 对象。这种通过 typeof(void) 返回的类型对象与为任何类型返回的类型对象截然不同。这种特殊的类型对象在这样的类库中很有用:它允许在源语言中能仔细考虑一些方法,希望有一种方式以用 System.Type 的实例来表示任何方法(包括 void 方法)的返回类型。
在以下示例中:
using System;
class Test
{
static void Main() {
Type[] t = {
typeof(int),
typeof(System.Int32),
typeof(string),
typeof(double[]),
typeof(void)
};
for (int i = 0; i < t.Length; i++) {
Console.WriteLine(t[i].FullName);
}
}
}
产生下列输出:
System.Int32
System.Int32
System.String
System.Double[]
System.Void
注意 int 和 System.Int32 是相同的类型。
1.5.12 checked 和 unchecked 运算符
checked 和 unchecked 运算符用于控制整型算术运算和转换的溢出检查上下文 (overflow checking context)。
checked-expression:
checked ( expression )
unchecked-expression:
unchecked ( expression )
checked 运算符在 checked 上下文中计算所包含的表达式,unchecked 运算符在 unchecked 上下文中计算所包含的表达式。除了在给定的溢出检查上下文中计算所包含的表达式外,checked-expression 或 unchecked-expression 表达式与 parenthesized-expression(第 7.5.3 节)完全对应。
也可以通过 checked 和 unchecked 语句(第 8.11 节)控制溢出检查上下文。
下列运算受由 checked 和 unchecked 运算符和语句所确定的溢出检查上下文影响:
· 预定义的 ++ 和 -- 一元运算符(第 7.5.9 节和第 7.6.5 节)(当操作数为整型时)。
· 预定义的 - 一元运算符(第 7.6.2 节)(当操作数为整型时)。
· 预定义的 +、-、* 和 / 二元运算符(第 7.7 节)(当两个操作数均为整型时)。
· 从一个整型到另一个整型或从 float 或 double 到整型的显式数值转换(第 6.2.1 节)。
当上面的运算之一产生的结果太大,无法用目标类型表示时,执行运算的上下文控制由此引起的行为:
· 在 checked 上下文中,如果运算发生在一个常量表达式(第 7.15 节)中,则发生编译时错误。否则,当在运行时执行运算时,引发 System.OverflowException。
· 在 unchecked 上下文中,计算的结果被截断,放弃不适合目标类型的任何高序位。
对于不用任何 checked 或 unchecked 运算符或语句括起来的非常量表达式(在运行时计算的表达式),除非外部因素(如编译器开关和执行环境配置)要求 checked 计算,否则默认溢出检查上下文为 unchecked。
对于常量表达式(可以在编译时完全计算的表达式),默认溢出检查上下文总是 checked。除非将常量表达式显式放置在 unchecked 上下文中,否则在表达式的编译时计算期间发生的溢出总是导致编译时错误。
在下面的示例中
class Test
{
static readonly int x = 1000000;
static readonly int y = 1000000;
static int F() {
return checked(x * y); // Throws OverflowException
}
static int G() {
return unchecked(x * y); // Returns -727379968
}
static int H() {
return x * y; // Depends on default
}
}
由于在编译时没有表达式可以计算,所以不报告编译时错误。在运行时,F 方法引发 System.OverflowException,G 方法返回 –727379968(从超出范围的结果中取较低的 32 位)。H 方法的行为取决于编译时设定的默认溢出检查上下文,但它不是与 F 相同就是与 G 相同。
在下面的示例中
class Test
{
const int x = 1000000;
const int y = 1000000;
static int F() {
return checked(x * y); // Compile error, overflow
}
static int G() {
return unchecked(x * y); // Returns -727379968
}
static int H() {
return x * y; // Compile error, overflow
}
}
在计算 F 和 H 中的常量表达式时发生的溢出导致报告编译时错误,原因是表达式是在 checked 上下文中计算的。在计算 G 中的常量表达式时也发生溢出,但由于计算是在 unchecked 上下文中发生的,所以不报告溢出。
checked 和 unchecked 运算符只影响原文包含在“(”和“)”标记中的那些运算的溢出检查上下文。这些运算符不影响因计算包含的表达式而调用的函数成员。在下面的示例中
class Test
{
static int Multiply(int x, int y) {
return x * y;
}
static int F() {
return checked(Multiply(1000000, 1000000));
}
}
在 F 中使用 checked 不影响 Multiply 中的 x * y 计算,因此在默认溢出检查上下文中计算 x * y。
当以十六进制表示法编写有符号整型的常量时,unchecked 运算符很方便。例如:
class Test
{
public const int AllBits = unchecked((int)0xFFFFFFFF);
public const int HighBit = unchecked((int)0x80000000);
}
上面的两个十六进制常量均为 uint 类型。因为这些常量超出了 int 范围,所以如果不使用 unchecked 运算符,强制转换到 int 将产生编译时错误。
checked 和 unchecked 运算符和语句允许程序员控制一些数值计算的某些方面。当然,某些数值运算符的行为取决于其操作数的数据类型。例如,两个小数相乘总是导致溢出异常,即使是在显式 unchecked 构造内也如此。同样,两个浮点数相乘从不会导致溢出异常,即使是在显式 checked 构造内也如此。另外,其他运算符“从不”受检查模式(不管是默认的还是显式的)的影响。
1.6 一元运算符
+、-、!、~、++、-- 和强制转换运算符被称为一元运算符。
unary-expression:
primary-expression
+ unary-expression
- unary-expression
! unary-expression
~ unary-expression
pre-increment-expression
pre-decrement-expression
cast-expression
1.6.1 一元加运算符
对于 +x 形式的运算,应用一元运算符重载决策(第 7.2.3 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果类型是该运算符的返回类型。预定义的一元加运算符为:
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);
对于这些运算符,结果只是操作数的值。
1.6.2 一元减运算符
对于 –x 形式的运算,应用一元运算符重载决策(第 7.2.3 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果类型是该运算符的返回类型。预定义的否定运算符为:
· 整数否定:
int operator –(int x);
long operator –(long x);
通过从 0 中减去 x 来计算结果。如果 x 的值是操作数类型的最小可表示值(对 int 是 −231,对 long 是 −263),则 x 的算术否定在操作数类型中不可表示。如果这种情况发生在 checked 上下文中,则引发 System.OverflowException;如果它发生在 unchecked 上下文中,则结果是操作数的值而且不报告溢出。
如果否定运算符的操作数为 uint 类型,则它转换为 long 类型,并且结果的类型为 long。例外是允许将 int 值 −2147483648 (−231) 写为十进制整数(第 2.4.4.2 节)的规则。
如果否定运算符的操作数为 ulong 类型,则发生编译时错误。例外是允许将 long 值 −9223372036854775808 (−263) 写为十进制整数(第 2.4.4.2 节)的规则。
· 浮点否定:
float operator –(float x);
double operator –(double x);
结果是符号被反转的 x 的值。如果 x 为 NaN,则结果也为 NaN。
· 小数否定:
decimal operator –(decimal x);
通过从 0 中减去 x 来计算结果。小数否定等效于使用 System.Decimal 类型的一元减运算符。
1.6.3 逻辑否定运算符
对于 !x 形式的运算,应用一元运算符重载决策(第 7.2.3 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果类型是该运算符的返回类型。只存在一个预定义的逻辑否定运算符:
bool operator !(bool x);
此运算符计算操作数的逻辑否定:如果操作数为 true,则结果为 false。如果操作数为 false,则结果为 true。
1.6.4 按位求补运算符
对于 ~x 形式的运算,应用一元运算符重载决策(第 7.2.3 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果类型是该运算符的返回类型。预定义的按位求补运算符为:
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) 的计算结果完全相同。
1.6.5 前缀增量和减量运算符
pre-increment-expression:
++ unary-expression
pre-decrement-expression:
-- unary-expression
前缀增量或减量运算的操作数必须是属于变量、属性访问或索引器访问类别的表达式。该运算的结果是与操作数类型相同的值。
如果前缀增量或减量运算的操作数是属性或索引器访问,则属性或索引器必须同时具有 get 和 set 访问器。如果不是这样,则发生编译时错误。
一元运算符重载决策(第 7.2.3 节)被用于选择一个特定的运算符实现。对于下列类型存在着预定义的 ++ 和 -- 运算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 和任何枚举类型。预定义 ++ 运算符返回的结果值为操作数加上 1,预定义 -- 运算符返回的结果值为操作数减去 1。在 checked 上下文中,如果此加法或减法运算的结果在结果类型的范围之外,且结果类型为整型或枚举类型,则会引发 System.OverflowException。
++x 或 --x 形式的前缀增量或前缀减量运算的运行时处理包括以下步骤:
· 如果 x 属于变量:
o 计算 x 以产生变量。
o 调用选定的运算符,将 x 的值作为参数。
o 运算符返回的值存储在由 x 的计算结果给定的位置中。
o 运算符返回的值成为该运算的结果。
· 如果 x 属于属性或索引器访问:
o 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),结果用于后面的 get 和 set 访问器调用。
o 调用 x 的 get 访问器。
o 调用选定的运算符,将 get 访问器返回的值作为参数。
o 调用 x 的 set 访问器,将运算符返回的值作为 value 参数。
o 运算符返回的值成为该运算的结果。
++ 和 -- 运算符也支持后缀表示法(第 7.5.9 节)。x++ 或 x-- 的结果是运算“之前”x 的值,而 ++x 或 --x 的结果是运算“之后”x 的值。在任何一种情况下,运算后 x 本身都具有相同的值。
operator ++ 或 operator -- 的实现既可以用后缀表示法调用,也可以用前缀表示法调用。但是,不能让这两种表示法分别去调用该运算符的不同的实现。
1.6.6 强制转换表达式
cast-expression 用于将表达式显式转换为给定类型。
cast-expression:
( type ) unary-expression
(T)E 形式(其中 T 是 type,E 是 unary-expression)的 cast-expression 执行把 E 的值转换到类型 T 的显式转换(第 6.2 节)。如果不存在从 E 的类型到 T 的显式转换,则发生编译时错误。否则,结果为显式转换产生的值。即使 E 表示变量,结果也总是为值类别。
cast-expression 的语法可能导致某些语法多义性。例如,表达式 (x)-y 既可以按 cast-expression 解释(-y 到类型 x 的强制转换),也可以按结合了 parenthesized-expression 的 additive-expression 解释(计算 x - y 的值)。
为了解决 cast-expression 的多义性,存在下列规则:仅当以下条件中至少一个条件成立时,括在括号中由一个或多个 token(第 2.3.3 节)排列起来的序列才被视为 cast-expression 的开始:
· 标记的序列对于 type 是正确的语法,但对于 expression 则不是。
· 标记的序列对于 type 是正确的语法,而且紧跟在右括号后面的标记是标记“~”、标记“!”、标记“(”、identifier(第 2.4.1 节)、literal(第 2.4.4 节)或除 as 和 is 外的任何 keyword(第 2.4.3 节)。
上面出现的术语“正确的语法”仅指标记的序列必须符合特定的语法产生式。它并没有特别考虑任何构成标识符的实际含义。例如,如果 x 和 y 是标识符,则 x.y 对于类型是正确的语法,即使 x.y 实际并不表示类型。
从上述消除歧义规则可以得出下述结论:如果 x 和 y 是标识符,则 (x)y、(x)(y) 和 (x)(-y) 为 cast-expression,但 (x)-y 不是,即使 x 标识的是类型。然而,如果 x 是一个标识预定义类型(如 int)的关键字,则所有四种形式均为 cast-expression(因为这种关键字本身不可能是表达式)。
1.7 算术运算符
*、/、%、+ 和 – 运算符称为算术运算符。
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
1.7.1 乘法运算符
对于 x * y 形式的运算,应用二元运算符重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下面列出了预定义的乘法运算符。这些运算符均计算 x 和 y 的乘积。
· 整数乘法:
int operator *(int x, int y);
uint operator *(uint x, uint y);
long operator *(long x, long y);
ulong operator *(ulong x, ulong y);
在 checked 上下文中,如果乘积超出结果类型的范围,则引发 System.OverflowException。在 unchecked 上下文中,不报告溢出并且结果类型范围外的任何有效高序位都被放弃。
· 浮点乘法:
float operator *(float x, float y);
double operator *(double x, double y);
根据 IEEE 754 算法法则计算乘积。下表列出了非零有限值、零、无穷大和 NaN 的所有可能的组合结果。在表中,x 和 y 是正有限值,z 是 x * y 的结果。如果结果对目标类型而言太大,则 z 为无穷大。如果结果对目标类型而言太小,则 z 为零。
|
+y |
–y |
+0 |
–0 |
+∞ |
–∞ |
NaN |
+x |
+z |
–z |
+0 |
–0 |
+∞ |
–∞ |
NaN |
–x |
–z |
+z |
–0 |
+0 |
–∞ |
+∞ |
NaN |
+0 |
+0 |
–0 |
+0 |
–0 |
NaN |
NaN |
NaN |
–0 |
–0 |
+0 |
–0 |
+0 |
NaN |
NaN |
NaN |
+∞ |
+∞ |
–∞ |
NaN |
NaN |
+∞ |
–∞ |
NaN |
–∞ |
–∞ |
+∞ |
NaN |
NaN |
–∞ |
+∞ |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
· 小数乘法:
decimal operator *(decimal x, decimal y);
如果结果值太大,不能用 decimal 格式表示,则引发 System.OverflowException。如果结果值太小,不能用 decimal 格式表示,则结果为零。在进行任何舍入之前,结果的小数位数是两个操作数的小数位数的和。
小数乘法等效于使用 System.Decimal 类型的乘法运算符。
1.7.2 除法运算符
对于 x / y 形式的运算,应用二元运算符重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下面列出了预定义的除法运算符。这些运算符均计算 x 和 y 的商。
· 整数除法:
int operator /(int x, int y);
uint operator /(uint x, uint y);
long operator /(long x, long y);
ulong operator /(ulong x, ulong y);
如果右操作数的值为零,则引发 System.DivideByZeroException。
除法将结果舍入到零,并且结果的绝对值是小于两个操作数的商的绝对值的最大可能整数。当两个操作数符号相同时,结果为零或正;当两个操作数符号相反时,结果为零或负。
如果左操作数为最小可表示 int 或 long 值,右操作数为 –1,则发生溢出。在 checked 上下文中,这会导致引发 System.ArithmeticException(或其子类)。在 unchecked 上下文中,它由实现定义为或者引发 System.ArithmeticException(或其子类),或者不以左操作数的结果值报告溢出。
· 浮点除法:
float operator /(float x, float y);
double operator /(double x, double y);
根据 IEEE 754 算法法则计算商。下表列出了非零有限值、零、无穷大和 NaN 的所有可能的组合结果。在表中,x 和 y 是正有限值,z 是 x / y 的结果。如果结果对目标类型而言太大,则 z 为无穷大。如果结果对目标类型而言太小,则 z 为零。
|
+y |
–y |
+0 |
–0 |
+∞ |
–∞ |
NaN |
+x |
+z |
–z |
+∞ |
–∞ |
+0 |
–0 |
NaN |
–x |
–z |
+z |
–∞ |
+∞ |
–0 |
+0 |
NaN |
+0 |
+0 |
–0 |
NaN |
NaN |
+0 |
–0 |
NaN |
–0 |
–0 |
+0 |
NaN |
NaN |
–0 |
+0 |
NaN |
+∞ |
+∞ |
–∞ |
+∞ |
–∞ |
NaN |
NaN |
NaN |
–∞ |
–∞ |
+∞ |
–∞ |
+∞ |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
· 小数除法:
decimal operator /(decimal x, decimal y);
如果右操作数的值为零,则引发 System.DivideByZeroException。如果结果值太大,不能用 decimal 格式表示,则引发 System.OverflowException。如果结果值太小,不能用 decimal 格式表示,则结果为零。结果的小数位数是最小的小数位数,它保留等于最接近真实算术结果的可表示小数值的结果。
小数除法等效于使用 System.Decimal 类型的除法运算符。
1.7.3 余数运算符
对于 x % y 形式的运算,应用二元运算符重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下面列出了预定义的余数运算符。这些运算符均计算 x 除以 y 的余数。
· 整数余数:
int operator %(int x, int y);
uint operator %(uint x, uint y);
long operator %(long x, long y);
ulong operator %(ulong x, ulong y);
x % y 的结果是表达式 x – (x / y) * y 的值。如果 y 为零,则引发 System.DivideByZeroException。
如果左侧的操作数是最小的 int 或 long 值,且右侧的操作数是 -1,则会引发 System.OverflowException¡£只要 x / y 不引发异常,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 的最大可能整数)计算。这种计算余数的方法类似于用于整数操作数的方法,但不同于 IEEE 754 定义(在此定义中,n 是最接近 x / y 的整数)。
|
+y |
–y |
+0 |
–0 |
+∞ |
–∞ |
NaN |
+x |
+z |
+z |
NaN |
NaN |
x |
x |
NaN |
–x |
–z |
–z |
NaN |
NaN |
–x |
–x |
NaN |
+0 |
+0 |
+0 |
NaN |
NaN |
+0 |
+0 |
NaN |
–0 |
–0 |
–0 |
NaN |
NaN |
–0 |
–0 |
NaN |
+∞ |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
–∞ |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
NaN |
· 小数余数:
decimal operator %(decimal x, decimal y);
如果右操作数的值为零,则引发 System.DivideByZeroException。在进行任何舍入之前,结果的小数位数是两个操作数中较大的小数位数,而且结果的符号与 x 的相同(如果非零)。
小数余数等效于使用 System.Decimal 类型的余数运算符。
1.7.4 加法运算符
对于 x + y 形式的运算,应用二元运算符重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下面列出了预定义的加法运算符。对于数值和枚举类型,预定义的加法运算符计算两个操作数的和。当一个或两个操作数为 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);
根据 IEEE 754 算术运算法则计算和。下表列出了非零有限值、零、无穷大和 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);
如果结果值太大,不能用 decimal 格式表示,则引发 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);
当一个或两个操作数为 string 类型时,二元 + 运算符执行字符串串联。在字符串串联运算中,如果它的一个操作数为 null,则用空字符串来替换此操作数。否则,任何非字符串参数都通过调用从 object 类型继承的虚 ToString 方法,转换为它的字符串表示形式。如果 ToString 返回 null,则替换成空字符串。
using System;
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+
Console.WriteLine("f = " + f); // displays f = 1.23E+15
decimal d =
Console.WriteLine("d = " + d); // displays d = 2.900
}
}
字符串串联运算符的结果是一个字符串,由左操作数的字符后跟右操作数的字符组成。字符串串联运算符从不返回 null 值。如果没有足够的内存可用于分配得到的字符串,则可能引发 System.OutOfMemoryException。
· 委托组合。每个委托类型都隐式提供以下预定义运算符,其中 D 是委托类型:
D operator +(D x, D y);
当两个操作数均为某个委托类型 D 时,二元 + 运算符执行委托组合。(如果操作数具有不同的委托类型,则发生编译时错误。)如果第一个操作数为 null,则运算结果为第二个操作数的值(即使此操作数也为 null)。否则,如果第二个操作数为 null,则运算结果为第一个操作数的值。否则,运算结果是一个新委托实例,该实例在被调用时调用第一个操作数,然后调用第二个操作数。有关委托组合的示例,请参见第 7.7.5 节和第 15.3 节。由于 System.Delegate 不是委托类型,因此不为它定义 operator +。
1.7.5 减法运算符
对于 x – y 形式的运算,应用二元运算符重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下面列出了预定义的减法运算符。这些运算符均从 x 中减去 y。
· 整数减法:
int operator –(int x, int y);
uint operator –(uint x, uint y);
long operator –(long x, long y);
ulong operator –(ulong x, ulong y);
在 checked 上下文中,如果差超出结果类型的范围,则引发 System.OverflowException。在 unchecked 上下文中,不报告溢出并且结果类型范围外的任何有效高序位都被放弃。
· 浮点减法:
float operator –(float x, float y);
double operator –(double x, double y);
根据 IEEE 754 算术运算法则计算差。下表列出了非零有限值、零、无穷大和 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);
如果结果值太大,不能用 decimal 格式表示,则引发 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);
当两个操作数均为某个委托类型 D 时,二元 – 运算符执行委托移除。如果操作数具有不同的委托类型,则发生编译时错误。如果第一个操作数为 null,则运算结果为 null。否则,如果第二个操作数为 null,则运算结果为第一个操作数的值。否则,两个操作数都表示包含一项或多项的调用列表(第 15.1 节),并且只要第二个操作数列表是第一个操作数列表的适当的邻接子列表,那么结果就是从第一个操作数的调用列表中移除了第二个操作数的调用列表所含各项后的一个新调用列表。(为确定子列表是否相等,用委托相等运算符比较相对应的项。请参见第 7.9.8 节。)否则,结果为左操作数的值。在此过程中两个操作数的列表均未被更改。如果第二个操作数的列表与第一个操作数的列表中的多个邻接项子列表相匹配,则移除最右边的那个匹配邻接项的子列表。如果移除导致空列表,则结果为 null。例如:
delegate void D(int x);
class C
{
public static void M1(int i) { /* … */ }
public static void M2(int i) { /* … */ }
}
class Test
{
static void Main() {
D cd1 = new D(C.M1);
D cd2 = new D(C.M2);
D cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1; // => M1 + M2 + M2
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1 + cd2; // => M2 + M1
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd2 + cd2; // => M1 + M1
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd2 + cd1; // => M1 + M2
cd3 = cd1 + cd2 + cd2 + cd1; // M1 + M2 + M2 + M1
cd3 -= cd1 + cd1; // => M1 + M2 + M2 + M1
}
}
1.8 移位运算符
<< 和 >> 运算符用于执行移位运算。
shift-expression:
additive-expression
shift-expression << additive-expression
shift-expression >> additive-expression
对于 x << count 或 x >> count 形式的运算,应用二元运算符重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
当声明重载移位运算符时,第一个操作数的类型必须总是包含运算符声明的类或结构,并且第二个操作数的类型必须总是 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 的低序的 5 位给出。换言之,位移计数由 count & 0x
· 当 x 的类型为 long 或 ulong 时,位移计数由 count 的低序的 6 位给出。换言之,位移计数由 count & 0x
如果计算位移计数的结果为零,则移位运算符只返回 x 的值。
移位运算从不会导致溢出,并且在 checked 和 unchecked 上下文中产生的结果相同。
当 >> 运算符的左操作数为有符号的整型时,该运算符执行算术右移位,在此过程中,操作数的最有效位(符号位)的值扩展到高序空位位置。当 >> 运算符的左操作数为无符号的整型时,该运算符执行逻辑右移位,在此过程中,高序空位位置总是设置为零。若要执行与由操作数类型确定的不同的移位运算,可以使用显式强制转换。例如,如果 x 是 int 类型的变量,则 unchecked((int)((uint)x >> y)) 运算执行 x 的逻辑右移位。
1.9 关系和类型测试运算符
==、!=、<、>、<=、>=、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 as type
equality-expression:
relational-expression
equality-expression == relational-expression
equality-expression != relational-expression
is 和 as 运算符分别在第 7.9.9 节和第 7.9.10 节中说明。
==、!=、<、>、<= 和 >= 运算符为比较运算符 (comparison operator)。对于 x op y 形式(其中 op 为比较运算符)的运算,应用重载决策(第 7.2.4 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
预定义的比较运算符详见下面各节的介绍。所有预定义的比较运算符都返回 bool 类型的结果,详见下表。
运算 |
结果 |
x == y |
如果 x 等于 y,则为 true,否则为 false |
x != y |
如果 x 不等于 y,则为 true,否则为 false |
x < y |
如果 x 小于 y,则为 true,否则为 false |
x > y |
如果 x 大于 y,则为 true,否则为 false |
x <= y |
如果 x 小于等于 y,则为 true,否则为 false |
x >= y |
如果 x 大于等于 y,则为 true,否则为 false |
1.9.1 整数比较运算符
预定义的整数比较运算符为:
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。
1.9.2 浮点比较运算符
预定义的浮点比较运算符为:
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);
这些运算符根据 IEEE 754 标准法则比较操作数:
· 如果两个操作数中的任何一个为 NaN,则对于除 !=(对于此运算符,结果为 true)外的所有运算符,结果为 false。对于任何两个操作数,x != y 总是产生与 !(x == y) 相同的结果。然而,当一个操作数或两个操作数为 NaN 时,<、>、<= 和 >= 运算符不产生与其对应的反向运算符的逻辑否定相同的结果。例如,如果 x 和 y 中的任何一个为 NaN,则 x < y 为 false,而 !(x >= y) 为 true。
· 当两个操作数都不为 NaN 时,这些运算符就按下列排序来比较两个浮点操作数的值
–∞ < –max < ... < –min < –0.0 == +0.0 < +min < ... < +max < +∞
这里的 min 和 max 是可以用给定浮点格式表示的最小和最大正有限值。这样排序的显著特点是:
o 负零和正零被视为相等。
o 负无穷大被视为小于所有其他值,但等于其他负无穷大。
o 正无穷大被视为大于所有其他值,但等于其他正无穷大。
1.9.3 小数比较运算符
预定义的小数比较运算符为:
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 类型的相应关系运算符或相等运算符。
1.9.4 布尔相等运算符
预定义的布尔相等运算符为:
bool operator ==(bool x, bool y);
bool operator !=(bool x, bool y);
如果 x 和 y 都为 true,或者如果 x 和 y 都为 false,则 == 的结果为 true。否则,结果为 false。
如果 x 和 y 都为 true,或者 x 和 y 都为 false,则 != 的结果为 false。否则,结果为 true。当操作数为 bool 类型时,!= 运算符产生与 ^ 运算符相同的结果。
1.9.5 枚举比较运算符
每种枚举类型都隐式提供下列预定义的比较运算符:
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) 的计算结果完全相同。换言之,枚举类型比较运算符只比较两个操作数的基础整数值。
1.9.6 引用类型相等运算符
预定义的引用类型相等运算符为:
bool operator ==(object x, object y);
bool operator !=(object x, object y);
这些运算符返回两个引用是相等还是不相等的比较结果。
由于预定义的引用类型相等运算符接受 object 类型的操作数,因此它们适用于所有那些没有为自己声明适用的 operator == 和 operator != 成员的类型。相反,任何适用的用户定义的相等运算符都有效地隐藏上述预定义的引用类型相等运算符。
预定义引用类型相等运算符要求操作数是 reference-type 值或 null 值;此外,它们还要求存在从一种操作数类型到另一种操作数类型的标准隐式转换(第 6.3.1 节)。除非这两个条件都为真,否则将发生编译时错误。这些规则中值得注意的含义是:
· 使用预定义的引用类型相等运算符比较两个在编译时已能确定是不相同的引用时,会导致编译时错误。例如,如果操作数的编译时类型分属两个类类型 A 和 B,并且 A 和 B 都不从对方派生,则两个操作数不可能引用同一对象。因此,此运算被认为是编译时错误。
· 预定义的引用类型相等运算符不允许比较值类型操作数。因此,除非结构类型声明自己的相等运算符,否则不可能比较该结构类型的值。
· 预定义的引用类型相等运算符从不会导致对它们的操作数执行装箱操作。执行此类装箱操作毫无意义,这是因为对新分配的已装箱实例的引用必将不同于所有其他引用。
对于 x == y 或 x != y 形式的运算,如果存在任何适用的 operator == 或 operator !=,运算符重载决策(第 7.2.4 节)规则将选择该运算符而不是上述的预定义的引用类型相等运算符。不过,始终可以通过将一个或两个操作数显式强制转换为 object 类型来选择预定义的引用类型相等运算符。在以下示例中:
using System;
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 引用两个包含相同字符的不同 string 实例。第一个比较输出 True,这是因为是在两个操作数都为 string 类型时选定预定义的字符串相等运算符(第 7.9.7 节)。其余的比较全都输出 False,这是因为是在一个或两个操作数为 object 类型时选定预定义的引用类型相等运算符。
注意,以上技术对值类型没有意义。在以下示例中:
class Test
{
static void Main() {
int i = 123;
int j = 123;
System.Console.WriteLine((object)i == (object)j);
}
}
输出 False,这是因为强制转换创建对已装箱 int 值的两个单独实例的引用。
1.9.7 字符串相等运算符
预定义的字符串相等运算符为:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
当下列条件中有一个为真时,两个 string 值被视为相等:
· 两个值都为 null。
· 两个值都是对字符串实例的非空引用,这两个字符串不仅具有相同的长度,而且在每个字符位置上的字符亦都彼此相同。
字符串相等运算符比较的是字符串值而不是对字符串的引用。当两个单独的字符串实例包含完全相同的字符序列时,字符串的值相等,但引用不相同。正如第 7.9.6 节中所描述的那样,引用类型相等运算符可用于比较字符串引用而不是字符串值。
1.9.8 委托相等运算符
每个委托类型都隐式地提供下列预定义的比较运算符:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
两个委托实例按下面这样被视为相等:
· 如果两个委托实例中有一个为 null,则当且仅当它们都为 null 时相等。
· 如果两个委托实例中有一个具有包含一个项的调用列表(第 15.1 节),则当且仅当另一个委托实例也具有包含一个项的调用列表且符合下列条件之一时,两个委托实例相等:
· 两者引用同一静态方法,或
· 两者都引用同一目标对象的同一非静态方法。
· 如果两个委托实例中有一个具有包含两项或更多项的调用列表,则当且仅当它们的调用列表长度相同,并且一个实例的调用列表中的每项依次等于另一个的调用列表中的相应项时,这两个委托实例相等。
注意,根据上面的定义,只要委托具有相同的返回类型和参数类型,即使它们的类型不同也被视为相等。
1.9.9 is 运算符
is 运算符用于动态检查对象的运行时类型是否与给定类型兼容。e is T 运算(其中 e 为表达式,T 为类型)的结果是布尔值,表示 e 的类型是否可通过引用转换、装箱转换或拆箱转换成功地转换为类型 T。运算按下面这样计算:
· 如果 e 的编译时类型与 T 相同,或存在从 e 的编译时类型到 T 的隐式引用转换(第 6.1.4 节)或装箱转换(第 6.1.5 节):
o 如果 e 为引用类型,则运算结果等效于计算 e != null。
o 如果 e 为值类型,则运算结果为 true。
· 否则,如果存在从 e 的编译时类型到 T 的显式引用转换(第 6.2.3 节)或拆箱转换(第 6.2.4 节),则执行动态类型检查:
o 如果 e 的值为 null,则结果为 false。
o 否则,假设 R 为 e 引用的实例的运行时类型。如果 R 和 T 的类型相同,或者如果 R 为引用类型且存在从 R 到 T 的隐式引用转换,或者如果 R 为值类型且 T 为 R 实现的接口类型,则结果为 true。
o 否则,结果为 false。
· 否则,不可能实现从 e 到类型 T 的引用转换或装箱转换,且运算结果为 false。
请注意,is 运算符只考虑引用转换、装箱转换和拆箱转换。其他转换(如用户定义的转换)不在 is 运算符考虑之列。
1.9.10 as 运算符
as 运算符用于将一个值显式地转换(使用引用转换或装箱转换)为一个给定的引用类型。与强制转换表达式(第 7.6.6 节)不同,as 运算符从不引发异常。它采用的是:如果指定的转换不可能实施,则运算结果为 null。
在 e as T 形式的运算中,e 必须是表达式,T 必须是引用类型。该运算的结果属于类型 T,且总是可归类为值类别。运算按下面这样计算:
· 如果 e 的编译时类型与 T 相同,则结果就是 e 的值。
· 否则,如果存在从 e 的编译时类型到 T 的隐式引用转换(第 6.1.4 节)或装箱转换(第 6.1.5 节),则执行该转换,且该转换的结果就是运算结果。
· 否则,如果存在从 e 的编译时类型到 T 的显式引用转换(第 6.2.3 节),则执行动态类型检查:
o 如果 e 的值为 null,则结果为具有编译时类型 T 的值 null。
o 否则,假设 R 为 e 引用的实例的运行时类型。如果 R 和 T 的类型相同,或者如果 R 为引用类型且存在从 R 到 T 的隐式引用转换,或者如果 R 为值类型且 T 是由 R 实现的一个接口类型,则结果为由 e 给定的具有编译时类型 T 的引用。
o 否则,结果为具有编译时类型 T 的值 null。
· 否则,指定的转换根本不可能实现,且发生编译时错误。
注意,as 运算符只执行引用转换和装箱转换。不可能使用 as 运算符执行其他转换(如用户定义的转换),应改为使用强制转换表达式来执行这些转换。
1.10 逻辑运算符
&、^ 和 | 运算符称为逻辑运算符。
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
对于 x op y 形式的运算(其中 op 为一个逻辑运算符),应用重载决策(第 7.2.4 节)以选择一个特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下列章节介绍了预定义的逻辑运算符。
1.10.1 整数逻辑运算符
预定义的整数逻辑运算符为:
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,而 ^ 运算符计算两个操作数的按位逻辑 XOR。这些运算不可能产生溢出。
1.10.2 枚举逻辑运算符
每个枚举类型 E 都隐式地提供下列预定义的逻辑运算符:
E operator &(E x, E y);
E operator |(E x, E y);
E operator ^(E x, E y);
x op y(其中 x 和 y 是具有基础类型 U 的枚举类型 E 的表达式,op 是一个逻辑运算符)的计算结果与 (E)((U)x op (U)y) 的计算结果完全相同。换言之,枚举类型逻辑运算符直接对两个操作数的基础类型执行逻辑运算。
1.10.3 布尔逻辑运算符
预定义的布尔逻辑运算符为:
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 为 true 而 y 为 false,或者 x 为 false 而 y 为 true,则 x ^ y 的结果为 true。否则,结果为 false。当操作数为 bool 类型时,^ 运算符计算结果与 != 运算符相同。
1.11 条件逻辑运算符
&& 和 || 运算符称为条件逻辑运算符。也称为“短路”逻辑运算符。
conditional-and-expression:
inclusive-or-expression
conditional-and-expression && inclusive-or-expression
conditional-or-expression:
conditional-and-expression
conditional-or-expression || conditional-and-expression
&& 和 || 运算符是 & 和 | 运算符的条件版本:
· x && y 运算对应于 x & y 运算,但仅当 x 为 true 时才计算 y。
· x || y 运算对应于 x | y 运算,但仅当 x 为 false 时才计算 y。
x && y 或 x || y 形式的运算通过采用重载决策(第 7.2.4 节)按运算被写为 x & y 或 x | y 来处理。然后,
· 如果重载决策未能找到单个最佳运算符,或者重载决策选择一个预定义的整数逻辑运算符,则发生编译时错误。
· 否则,如果选定的运算符是一个预定义的布尔逻辑运算符(第 7.10.3 节),则运算按(第 7.11.1 节)中所描述的那样进行处理。
· 否则,选定的运算符为用户定义的运算符,且运算按(第 7.11.2 节)中所描述的那样进行处理。
不可能直接重载条件逻辑运算符。不过,由于条件逻辑运算符按通常的逻辑运算符计算,因此通常的逻辑运算符的重载,在某些限制条件下,也被视为条件逻辑运算符的重载。第 7.11.2 节对此有进一步描述。
1.11.1 布尔条件逻辑运算符
当 && 或 || 的操作数为 bool 类型时,或者当操作数的类型本身未定义适用的 operator & 或 operator |,但确实定义了到 bool 的隐式转换时,运算按下面这样处理:
· 运算 x && y 的求值过程相当于 x ? y : false。换言之,首先计算 x 并将其转换为 bool 类型。如果 x 为 true,则计算 y 并将其转换为 bool 类型,并且这成为运算结果。否则,运算结果为 false。
· 运算 x || y 的求值过程相当于 x ? true : y。换言之,首先计算 x 并将其转换为 bool 类型。然后,如果 x 为 true,则运算结果为 true。否则,计算 y 并将其转换为 bool 类型,并且这作为运算结果。
1.11.2 用户定义的条件逻辑运算符
当 && 或 || 的操作数所属的类型声明了适用的用户定义的 operator & 或 operator | 时,下列两个条件必须都为真(其中 T 是声明的选定运算符的类型):
· 选定运算符的返回类型和每个参数的类型都必须为 T。换言之,该运算符必须计算类型为 T 的两个操作数的逻辑 AND 或逻辑 OR,且必须返回类型为 T 的结果。
· T 必须包含关于 operator true 和 operator false 的声明。
如果这两个要求中有一个未满足,则发生编译时错误。如果这两个要求都满足,则通过将用户定义的 operator true 或 operator false 与选定的用户定义的运算符组合在一起来计算 && 运算或 || 运算:
· x && y 运算按 T.false(x) ? x : T.&(x, y) 进行计算,其中 T.false(x) 是 T 中声明的 operator false 的调用,T.&(x, y) 是选定 operator & 的调用。换言之,首先计算 x,并对结果调用 operator false 以确定 x 是否肯定为假。如果 x 肯定为假,则运算结果为先前为 x 计算的值。否则将计算 y,并对先前为 x 计算的值和为 y 计算的值调用选定的 operator & 以产生运算结果。
· x || y 运算按 T.true(x) ? x : T.|(x, y) 进行计算,其中 T.true(x) 是 T 中声明的 operator true 的调用,T.|(x, y) 是选定 operator | 的调用。换言之,首先计算 x,并对结果调用 operator true 以确定 x 是否肯定为真。然后,如果 x 肯定为真,则运算结果为先前为 x 计算的值。否则将计算 y,并对先前为 x 计算的值和为 y 计算的值调用选定的 operator | 以产生运算结果。
在这两个运算中,x 给定的表达式只计算一次,y 给定的表达式要么不计算,要么只计算一次。
有关实现 operator true 和 operator false 的类型的示例,请参见第 11.4.2 节。
1.12 条件运算符
?: 运算符称为条件运算符。有时,它也称为三元运算符。
conditional-expression:
conditional-or-expression
conditional-or-expression ? expression : expression
b ? x : y 形式的条件表达式首先计算条件 b。然后,如果 b 为 true,则计算 x,并且它成为运算结果。否则计算 y,并且它成为运算结果。条件表达式从不同时计算 x 和 y。
条件运算符向右关联,表示运算从右到左分组。例如,a ? b : c ? d : e 形式的表达式按 a ? b : (c ? d : e) 计算。
?: 运算符的第一个操作数必须是可以隐式转换为 bool 的类型的表达式,或者是实现 operator true 的类型的表达式。如果两个要求都不满足,则发生编译时错误。
?: 运算符的第二个和第三个操作数决定了条件表达式的类型。设 X 和 Y 为第二个和第三个操作数所属的类型。然后,
· 如果 X 和 Y 的类型相同,则此类型为该条件表达式的类型。
· 如果存在从 X 到 Y 的隐式转换(第 6.1 节),但不存在从 Y 到 X 的隐式转换,则 Y 为条件表达式的类型。
· 如果存在从 Y 到 X 的隐式转换(第 6.1 节),但不存在从 X 到 Y 的隐式转换,则 X 为条件表达式的类型。
b ? x : y 形式的条件表达式的运行时处理包括以下步骤:
· 首先计算 b,并确定 b 的 bool 值:
o 如果存在从 b 的类型到 bool 的隐式转换,则执行该隐式转换以产生 bool 值。
o 否则,调用 b 的类型中定义的 operator true 以产生 bool 值。
· 如果以上步骤产生的 bool 值为 true,则计算 x 并将其转换为条件表达式的类型,且这成为条件表达式的结果。
· 否则,计算 y 并将其转换为条件表达式的类型,且这成为条件表达式的结果。
1.13 赋值运算符
赋值运算符为变量、属性、事件或索引器元素赋新值。
assignment:
unary-expression assignment-operator expression
assignment-operator: 下列之一
= += -= *= /= %= &= |= ^= <<= >>=
赋值的左操作数必须是属于变量、属性访问、索引器访问或事件访问类别的表达式。
= 运算符称为简单赋值运算符 (simple assignment operator)。它将右操作数的值赋予左操作数给定的变量、属性或索引器元素。简单赋值运算符的左操作数一般不可能是一个事件访问(第 10.7.1 节中描述的例外)。简单赋值运算符的介绍详见第 7.13.1 节。
除 = 运算符以外的赋值运算符称为复合赋值运算符 (compound assignment operators)。这些运算符对两个操作数执行指示的运算,然后将结果值赋予左操作数指定的变量、属性或索引器元素。复合赋值运算符的介绍详见第 7.13.2 节。
以事件访问表达式作为左操作数的 += 和 -= 运算符称为 event assignment operators。当左操作数是事件访问时,其他赋值运算符都是无效的。事件赋值运算符的介绍详见第 7.13.3 节。
赋值运算符为向右关联,即此类运算从右到左分组。例如,a = b = c 形式的表达式按 a = (b = c) 计算。
1.13.1 简单赋值
= 运算符称为简单赋值运算符。在简单赋值中,右操作数表达式所属的类型必须可隐式地转换为左操作数所属的类型。运算将右操作数的值赋予左操作数指定的变量、属性或索引器元素。
简单赋值表达式的结果是赋予左操作数的值。结果的类型与左操作数相同,且始终为值类别。
如果左操作数为属性或索引器访问,则该属性或索引器必须具有 set 访问器。如果不是这样,则发生编译时错误。
x = y 形式的简单赋值的运行时处理包括以下步骤:
· 如果 x 属于变量:
o 计算 x 以产生变量。
o 计算 y,必要时还需通过隐式转换(第 6.1 节)将其转换为 x 的类型。
o 如果 x 给定的变量是 reference-type 的数组元素,则执行运行时检查以确保为 y 计算的值与以 x 为其元素的那个数组实例兼容。如果 y 为 null,或存在从 y 引用的实例的实际类型到包含 x 的数组实例的实际元素类型的隐式引用转换(第 6.1.4 节),则检查成功。否则,引发 System.ArrayTypeMismatchException。
o y 的计算和转换后所产生的值存储在 x 的计算所确定的位置中。
· 如果 x 属于属性或索引器访问:
o 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),结果用于后面的对 set 访问器调用。
o 计算 y,必要时还需通过隐式转换(第 6.1 节)将其转换为 x 的类型。
o 调用 x 的 set 访问器,并将 y 的上述结果值作为该访问器的 value 参数。
如果存在从 B 到 A 的隐式引用转换,则数组协变规则(第 12.5 节)允许数组类型 A[] 的值成为对数组类型 B[] 的实例的引用。由于这些规则,对 reference-type 的数组元素的赋值需要运行时检查以确保所赋的值与数组实例兼容。在下面的示例中
string[] sa = new string[10];
object[] oa = sa;
oa[0] = null; // Ok
oa[1] = "Hello"; // Ok
oa[2] = new ArrayList(); // ArrayTypeMismatchException
最后的赋值导致引发 System.ArrayTypeMismatchException,这是因为 ArrayList 的实例不能存储在 string[] 的元素中。
当 struct-type 中声明的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式必须为变量类别。如果该实例表达式归类为值类别,则发生编译时错误。由于第 7.5.4 节中所说明的原因,同样的规则也适用于字段。
给定下列声明:
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 和 r 为变量,因此允许对 p.X、p.Y、r.A 和 r.B 赋值。但是,在以下示例中
Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;
由于 r.A 和 r.B 不是变量,所以赋值全部无效。
1.13.2 复合赋值
x op= y 形式的运算是这样来处理的:先将二元运算符重载决策(第 7.2.4 节)应用于运算 x op y。然后,
· 如果选定的运算符的返回类型可“隐式”转换为 x 的类型,则运算按 x = x op y 计算,但 x 只计算一次。
· 否则,如果选定运算符是预定义的运算符,选定运算符的返回类型可“显式”转换为 x 的类型,并且 y 可“隐式”转换为 x 的类型,则运算按 x = (T)(x op y) 计算(其中 T 是 x 的类型),但 x 只计算一次。
· 否则,复合赋值无效,且发生编译时错误。
术语“只计算一次”表示:在 x op y 的计算中,任何 x 的要素表达式的计算结果都临时保存起来,然后在执行对 x 的赋值时重用这些结果。例如,在计算赋值 A()[B()] += C() 时(其中 A 为返回 int[] 的方法,B 和 C 为返回 int 的方法),按 A、B、C 的顺序只调用这些方法一次。
当复合赋值的左操作数为属性访问或索引器访问时,属性或索引器必须同时具有 get 访问器和 set 访问器。如果不是这样,则发生编译时错误。
上面的第二条规则允许在某些上下文中将 x op= y 按 x = (T)(x op y) 计算。按此规则,当左操作数为 sbyte、byte、short、ushort 或 char 类型时,预定义的运算符可用来构造复合运算符。甚至当两个参数都为这些类型之一时,预定义的运算符也产生 int 类型的结果,详见第 7.2.6.2 节中的介绍。因此,不进行强制转换,就不可能把结果赋值给左操作数。
此规则对预定义运算符的直观效果只是:如果同时允许 x op y 和 x = y,则允许 x op= y。在下面的示例中
byte b = 0;
char ch = '\0';
int i = 0;
b += 1; // Ok
b += 1000; // Error, b = 1000 not permitted
b += i; // Error, b = i not permitted
b += (byte)i; // Ok
ch += 1; // Error, ch = 1 not permitted
ch += (char)1; // Ok
每个错误的直观理由是对应的简单赋值也发生错误。
1.13.3 事件赋值
如果 += 或 -= 运算符的左操作数属于事件访问类别,则表达式按下面这样计算:
· 计算事件访问的实例表达式(如果有)。
· 计算 += 或 -= 运算符的右操作数,如果需要,通过隐式转换(第 6.1 节)转换为左操作数的类型。
· 调用该事件的事件访问器,所需的参数列表由右操作数(经过计算和必要的转换后)组成。如果运算符为 +=,则调用 add 访问器;如果运算符为 -=,则调用 remove 访问器。
事件赋值表达式不产生值。因此,事件赋值表达式只在 statement-expression(第 8.6 节)的上下文中是有效的。
1.14 表达式
expression 可以是 conditional-expression 或 assignment。
expression:
conditional-expression
assignment
1.15 常量表达式
constant-expression 是在编译时可以完全计算出结果的表达式。
constant-expression:
expression
常量表达式的类型可以为下列之一:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、任何枚举类型或空类型。常量表达式中允许下列构造:
· 文本(包括 null)。
· 对类和结构类型的 const 成员的引用。
· 对枚举类型的成员的引用。
· 带括号的子表达式,其自身是常量表达式。
· 强制转换表达式(前提是目标类型为以上列出的类型之一)。
· 预定义的一元运算符 +、–、! 和 ~。
· 预定义的二元运算符 +、–、*、 /、%、<<、>>、&、|、^、&&、||、==、!=、<、>、<= 和 >=(前提是每个操作数都为上面列出的类型)。
· 条件运算符 ?:。
常量表达式中允许下列转换:
· 标识转换
· 数值转换
· 枚举转换
· 常量表达式转换
· 隐式和显式引用转换,条件是转换的源是计算结果为 Null 值的常量表达式。
在常量表达式中,不进行其他转换,包括非 Null 值的装箱、拆箱和隐式引用转换。例如:
class C {
const object i = 5; // error: boxing conversion not permitted
const object str = “hello”; // error: implicit reference conversion
}
因为需要装箱转换,i 的初始化出错。因为需要对非 Null 值的隐式引用转换,str 的初始化出错。
只要表达式属于上面列出的类型之一且只包含上面列出的构造,就在编译时计算该表达式。即使该表达式是另一个包含有非常量构造的较大表达式的子表达式,亦是如此。
常量表达式的编译时计算使用与非常量表达式的运行时计算相同的规则,区别仅在于:当出现错误时,运行时计算引发异常,而编译时计算导致发生编译时错误。
除非常量表达式被显式放置在 unchecked 上下文中,否则在表达式的编译时计算期间,整型算术运算和转换中发生的溢出总是导致编译时错误(第 7.5.12 节)。
常量表达式出现在以下列出的上下文中。在这些上下文中,如果无法在编译时充分计算表达式,则发生编译时错误。
· 常量声明(第 10.3 节)。
· 枚举成员声明(第 14.3 节)。
· switch 语句的 case 标签(第 8.7.2 节)。
· goto case 语句(第 8.9.3 节)。
· 包含初始值设定项的数组创建表达式(第 7.5.10.2 节)中的维度长度。
· 属性(第 17 节)。
只要常量表达式的值在目标类型的范围内,隐式常量表达式转换(第 6.1.6 节)就允许将 int 类型的常量表达式转换为 sbyte、byte、short、ushort、uint 或 ulong。
1.16 布尔表达式
boolean-expression 是产生 bool 类型结果的表达式。
boolean-expression:
expression
if-statement(第 8.7.1 节)、while-statement(第 8.8.1 节)、do-statement(第 8.8.2 节)或 for-statement(第 8.8.3 节)的控制条件表达式都是 boolean-expression。?: 运算符(第 7.12 节)的控制条件表达式遵守与 boolean-expression 相同的规则,但由于运算符优先级的缘故,被归为 conditional-or-expression。
要求 boolean-expression 的类型或者可隐式地转换为 bool 的类型,或者实现了 operator true 的类型。如果两个要求都不满足,则发生编译时错误。
当布尔表达式的类型不能隐式转换为 bool 但它的确实现了 operator true 时,则在完成表达式计算后,会调用该类型提供的 operator true 以产生 bool 值。
第 11.4.2 节中的 DBBool 结构类型提供了一个实现了 operator true 和 operator false 的类型的示例。