C# 语言规范_版本5.0 (第7章 表达式)
1. 表达式
表达式是一个运算符和操作数的序列。本章定义语法、操作数和运算符的计算顺序以及表达式的含义。
1.1 表达式的分类
一个表达式可归类为下列类别之一:
- 值。每个值都有关联的类型。
- 变量。每个变量都有关联的类型,称为该变量的已声明类型。
- 命名空间。归为此类的表达式只能出现在 member-access(第 7.6.4 节)的左侧。在任何其他上下文中,归类为命名空间的表达式将导致编译时错误。
- 类型。归为此类的表达式只能出现在 member-access(第 7.6.4 节)的左侧,或作为 as 运算符(第 7.10.11 节)、is 运算符(第 REF7.10.10节)或 typeof 运算符(第 7.6.11 节)的操作数。在任何其他上下文中,归类为类型的表达式将导致编译时错误。
- 方法组。它是一组重载方法,是成员查找(第 7.4 节)的结果。方法组可能具有关联的实例表达式和关联的类型实参列表。当调用实例方法时,实例表达式的计算结果成为由 this(第 7.6.7 节)表示的实例。在 invocation-expression(第 7.6.5 节)和 delegate-creation-expression(第 7.6.10.5 节)中允许使用方法组,且这两种表达式的左边均为运算符,可以隐式转换为兼容的委托类型(第 6.6 节)。在任何其他上下文中,归类为方法组的表达式将导致编译时错误。
- null 文本。归类为 null 文本的表达式可以隐式转换为引用类型或可以为 null 的类型。
- 匿名函数。归类为匿名函数的表达式可以隐式转换为兼容的委托类型或表达式目录树类型。
- 属性访问。每个属性访问都有关联的类型,即该属性的类型。此外,属性访问可以有关联的实例表达式。当调用实例属性访问的访问器(get 或 set 块)时,实例表达式的计算结果将成为由 this(第 7.6.7 节)表示的实例。
- 事件访问。每个事件访问都有关联的类型,即该事件的类型。此外,事件访问还可以有关联的实例表达式。事件访问可作为 += 和 -= 运算符(第 7.17.3 节)的左操作数出现。在任何其他上下文中,归类为事件访问的表达式将导致编译时错误。
- 索引器访问。每个索引器访问都有关联的类型,即该索引器的元素类型。此外,索引器访问还可以有关联的实例表达式和关联的参数列表。当调用索引器访问的访问器(get 或 set 块)时,实例表达式的计算结果将成为由 this(第 7.6.7 节)表示的实例,而实参列表的计算结果将成为调用的形参列表。
- Nothing。这出现在当表达式是调用一个具有 void 返回类型的方法时。归类为 Nothing 的表达式仅在 statement-expression(第 8.6 节)的上下文中有效。
表达式的最终结果绝不会是一个命名空间、类型、方法组或事件访问。恰如以上所述,这些类别的表达式是只能在特定上下文中使用的中间构造。
通过执行对 get-accessor 或 set-accessor 的调用,属性访问或索引器访问总是被重新归类为值。该特殊访问器由属性或索引器访问的上下文确定:如果访问是赋值的目标,则通过调用 set-accessor 来赋新值(第 7.17.1 节)。否则,通过调用 get-accessor 来获取当前值(第 7.1.1 节)。
1.1.1 表达式的值
大多数含有表达式的构造最后都要求表达式表示一个值 (value)。在此情况下,如果实际的表达式表示命名空间、类型、方法组或 Nothing,则将发生编译时错误。但是,如果表达式表示属性访问、索引器访问或变量,则将它们隐式替换为相应的属性、索引器或变量的值:
- 变量的值只是当前存储在该变量所标识的存储位置的值。必须先将变量视为已明确赋值(第 5.3 节)才可以获取其值,否则将出现编译时错误。
- 通过调用属性的 get-accessor 可获取属性访问表达式的值。如果属性没有 get-accessor,则会出现编译时错误。否则将执行函数成员调用(第 7.5.4 节),然后调用结果将成为属性访问表达式的值。
- 通过调用索引器的 get-accessor 可获取索引器访问表达式的值。如果索引器没有 get-accessor,则会出现编译时错误。否则,将使用与索引器访问表达式关联的参数列表来执行函数成员调用(第 7.5.4 节)然后调用结果将成为索引器访问表达式的值。
1.2 静态和动态绑定
根据构成表达式(参数、操作数、接收器)的类型或值确定操作含义的过程通常称为绑定。例如,方法调用的含义是根据接收器和参数的类型确定的。运算符的含义是根据其操作数的类型确定的。
在 C# 中,操作的含义通常在编译时根据其构成表达式的编译时类型确定。同样,如果表达式包含错误,编译器将检测并报告该错误。此方法称为静态绑定。
但是,如果表达式为动态表达式(即类型为 dynamic),则这指示它所参与的任何绑定都应基于其运行时类型(即它在运行时所表示的对象的实际类型),而不是它在编译时的类型。因此,此类操作的绑定推迟到要在程序运行过程中执行此操作的时间。这称为动态绑定 (dynamic binding)。
当操作是动态绑定时,编译器只执行很少检查或根本不执行检查。而当运行时绑定失败时,错误将在运行时报告为异常。
C# 中的以下操作会进行绑定:
- 成员访问:e.M
- 调用方法:e.M(e1,…,en)
- 委托调用: e(e1,…,en)
- 元素访问:e[e1,…,en]
- 对象创建:new C(e1,…,en)
- 重载的一元运算符:+、-、!、~、++、--、true、false
- 重载的二元运算符:+、-、*、/、%、&、&&、|、||、??、^、<<、>>、==、!=、>、<、>=、<=
- 赋值运算符:=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=
- 隐式转换和显式转换
不涉及动态表达式时,C# 默认为静态绑定,这表示在选择过程中使用构成表达式的编译时类型。但是,当上面列出的操作中的构成表达式之一为动态表达式时,操作会改为动态绑定。
1.2.1 绑定时间
静态绑定在编译时进行,而动态绑定在运行时进行。在以下各节中,术语绑定时间 (binding-time) 指编译时或运行时,具体取决于进行绑定的时间。
下面的示例演示静态绑定和动态绑定的表示法以及绑定时间的表示法:
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 的重载是基于其参数的运行时类型选择的。出现这种情况是因为参数为动态表达式(其编译时类型为 dynamic)。因此,第三个调用的绑定时间为运行时。
1.2.2 动态绑定
动态绑定的用途是允许 C# 程序与动态对象(dynamic object,即不遵循 C# 类型系统的一般规则的对象)进行交互。动态对象可以是来自具有不同类型系统的其他编程语言的对象,也可以是以编程方式设置为针对不同操作实现其自己的绑定语义的对象。
动态对象用于实现其自己语义的机制由实现定义。动态对象实现给定接口(再次定义的实现),以便向 C# 运行时发送信号,指示这些对象具有特殊语义。因此,只要对动态对象的操作是动态绑定的,就将采用其自己的绑定语义,而不是本文档中指定的 C# 绑定语义。
尽管动态绑定的用途是允许与动态对象进行互操作,然而 C# 允许对所有对象(无论是否为动态对象)进行动态绑定。这允许更加顺畅地集成动态对象,因为虽然对这些对象进行的操作的结果本身可能不是动态对象,但仍是程序员在编译时未知的类型。即使所涉及的对象都不是动态对象,动态绑定也有助于消除易于出错的基于反射的代码。
以下各节对于语言中的每种构造,介绍了应用动态绑定的确切时间、应用何种编译时检查(如果有)以及编译时结果和表达式分类是什么。
1.2.3 构成表达式的类型
当操作静态绑定时,构成表达式的类型(例如,接收器、实参、索引或操作数)通常视为该表达式的编译时类型。
当操作动态绑定时,构成表达式的类型由不同的方式确定,具体取决于构成表达式的编译时类型:
- 编译时类型为 dynamic 的构成表达式视为具有该表达式在运行时计算的实际值的类型
- 编译时类型为类型形参的构成表达式视为有类型形参在运行时绑定到的类型
- 否则,构成表达式视为具有其编译时类型
1.3 运算符
表达式由操作数 (operand) 和运算符 (operator) 构成。表达式的运算符指示对操作数适用什么样的运算。运算符的示例包括+、-、*、/ 和 new。操作数的示例包括文本、字段、局部变量和表达式。
有三类运算符:
- 一元运算符。一元运算符带一个操作数并使用前缀表示法(如 –x)或后缀表示法(如 x++)。
- 二元运算符。二元运算符带两个操作数并且全都使用中缀表示法(如 x + y)。
- 三元运算符。只存在一个三元运算符 ?:,它带三个操作数并使用中缀表示法 (c? x: y)。
表达式中运算符的计算顺序由运算符的优先级 (precedence) 和关联性 (associativity)(第 7.3.1 节)决定。
表达式中的操作数从左到右进行计算。例如,在 F(i) + G(i++) * H(i) 中,F 方法是使用 i 的旧值调用的,然后 G 方法也是使用 i 的旧值进行调用,最后 H 方法使用 i 的新值调用。这与运算符的优先级无关。
某些运算符可以重载 (overloaded)。运算符重载允许指定用户定义的运算符实现来执行某些运算,这些运算的操作数中至少有一个,甚至两个都属于用户定义的类或结构类型(第 7.3.2 节)。
1.3.1 运算符的优先级和顺序关联性
当表达式包含多个运算符时,运算符的优先级 (precedence) 控制各运算符的计算顺序。例如,表达式 x + y * z 按 x + (y * z) 计算,因为 * 运算符的优先级高于二元 + 运算符。运算符的优先级由运算符的关联语法产生式的定义确定。例如,additive-expression 由以 + 或 + 或 - 运算符分隔的 multiplicative-expression 序列组成,因而 + 和 - 运算符的优先级比 *、/ 和 % 运算符要低。
下表按照从最高到最低的优先级顺序概括了所有的运算符:
章节 |
类别 |
运算符 |
7.6 |
基本 |
x.y f(x) a[x] x++ x-- new typeof default checked unchecked delegate |
7.7 |
一元 |
+ - ! ~ ++x --x (T)x await x |
7.8 |
乘法 |
* / % |
7.8 |
加减 |
+ - |
7.9 |
移位 |
<< >> |
7.10 |
关系和类型检测 |
< > <= >= is as |
7.10 |
相等 |
== != |
7.11 |
逻辑 AND |
& |
7.11 |
逻辑 XOR |
^ |
7.11 |
逻辑 OR |
| |
7.12 |
条件 AND |
&& |
7.12 |
条件 OR |
|| |
7.13 |
null 合并 |
?? |
7.14 |
条件 |
?: |
7.17, 7.15 |
赋值和 lambda 表达式 |
= *= /= %= += -= <<= >>= &= ^= |= => |
当操作数出现在具有相同优先级的两个运算符之间时,运算符的顺序关联性控制运算的执行顺序:
- 除赋值运算符和 null 合并运算符外,所有二元运算符均为左结合,表示从左向右执行运算。例如,x + y + z 可以按 (x + y) + z进行计算。
- 赋值运算符、null 合并运算符和条件运算符 (?:) 为右结合,表示从右向左执行运算。例如,x = y = z 可以按 x = (y = z)进行计算。
优先级和顺序关联性都可以用括号控制。例如,x + y * z 先将 y 乘以 z,然后将结果与 x 相加,而 (x + y) * z 先将 x 与 y 相加,然后再将结果乘以 z。
1.3.2 运算符重载
所有一元和二元运算符都具有可自动用于任何表达式的预定义实现。除了预定义实现外,还可通过在类或结构(第 10.10 节)中包括 operator 声明来引入用户定义的实现。用户定义的运算符实现的优先级始终高于预定义运算符实现的优先级:仅当不存在适用的用户定义运算符实现时才考虑预定义的运算符实现,如第 7.3.3 节和第 7.3.4 节中所述。
可重载的一元运算符 (overloadable unary operator) 有:
+ - ! ~ ++ -- true false
虽然不在表达式中显式使用 true 和 false(因而未包括在第 7.3.1 节的优先级表中),但仍将它们视为运算符,原因是它们在多种表达式上下文中被调用:布尔表达式(第 7.20 节)以及涉及条件(第 7.14 节)运算符和条件逻辑运算符(第 7.12 节)的表达式。
可重载的二元运算符 (overloadable binary operator) 有:
+ - * / % & | ^ << >> == != > < >= <=
只有以上所列的运算符可以重载。具体而言,不可能重载成员访问、方法调用或 =、&&、||、??、?:、=>、checked、unchecked、new、typeof、default、as 和 s 运算符。
当重载一个二元运算符时,也会隐式重载相应的赋值运算符(若有)。例如,运算符 * 的重载也是运算符 *= 的重载。第 7.17.2 节对此有进一步描述。请注意,赋值运算符本身 (=) 不能重载。赋值总是简单地将值按位复制到变量中。
强制转换运算(如 (T)x)通过提供用户定义的转换(第 6.4 节)来重载。
元素访问(如 a[x])不被视为可重载的运算符。但是,可通过索引器(第 10.9 节)支持用户定义的索引。
在表达式中,使用运算符表示法来引用运算符,而在声明中,使用函数表示法来引用运算符。下表显示了一元运算符和二元运算符的运算符表示法和函数表示法之间的关系。在第一项中,op 表示任何可重载的一元前缀运算符。在第二项中,op 表示 ++ 和 -- 一元后缀运算符。在第三项中,op 表示任何可重载的二元运算符。
运算符表示法 |
函数表示法 |
op x |
operator op(x) |
x op |
operator op(x) |
x op y |
operator op(x, y) |
用户定义的运算符声明总是要求至少一个参数为包含运算符声明的类或结构类型。因此,用户定义的运算符不可能具有与预定义运算符相同的签名。
用户定义的运算符声明不能修改运算符的语法、优先级或顺序关联性。例如,/ 运算符始终为二元运算符,始终具有在第 7.3.1 节中指定的优先级,并且始终左结合。
虽然用户定义的运算符可以执行它想执行的任何计算,但是强烈建议不要采用产生的结果与直觉预期不同的实现。例如,operator == 的实现应比较两个操作数是否相等,然后返回一个适当的 bool 结果。
在从第 7.6 节到第 7.12 节的关于各运算符的说明中,运算符的预定义实现以及适用于各运算符的任何其他规则都有规定。在这些说明中使用了“一元运算符重载决策”(unary operator overload resolution)、“二元运算符重载决策”(binary operator overload resolution) 和“数值提升”(numeric promotion) 这样的术语,在后面的章节中可以找到它们的定义。
1.3.3 一元运算符重载决策
op x 或 x op 形式的运算(其中 op 是可重载一元运算符,x 是 X 类型的表达式)按如下方式处理:
- 对于由 X 为运算 operator op(x) 提供的候选的用户定义运算符集,应根据第 7.3.5 节中的规则来确定。
- 如果候选的用户定义运算符集不为空,则它就会成为运算的候选运算符集。否则,预定义一元 operator op 实现(包括它们的提升形式)将成为关于该运算的候选运算符集。关于给定运算符的预定义实现,在有关运算符的说明(第 7.6 节和第 7.7 节)中指定。
- 第 7.5.3 节中的重载决策规则应用于候选运算符集,以选择一个关于参数列表 (x) 的最佳运算符,此运算符将成为重载决策过程的结果。如果重载决策未能选出单个最佳运算符,则发生绑定时错误。
1.3.4 二元运算符重载决策
x op y 形式的运算(其中 op 是可重载的二元运算符,x 是 X 类型的表达式,y 是 Y 类型的表达式)按如下方式处理:
- 确定 X 和 Y 为运算 operator op(x, y) 提供的候选用户定义运算符集。该集包括由 X 提供的候选运算符和由 Y 提供的候选运算符的并集,每个候选运算符都使用第 7.3.5 节中的规则来确定。对于并集,将按如下方式合并候选项:
- 如果 X 和 Y 为同一类型,或者 X 和 Y 派生自一个公共基类型,则两者共有的候选运算符只在该并集中出现一次。
- 如果 X 和 Y 之间存在标识转换,Y 提供的运算符 opY 与 X 提供的 opX 具有相同的返回类型,并且 opY 的操作数类型具有到 opX 的对应操作数类型的标识转换,则该集中仅出现 opX。
- 如果候选的用户定义运算符集不为空,则它就会成为运算的候选运算符集。否则,预定义二元 operator op 实现(包括它们的提升形式)将成为关于该运算的候选运算符集。关于给定运算符的预定义实现,在有关运算符的说明(第 7.8 节到第 7.12 节)中指定。对于预定义的枚举和委托运算符而言,所考虑的唯一运算符是那些由枚举或委托类型(即其中一个操作数的绑定时间类型)定义的运算符。
- 第 7.5.3 节中的重载决策规则应用于候选运算符集,以选择一个关于参数列表 (x, y) 的最佳运算符,此运算符将成为重载决策过程的结果。如果重载决策未能选出单个最佳运算符,则发生绑定时错误。
1.3.5 候选用户定义运算符
给定一个 T 类型和运算 operator op(A),其中 op 是可重载的运算符,A 是参数列表,对 T 为 operator op(A) 提供的候选用户定义运算符集按如下方式确定:
- 确定类型 T0。如果 T 是可以为 null 的类型,则 T0 是其基础类型;否则 T0 等于 T。
- 对于 T0 中的所有 operator op 声明和此类运算符的提升形式,如果关于参数列表 A 至少有一个运算符是适用的(第 7.5.3.1 节),则候选运算符集将由 T0 中所有适用的此类运算符组成。
- 否则,如果 T0 为 object,则候选运算符集为空。
- 否则,T0 提供的候选运算符集为 T0 的直接基类提供的候选运算符集,或者为 T0 的有效基类(如果 T0 为类型参数)。
1.3.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.5.3 节)应用于此运算符集时,这些运算符中第一个能满足下述条件的运算符将被选中:存在从操作数类型的隐式转换。例如,对于运算 b * s(其中 b 为 byte,s 为 short),重载决策将选择 operator *(int, int) 作为最佳运算符。因此,效果是 b 和 s 转换为 int,并且结果的类型为 int。同样,对于运算 i * d(其中 i 为 int,d 为 double),重载决策将选择 operator *(double, double) 作为最佳运算符。
1.3.6.1 一元数值提升
一元数值提升是针对预定义的 +、–和 ~ 一元运算符的操作数发生的。一元数值提升仅包括将 sbyte、byte、short、ushort 或 char 类型的操作数转换为 int 类型。此外,对于 – 一元运算符,一元数值提升将 uint 类型的操作数转换为 long 类型。
1.3.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.7 提升运算符
提升运算符 (lifted operator) 允许操作不可以为 null 的值类型的预定义运算符及用户定义运算符,亦可用于这些类型的可以为 null 的形式。提升运算符是根据符合某些要求的预定义和用户定义运算符构造而成的,如下所述:
- 对于一元运算符
+ ++ - -- ! ~
如果操作数和结果类型都为不可以为 null 的值类型,则存在运算符的提升形式。该提升形式是通过将一个 ? 修饰符添加到操作数和结果类型构造而成的。如果操作数为 null,则提升运算符产生一个 null 值。否则,提升运算符对该操作数进行解包,应用基础运算符,并包装结果。
- 对于二元运算符
+ - * / % & | ^ << >>
如果操作数和结果类型都为不可以为 null 的值类型,则存在运算符的提升形式。该提升形式是通过将一个 ? 修饰符添加到每个操作数和结果类型构造的。如果一个操作数为 null 或两个操作数皆为 null,则提升运算符产生一个 null 值(bool? 类型的 & 和 | 运算符除外,如第 7.11.3 节所述)。否则,提升运算符对这些操作数进行解包,应用基础运算符,并包装结果。
- 对于相等运算符
== !=
如果两个操作数类型都为不可以为 null 的值类型,并且结果类型为 bool,则存在运算符的提升形式。该提升形式是通过将一个 ? 修饰符添加到每个操作数类型构造的。该提升运算符认为两个 null 值相等,null 值不等于任何非 null 值。如果两个操作数都为非 null,则提升运算符对这两个操作数进行解包,并应用基础运算符以产生 bool 结果。
- 对于关系运算符
< > <= >=
如果两个操作数类型都为不可以为 null 的值类型,并且结果类型为 bool,则存在运算符的提升形式。该提升形式是通过将一个 ? 修饰符添加到每个操作数类型构造的。如果一个操作数为 null 或两个操作数都为 null,则提升运算符产生 false 值。否则,提升运算符对这些操作数进行解包,并应用基础运算符以产生 bool 结果。
1.4 成员查找
成员查找成员查找是确定类型上下文中的名称含义的过程。成员查找可以作为表达式中计算 simple-name(第 7.6.2 节)或 member-access(第 7.6.4 节)的过程的一部分进行。如果 simple-name 或 member-access 以 invocation-expression(第 7.6.5.1 节)的 primary-expression 形式出现,则称调用该成员。
如果成员是方法或事件,或者如果成员是委托类型(第 15 章)或 dynamic 类型(第 4.7 节)的常量、字段或属性,则称该成员是可以调用的。
成员查找不仅考虑成员的名称,而且考虑该成员具有的类型形参的数目以及该成员是否可访问。对成员查找来说,泛型方法和嵌套泛型类型具有的类型形参数目就是在它们各自的声明中所指定的数目,其他所有成员则具有零个类型形参。
类型 T 中的具有 K 个类型形参的名称 N 的成员查找过程如下:
- 首先确定名为 N 的可访问的成员的集:
- 如果 T 是类型形参,则该集是被指定为 T 的主要约束或次要约束(第 10.1.5 节)的每个类型中名为 N 的可访问成员集与 object 中名为 N 的可访问成员集的并集。
- 否则,该集由 T 中所有名为 N 的可访问(第3.5 节)成员(包括继承的成员)和 object 中名为 N 的可访问成员构成。如果 T 为构造类型,则按第 10.3.2 节中所述通过替换类型实参来获取成员集。包含 override 修饰符的成员不包括在此集中。
- 下一步,如果 K 为零,则移除声明中包含类型形参的所有嵌套类型。如果 K 不为零,则移除所有具有不同数目的类型形参的成员。注意,当 K 为零时,将不会移除具有类型形参的方法,因为类型推断过程(第 7.5.2 节)也许能够推断出类型实参。
- 接着,如果调用该成员,则从该集中移除所有不可调用的成员。
- 然后,从该集中移除被其他成员隐藏的成员。对于该集中的每个成员 S.M(其中 S 是声明了成员 M 的类型),应用下面的规则:
- 如果 M 是一个常量、字段、属性、事件或枚举成员,则从该集中移除在 S 的基类型中声明的所有成员。
- 如果 M 是一个类型声明,则从该集中移除在 S 的基类型中声明的所有非类型,并从该集中移除与在 S 的基类型中声明的 M具有相同数目的类型形参的所有类型声明。
- 如果 M 是方法,则从该集移除在 S 的基类型中声明的所有非方法成员。
- 然后,从该集中移除被类成员隐藏的接口成员。仅当 T 为类型形参,并且 T 同时具有除 object 以外的有效基类和非空有效接口集(第 10.1.5 节)时,此步骤才会产生效果。对于该集中的每个成员 S.M(其中 S 是声明了成员 M 的类型),如果 S 是除 object 以外的类声明,则应用下面的规则:
- 如果 M 是一个常量、字段、属性、事件、枚举成员或类型声明,则从该集中移除在接口声明中声明的所有成员。
- 如果 M 是一个方法,则从该集中移除在接口声明中声明的所有非方法成员,并从该集中移除与在接口声明中声明的 M 具有相同签名的所有方法。
- 最后,移除了隐藏成员后,按下述规则确定查找结果:
- 如果该集由单个非方法成员组成,则此成员即为查找的结果。
- 否则,如果该集只包含方法,则这组方法为查找的结果。
- 否则,该查找是不明确的,将会发生绑定时错误。
对于非类型形参和接口的类型中的成员查找,以及严格单一继承的接口(继承链中的每个接口都只有零个或一个直接基接口)中的成员查找,这些查找规则的效果就相当于派生成员隐藏具有相同名称或签名的基成员。这种单一继承查找决不会产生多义性。有关多重继承接口中的成员查找可能引起的多义性的介绍详见第 13.2.5 节。
1.4.1 基类型
出于成员查找的目的,类型 T 被视为具有下列基类型:
- 如果 T 为 object 或 dynamic,则 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.5 函数成员
函数成员是包含可执行语句的成员。函数成员总是类型的成员,不能是命名空间的成员。C# 定义了以下类别的函数成员:
- 方法
- 属性
- 事件
- 索引器
- 用户定义运算符
- 实例构造函数
- 静态构造函数
- 析构函数
除了析构函数和静态构造函数(它们不能被显式调用),函数成员中包含的语句通过函数成员调用执行。编写函数成员调用的实际语法取决于具体的函数成员类别。
函数成员调用中所带的实参列表(第 7.5.1 节)为函数成员的形参提供实际值或变量引用。
泛型方法的调用可能会使用类型推断确定要传递到方法的类型实参集。有关此过程的介绍详见第 7.5.2 节。
调用方法、索引器、运算符和实例构造函数时,使用重载决策来确定要调用的候选函数成员集。有关此过程的介绍详见第 7.5.3 节。
在绑定时(可能通过重载决策)确定了具体的函数成员后,有关运行时调用函数成员的实际过程的介绍详见第 7.5.4 节。
下表概述了在涉及六个可被显式调用的函数成员类别的构造中发生的处理过程。在下表中,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.5.1 实参列表
每个函数成员和委托调用均包括一个实参列表,其中列出函数成员形参的实际值或变量引用。用于指定函数成员调用的实参列表的语法取决于函数成员类别:
- 对于实例构造函数、方法、索引器和委托,将实参指定为 argument-list,如下所述。对于索引器,当调用 set 访问器时,实参列表还需附加上一个表达式,该表达式被指定为赋值运算符的右操作数。
- 对于属性,当调用 get 访问器时,实参列表是空的;而当调用 set 访问器时,实参列表由指定为赋值运算符的右操作数的表达式组成。
- 对于事件,实参列表由指定为 += 或 -= 运算符的右操作数的表达式组成。
- 对于用户定义的运算符,实参列表由一元运算符的单个操作数或二元运算符的两个操作数组成。
对于属性(第 10.7 节)、事件(第 10.8 节)和用户定义运算符(第 10.10 节),其实参始终以值形参(第 10.6.1.1 节)的形式来传递。索引器(第 10.9 节)的实参始终以值形参(第 10.6.1.1 节)或形参数组(第 10.6.1.4 节)的形式来传递。这些函数成员类别不支持引用形参和输出形参。
实例构造函数、方法、索引器或委托调用的实参指定为 argument-list:
argument-list:
argument
argument-list ,
argument
argument:
argument-nameopt
argument-value
argument-name:
identifier :
argument-value:
expression
ref variable-reference
out variable-reference
argument-list 由一个或多个 argument 组成,各实参之间用逗号分隔。每个实参由一个可选的 argument-name 及后跟的 argument-value 构成。带有 argument-name 的 argument 称为命名实参 (named argument),而没有 argument-name 的 argument 称为位置实参 (positional argument)。在 argument-list 中,位置实参出现在命名实参后是错误的。
argument-value 可以采用下列形式之一:
- expression,指示将实参以值形参(第 10.6.1.1 节)的形式传递。
- 后跟 variable-reference(第 5.4节)的关键字 ref,指示将实参以引用形参(第 10.6.1.2 节)的形式来传递。变量在可以作为引用形参传递之前,必须先明确赋值(第 5.3 节)。
- 后跟 variable-reference(第 5.4 节)的关键字 out,指示将实参以输出形参(第 10.6.1.3 节)的形式来传递。在将变量作为输出形参传递的函数成员调用之后,可认为该变量已明确赋值(第 5.3 节)。
形式分别确定实参 的形参传递模式:值、引用或输出。
1.5.1.1 对应形参
对于实参列表中的每个实参,在所调用的函数成员或委托中必须存在对应形参。
后面使用的形参列表按以下方式确定:
- 对于类中定义的虚方法和索引器,形参列表从函数成员的最具体声明或重写中选取,方法是从接收器的静态类型开始,在其基类中进行搜索。
- 对于接口方法和索引器,形参列表从成员的最具体定义选取,方法是从接口类型开始,在基接口中进行搜索。如果未找到唯一的形参列表,则构造一个具有不可访问名称且没有可选形参的形参列表,从而使调用不能使用命名形参或省略可选实参。
- 对于分部方法,将使用定义分部方法声明的形参列表。
- 对于所有其他函数成员和委托,只有一个形参列表(即使用的形参列表)。
某个实参或形参的位置定义为实参列表或形参列表中,位于该实参或形参之前的实参或形参的数量。
函数成员实参的对应形参按以下方式建立:
- 实例构造函数、方法、索引器和委托的 argument-list 中的实参:
- 位置实参对应于出现在形参列表中相同位置上的固定形参。
- 如果函数成员的某个位置实参使用正常形式调用了某个形参数组,则该位置实参对应于该参数数组(必须出现在形参列表中的相同位置上)。
- 如果函数成员的某个位置实参使用展开形式调用了某个形参数组,并且形参列表中的相同位置上没有出现任何固定形参,则该位置实参对应于该形参数组中的某个元素。
- 命名实参对应于形参列表中具有相同名称的形参。
- 对于索引器,当调用 set 访问器时,作为赋值运算符的右操作数指定的表达式对应于 set 访问器声明的隐式 value 形参。
- 对于属性,在调用 get 访问器时没有实参。当调用 set 访问器时,作为赋值运算符的右操作数指定的表达式对应于 set 访问器声明的隐式 value 形参。
- 对于用户定义的一元运算符(包括转换),单个操作数对应于运算符声明的单个形参。
- 对于用户定义的二元运算符,左操作数对应于运算符声明的第一个形参,右操作数对应于第二个形参。
1.5.1.2 实参列表的运行时计算
在函数成员调用(第 7.5.4 节)的运行时处理期间,将按顺序从左到右计算实参列表的表达式或变量引用,具体规则如下:
- 对于值形参,计算实参表达式并执行到相应的形参类型的隐式转换(第 6.1 节)。结果值在函数成员调用中成为该值形参的初始值。
- 对于引用形参或输出形参,计算对应的变量引用,所得的存储位置在函数成员调用中成为该形参表示的存储位置。如果作为引用形参或输出形参给定的变量引用是一个 reference-type 的数组元素,则执行运行时检查以确保该数组的元素类型与形参类型相同。如果此检查失败,将引发 System.ArrayTypeMismatchException。
方法、索引器和实例构造函数可以将其最右边的形参声明为形参数组(第 10.6.1.4 节)。调用此类函数成员可采取标准形式或展开形式两种形式中适用的形式(第 7.5.3.1 节):
- 当具有形参数组的函数成员以其正常形式调用时,为该形参数组给定的实参必须是一个可隐式转换(第 6.1 节)为形参数组类型的表达式。在此情况下,形参数组的作用与值形参完全一样。
- 当具有形参数组的函数成员以其展开形式调用时,调用必须为形参数组指定零个或多个位置实参,其中每个实参都是一个可隐式转换(第 6.1 节)为该形参数组的元素类型的表达式。在此情况下,调用会创建一个该形参数组类型的实例,其所含的元素个数等于给定的实参个数,再用给定的实参值初始化此数组实例的每个元素,然后将新创建的数组实例用作实参。
实参列表的表达式始终按其书写的顺序进行计算。因此,示例
class Test
{
static void F(int x, int y = -1, int z =
-2) {
System.Console.WriteLine("x =
{0}, y = {1}, z = {2}", x, y, 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
如果存在从 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.6.10.4 节)的数组创建表达式。例如,给定下面的声明
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.5.2 类型推断
不指定类型实参而调用泛型方法时,类型推断 (type inference) 过程将尝试为调用推断类型实参。类型推断的存在允许使用更方便的语法调用泛型方法,并使得程序员不必指定多余的类型信息。例如,给定下面的方法声明:
class Chooser
{
static Random rand = new Random();
public
static T Choose<T>(T first, T second) {
return (rand.Next(2) == 0)? first:
second;
}
}
可以在不显式指定类型实参的情况下调用 Choose 方法:
int i = Chooser.Choose(5, 213); // Calls Choose<int>
string s = Chooser.Choose("foo", "bar"); // Calls Choose<string>
借助于类型实参推断,可通过传递给方法的实参来确定类型实参 int 和 string。
类型推断在方法调用(第 7.6.5.1 节)的绑定时处理过程中进行,发生在调用的重载决策步骤之前。当在方法调用中指定了特定的方法组,并且没有在方法调用中指定类型实参时,将会对该方法组中的每个泛型方法应用类型推断。如果类型推断成功,则使用推断出的类型实参确定用于后续重载解析的实参的类型。如果重载决择选择一个泛型方法作为要调用的方法,则使用推断出的类型实参作为用于调用的实际类型实参。如果特定方法的类型推断失败,则该方法不参与重载决策。类型推断失败本身不会导致绑定时错误。但是,当重载决策未能找到任何适用的方法时,它通常会导致绑定时错误。
如果所提供的实参的数目与方法中的形参的数目不同,则推断立即失败。否则,假定泛型方法具有以下签名:
Tr M<X1…Xn>(T1 x1 … Tm xm)
对于 M(E1 …Em) 形式的方法调用,类型推断的任务是为每个类型形参 X1…Xn 找到唯一的类型实参 S1…Sn,以使 M<S1…Sn>(E1…Em) 调用有效。
在推断过程中,每个类型形参 Xi 或者固定到一个特定类型 Si 或者未固定,而具有一组关联的界限。每个界限都属于某个类型 T。最初,每个类型变量 Xi 均未固定,具有一组空的界限。
类型推断分阶段进行。每个阶段都将尝试基于上一阶段的发现为更多类型变量推断类型实参。第一阶段进行一些初始的界限推断,而第二阶段将类型变量固定到特定类型并推断其他界限。第二阶段可能需要重复多次。
注意:类型推断不仅仅在调用泛型方法时发生。方法组转换的类型推断详见第 7.5.2.13 节中的说明,查找一组表达式的最佳通用类型详见第 7.5.2.14 节中的说明。
1.5.2.1 第一阶段
对于每个方法实参 Ei:
- 如果 Ei 为匿名函数,则从 Ei 到 Ti 进行显式参数类型推断(第 7.5.2.7 节)
- 否则,如果 Ei 具有类型 U 且 xi 为值形参,则从 U 到 Ti 进行下限推断。
- 否则,如果 Ei 具有类型 U 且 xi 为 ref 或 out 形参,则将从 U 到 Ti 进行精确推断。
- 否则,将不对此实参进行推断。
1.5.2.2 第二阶段
第二阶段如下进行:
- 所有不依赖(第 7.5.2.5 节)任何Xj 的未固定类型变量 Xi 都将被固定(第 7.5.2.10 节)。
- 如果不存在这样的类型变量,则固定所有未固定的类型变量 Xi,为此应符合下述所有规则:
- 至少有一个依赖 Xi 的类型变量 Xj
- Xi 具有非空界限集
- 如果不存在此类类型变量,但仍有未固定的类型变量,则类型推断会失败。
- 否则,如果不存在其他任何未固定的类型变量,则类型推断将成功。
- 否则,对于具有相应形参类型 Ti (其中输出类型(第 7.5.2.4 节)包含非固定类型变量 Xj,但输入类型(第 7.5.2.3 节)不包含此变量)的所有实参 Ei,从 Ei 到 Ti 进行输出类型推断(第 7.5.2.6 节)。然后重复第二阶段。
1.5.2.3 输入类型
如果 E 为方法组或隐式类型化的匿名函数,并且 T 为委托类型或表达式目录树类型,则 T 的所有形参类型都是具有类型 T 的 E 的输入 类型。
1.5.2.4 输出类型
如果 E 是一个方法组或匿名函数并且 T 是委托类型或表达式目录树类型,则 T 的返回类型是类型为 T 的 E 的输出类型。
1.5.2.5 依赖
未固定的类型变量 Xi 在下述情形中直接依赖未固定的类型变量 Xj:对于类型为 Tk 的某个实参 Ek,Xj 出现在类型为 Tk 的 Ek 的输入类型中,而 Xi 则出现在类型为 Tk 的 Ek 的输出类型中。
Xj 在下述情形中依赖 Xi:Xj 直接依赖 Xi,或者 Xi 直接依赖 Xk,而 Xk 又依赖于 Xj。因而,“依赖”具有传递性,但不形成“直接依赖”的自反闭包。
1.5.2.6 输出类型推断
按照以下方法从表达式 E 到 类型 T 进行输出类型推断:
- 如果 E 为具有推断返回类型 U(第 7.5.2.12 节)的匿名函数,且 T 为具有返回类型 Tb 的委托类型或表达式目录树类型,则从 U 到 Tb 进行下限推断(第 7.5.2.9 节)。
- 否则,如果 E 为方法组,T 为具有参数类型 T1…Tk 和返回类型 Tb 的委托类型或表达式目录树类型,且具有类型 T1…Tk 的 E 的重载决策产生了具有返回类型 U 的单个方法,则从 U 到 Tb 进行下限推断。
- 否则,如果 E 为具有类型 U 的表达式,则从 U 到 T 进行下限推断。
- 否则,不进行任何推断。
1.5.2.7 参数类型显式推断
按照以下方法从表达式 E 到类型 T 进行显式参数类型推断:
- 如果 E 为具有参数类型 U1…Uk 的显式类型匿名函数,T 为具有参数类型 V1…Vk 的委托类型或表达式目录树类型,则对于每个 Ui,从 Ui 到对应的 Vi 进行准确推断(第 7.5.2.8 节)。
1.5.2.8 精确推断
按如下所述从类型 U 到类型 V 进行精确推断:
- 如果 V 是未固定的 Xi 之一,则将 U 添加到 Xi 的精确界限集中。
- 否则,通过检查是否存在以下任何一种情况来确定集合 V1…Vk 和 U1…Uk :
- V 是数组类型 V1[…] , U 是具有相同秩的数组类型 U1[…]
- V 是类型 V1?,U 是类型 U1?
- V 是构造类型 C<V1…Vk> and U 是构造类型 CC<U1…Uk>
如果存在以上任意情况,则从每个 Ui 到对应的 Vi 进行精确推断。
- 否则,不进行任何推断。
1.5.2.9 下限推断
按如下所述从类型 U 到类型 V 进行下限推断:
- 如果 V 是未固定的 Xi 之一,则将 U 添加到 Xi 的精确下限界限集中。
- 否则,如果 V 为 V1? 类型,而 U 为 U1? 类型,则从 U1 到 V1 进行下限推断。
- 否则,通过检查是否存在以下任意一种情况,确定 U1…Uk 和 V1…Vk 这两个集:
- V 是数组类型 V1[…],U 是具有相同秩的数组类型 U1[…]
- V 是 IEnumerable<V1>、ICollection<V1> 或 IList<V1> 之一,U 是一维数组类型 U1[]
- V 是构造类、结构、接口或委托类型 C<V1…Vk>,并且存在唯一类型 C<U1…Uk> ,使 U(或者,如果 U 是类型形参,则为其有效基类或其有效接口集的任意成员)等于、(直接或间接)继承自或者(直接或间接)实现 C<U1…Uk>。
(“唯一性”限制表示对于 interface C<T>{} class U: C<X>, C<Y>{},不进行从 U 到 C<T> 的推断,因为 U1 可以是 X 或 Y。)
如果存在以上任意情况,则从每个 Ui 到对应的 Vi 进行推断,如下所示:
- 如果不知道 Ui 为引用类型,则进行精确推断
- 否则,如果 U 为数组类型,则进行下限推断
- 否则,如果 V 为 C<V1…Vk>,则推断依赖于 C 的第 i 个类型参数:
- 如果该参数是协变的,则进行下限推断。
- 如果该参数是逆变的,则进行上限推断。
- 如果该参数是固定的,则进行精确推断。
- 否则,不进行任何推断。
1.5.2.10 上限推断
按如下所述从类型 U 到类型 V 进行上限推断:
- 如果 V 是未固定的 Xi 之一,则将 U 添加到 Xi 的精确上限界限集中。
- 否则,通过检查是否存在以下任何一种情况来确定集合 V1…Vk 和 U1…Uk:
- U 是数组类型 U1[…],V 是具有相同秩的数组类型 V1[…]
- U 是 IEnumerable<Ue>、ICollection<Ue> 或 IList<Ue> 之一,V 是一维数组类型 Ve[]
- U 是类型 U1?,V 是类型 V1?
- U 是构造类、结构、接口或委托类型 C<U1…Uk>,V 是等于、(直接或间接)继承自或者(直接或间接)实现唯一类型 C<V1…Vk> 的类、结构、接口或委托类型
(“唯一性”限制表示如果我们有 interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{},则不进行从 C<U1> 到 V<Q> 的推断。也不进行从 U1 到 X<Q> 或 Y<Q> 的推断。)
如果存在以上任意情况,则从每个 Ui 到对应的 Vi 进行推断,如下所示:
- 如果不知道 Ui 为引用类型,则进行精确推断
- 否则,如果 V 为数组类型,则进行上限推断
- 否则,如果 U 为 C<U1…Uk>,则推断依赖于 C 的第 i 个类型参数:
- 如果该参数是协变的,则进行上限推断。
- 如果该参数是逆变的,则进行下限推断。
- 如果该参数是固定的,则进行精确推断。
- 否则,不进行任何推断。
1.5.2.11 固定
具有界限集的未固定类型变量 Xi 按如下方式固定:
- 候选类型 Uj 的集以所有类型的集形式在 Xi 的界限集中开始。
- 然后我们依次检查 Xi 的每个界限:对于 Xi 的每个精确界限 U,将与 U 不同的所有类型 Uj 都从候选集中移除。对于 Xi 的每个下限 U,将不存在从 U 进行的隐式转换的所有类型 Uj 都从候选集中移除。对于 Xi 的每个上限 U,将不存在从其到 U 进行的隐式转换的所有类型 Uj 都从候选集中移除。
- 如果在其余的候选类型 Uj 中,存在唯一类型 V(该类型可从所有其他候选类型隐式转换而来),则将 Xi 固定到 V。
- 否则,类型推断将失败。
1.5.2.12 推断返回类型
匿名函数 F 的推断返回类型 (Inferred return type) 在类型推断和重载决策期间使用。匿名函数的推断返回类型仅能在所有参数类型均已知的情况下确定,因为参数类型是隐式给出的;是通过匿名函数转换提供的;或者是在封闭泛型方法调用上进行类型推断期间推断出的。
推断结果类型按如下方式确定:
- 如果 F 的主体是具有某个类型的 expression,则 F 的推断结果类型为该表达式的类型。
- 如果 F 的函数体是一个 block 并且该块的 return 语句中的表达式集具有最通用类型 T(第 7.5.2.14 节),则 F 的推断返回类型为 T。
- 否则,无法为 F 推断返回类型。
推断返回类型按如下方式确定:
- 如果 F 为异步且 F 的主体是归类为 Nothing 的表达式(第 7.1 节)或一个语句块(其中的 return 语句没有表达式),则推断返回类型为 System.Threading.Tasks.Task
- 如果 F 为异步,且具有推断结果类型 T,则推动返回类型为 System.Threading.Tasks.Task<T>。
- 如果 F 为非异步,且具有推断结果类型 T,则推断返回类型为 T。
- 否则,无法为 F 推断返回类型。
请考虑使用在 System.Linq.Enumerable 类中声明的 Select 扩展方法,作为涉及匿名函数的类型推断示例:
namespace System.Linq
{
public static class Enumerable
{
public static
IEnumerable<TResult> Select<TSource,TResult>(
this IEnumerable<TSource>
source,
Func<TSource,TResult>
selector)
{
foreach (TSource element in
source) yield return selector(element);
}
}
}
假定 System.Linq 命名空间是使用 using 子句导入的,并假定类 Customer 具有类型为 string 的 Name 属性,则 Select 方法可用于选择客户列表中的名称:
List<Customer> customers =
GetCustomerList();
IEnumerable<string> names = customers.Select(c => c.Name);
Select 的扩展方法调用(第 7.6.5.2 节)是通过重写对静态方法调用的调用而进行处理的:
IEnumerable<string> names = Enumerable.Select(customers, c => c.Name);
因为类型实参不是显式指定的,所以类型推断用于推断类型实参。首先,将 customers 实参关联到 source 形参,推断出 T 为 Customer。然后,使用上述匿名函数类型推断过程,为 c 指定类型 Customer,将表达式 c.Name 与 selector 形参的返回类型相关联,推断出 S 为 string。因而,此调用等效于
Sequence.Select<Customer,string>(customers, (Customer c) => c.Name)
并且结果的类型为 IEnumerable<string>。
下面的示例演示匿名函数类型推断如何允许类型信息在泛型方法调用的实参之间“流动”。给定方法:
static Z F<X,Y,Z>(X value,
Func<X,Y> f1, Func<Y,Z> f2) {
return f2(f1(value));
}
针对此调用的类型推断:
double seconds = F("1:15:30", s => TimeSpan.Parse(s), t => t.TotalSeconds);
过程如下:首先将实参"1:15:30"关联到 value 形参,推断出 X 为 string。然后,为第一个匿名函数的形参 s 指定推断类型 string,并将表达式 TimeSpan.Parse(s) 与 f1 的返回类型相关联,推断出 Y 为 System.TimeSpan。最后,为第二个匿名函数的形参 t 指定推断类型 System.TimeSpan,并将表达式 t.TotalSeconds 与 f2 的返回类型相关联,推断出 Z 为 double。因而,调用结果为 double 类型。
1.5.2.13 方法组转换的类型推断
与泛型方法的调用类似,当将包含泛型方法的方法组 M 转换为给定的委托类型 D (§(第 6.6)节)时也必须应用类型推断。给定一个方法
Tr M<X1…Xn>(T1 x1 … Tm xm)
和分配给委托类型 D 的方法组 M,则类型推断的任务是查找类型实参 S1…Sn,以使表达式:
M<S1…Sn>
与 D 兼容(第 15.1 节)。
与泛型方法调用的类型推断算法不同的是,在这种情形下,只有实参类型,而没有实参表达式。特别是没有匿名函数,因此不需要进行多阶段推断。
而认为所有 Xi 均未固定,并从 D 的每个实参类型 Uj 到 M 的对应形参类型 Tj 进行下限推断。如果没有为任何 Xi 找到界限,则类型推断将失败。否则,所有将 Xi 均固定到对应的 Si,它们是类型推断的结果。
1.5.2.14 查找一组表达式的最通用类型
在某些情形下,需要为一组表达式推断出通用类型。特别是,用这种方式找到隐式类型化数组的元素类型和具有 block 体的匿名函数的返回类型的情形。
从直观上看,给定一组表达式 E1…Em,此推断应等效于调用某个方法
Tr M<X>(X x1 … X xm)
其中 Ei 为实参。
更确切地说,推断从未固定的类型变量 X 开始。然后从每个 Ei 到 X 进行输出类型推断。最后固定 X,如果成功,结果类型 S 就是表达式的最佳通用结果类型。如果不存在此类 S,则表达式没有最佳通用类型。
1.5.3 重载决策
重载决策是一种绑定时机制,用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来实施调用。在 C# 内,重载决策在下列不同的上下文中选择一个应调用的函数成员:
- 调用在 invocation-expression(第 7.6.5.1 节)中命名的方法。
- 调用在 object-creation-expression(第 7.6.10.1 节)中命名的实例构造函数。
- 通过 element-access(第 7.6.6 节)调用索引器访问器。
- 调用表达式(第 7.3.3 节和第 7.3.4 节)中引用的预定义运算符或用户定义运算符。
这些上下文中的每一个都以自己的唯一方式定义候选函数成员集和实参列表,上面列出的章节对此进行了详细说明。例如,方法调用的候选集不包括标记为 override(第 7.4 节)的方法,而且如果派生类中的任何方法适用(第 7.6.5.1 节),则基类中的方法不是候选方法。
一旦确定了候选函数成员和实参列表,对最佳函数成员的选择在所有情况下都相同,都遵循下列规则:
- 如果给定了适用的候选函数成员集,则在其中选出最佳函数成员。如果该集只包含一个函数成员,则该函数成员为最佳函数成员。否则,最佳函数成员的选择依据是:各成员对给定的实参列表的匹配程度。比其他所有函数成员匹配程度都高的那个函数成员就是最佳函数成员,但有一个前提:必须使用第 7.5.3.2 节中的规则将每个函数成员与其他所有函数成员进行比较。如果不是正好有一个函数成员比所有其他函数成员都好,则函数成员调用不明确并发生绑定时错误。
下面几节定义有关术语适用的函数成员 (applicable function member) 和更好的函数成员 (better function member) 的准确含义。
1.5.3.1 适用函数成员
当满足以下所有条件时,就称函数成员对于实参列表 A 是一个适用的函数成员:
- A 中的每个实参对应于函数成员声明中的一个形参(如第 7.5.1.1 节中所述),最多一个实参对应于一个形参,并且没有对应实参的所有形参都是可选形参。
- 对于 A 中的每个实参,实参的形参传递模式(第 7.5.1 节)与对应形参(第 1.6.6.1 节)的形参传递模式相同,而且
- 对于值形参或形参数组,存在从实参表达式到对应形参的类型的隐式转换(第 6.1 节),或者
- 对于 ref 或 out 形参,实参表达式的类型与对应形参的类型相同。ref 或 out 形参毕竟只是传递的实参的别名。
对于包含参数数组的函数成员,如果按上述规则判定该函数成员是适用的,则称它以正常形式 (normal form) 适用。如果包含参数数组的函数成员以正常形式不适用,则该函数成员可能以展开形式 (expanded form) 适用:
- 构造展开形式的方法是:用形参数组的元素类型的零个或更多值参数替换函数成员声明中的形参数组,使实参列表 A 中的实参数目匹配总的形参数目。如果 A 中的实参比函数成员声明中的固定形参的数目少,则该函数成员的展开形式无法构造,因而可判定该函数成员不适用。
- 否则,如果对于 A 中的每个实参,它的实参传递模式与相应形参的形参传递模式相同,并且下列条件成立,则称该成员函数以展开形式适用:
- 对于固定值形参或展开操作所创建的值形参,存在从实参类型到对应的形参类型的隐式转换(第 6.1 节),或者
- 对于 ref 或 out 参数,实参的类型与对应形参的类型相同。
1.5.3.2 更好的函数成员
为确定更好的函数成员,构造一个精炼的实参列表 A,其中只包含实参表达式本身,采用其出现在原始实参列表中的顺序。
每个候选函数成员的形参列表按以下方式构造:
- 如果函数成员只以展开形式适用,则使用展开形式。
- 从形参列表中移除没有对应实参的可选形参。
- 对形参重新排序,从而使其出现在与实参列表中的对应实参相同的位置上。
给定一个带有实参表达式集 { E1, E2, ..., EN } 的实参列表 A 和带有形参类型 { P1, P2, ..., PN } 和 { Q1, Q2, ..., QN } 的两个适用的函数成员 MP 和 MQ,则在以下情形中,MP 将定义为比 MQ 更好的函数成员:
- 对于每个实参,从 EX 到 QX 的隐式转换不如从 EX 到 PX 的隐式转换好,并且
- 对于至少一个参数而言,从 EX 到 PX 的转换比从 EX 到 QX 的转换更好。
当执行此计算时,如果 MP 或 MQ 以展开形式适用,则 PX 或 QX 所代表的是展开形式的参数列表中的参数。
在形参类型序列 {P1, P2, …, PN} 和 {Q1, Q2, …, QN} 等效(即每个 Pi 都有到对应 Qi 的标识转换)的情况下,将应用以下附加规则来确定更好的函数成员。
- 如果 MP 是非泛型方法而 MQ 是泛型方法,则 MP 比 MQ 好。
- 否则,如果 MP 在正常形式下适用,MQ 有一个 params 数组并且仅在其展开形式下适用,则 MP 比 MQ 好。
- 否则,如果 MP 具有比 MQ 更多的已声明形参,则 MP 比 MQ 好。如果两个方法都有 params 数组,并且都仅在其展开形式下适用,就可能出现这种情况。
- 否则,如果 MP 的所有形参都有对应实参,而需要使用默认实参替换 MQ 中的至少一个可选形参,则 MP 比 MQ 好。
- 否则,如果 MP 具有比 MQ 更明确的形参类型,则 MP 比 MQ 好。假设 {R1, R2, …, RN} 和 {S1, S2, …, SN} 表示 MP 和 MQ 的未实例化和未展开的形参类型。如果对于每个形参,RX 都不比 SX 更不明确,并且至少对于一个形参,RX 比 SX 更明确,则 MP 的形参类型比 MQ 的形参类型更明确:
- 类型形参不如非类型形参明确。
- 递归地,如果某个构造类型至少有一个类型实参更明确,并且没有类型实参比另一个构造类型(两者具有相同数目的类型实参)中的对应类型实参更不明确,则某个构造类型比另一个构造类型更明确。
- 如果一个数组类型的元素类型比另一个数组类型的元素类型更明确,则第一个数组类型比第二个数组类型(具有相同的维数)更明确。
- 否则,如果一个成员是非提升运算符而另一个是提升运算符,则非提升运算符更佳。
- 否则,两个函数成员都不是更好的。
1.5.3.3 表达式的更佳转换
给定一个从表达式 E 转换到类型 T1 的隐式转换 C1,以及一个从表达式 E 转换到类型 T2 的隐式转换 C2,如果至少符合以下条件之一,则 C1 与 C2 相比是更佳转换 (better conversion):
- E 具有类型 S,并存在从 S 到 T1 的标识转换,但不存在从 S 到 T2 的标识转换
- E 不是匿名函数,并且与 T2 相比,T1 是更佳转换目标(第 7.5.3.5 节)
- E 是匿名函数,T1 是委托类型 D1 或表达式树类型 Expression<D1>,T2 是委托类型 D2 或表达式树类型 Expression<D2> 并符合以下条件之一:
- D1 与 D2 相比是更佳转换目标
- D1 和 D2 有相同的形参列表,并且符合以下条件之一:
- D1 具有返回类型 Y1,D2 具有返回类型 Y2,对于 E,在形参列表上下文中存在推断返回类型 X(第 7.5.2.12 节),并且从 X 到 Y1 的转换比从 X 到 Y2 的转换好
- E 为异步,D1 具有返回类型 Task<Y1>,D2 具有返回类型 Task<Y2>,对于 E,在形参列表上下文中存在推断返回类型 Task<X>(第 7.5.2.12 节),并且从 X 到 Y1 的转换比从 X 到 Y2 的转换好
- D1 具有返回类型 Y 且 D2 返回 void
1.5.3.4 类型的更佳转换
给定一个从类型 S 转换到类型 T1 的转换 C1 以及一个从类型 S 转换到类型 T2 的转换 C2,如果至少符合以下条件之一,则 C1 与 C2 相比是更佳转换 (better conversion):
- 存在从 S 到 T1 的标识转换,但不存在从 S 到 T2 的标识转换
- T1 与 T2 相比是更佳转换目标(第 §7.5.3.5 节)
1.5.3.5 更佳转换目标
给定两个不同类型 T1 和 T2,如果至少符合以下条件之一,则 T1 与 T2 相比是更佳转换目标:
- 存在从 T1 到 T2 的隐式转换,不存在从 T2 到 T1 的隐式转换
- T1 为有符号的整型,T2 为无符号的整型。具体包括:
- T1 为 sbyte 且 T2 为 byte、ushort、uint 或 ulong
- T1 为 short 且 T2 为 ushort、uint 或 ulong
- T1 为 int 且 T2 为 uint 或 ulong
- T1 为 long,T2 为 ulong。
1.5.3.6 泛型类中的重载
虽然声明的签名必须唯一,但是在替换类型实参时可能会导致出现完全相同的签名。在此类情形中,上述重载决策的附加规则将挑选最明确的成员。
下面的示例根据此规则演示有效和无效的重载:
interface I1<T> {...}
interface I2<T> {...}
class G1<U>
{
int F1(U u); // Overload resulotion for G<int>.F1
int F1(int i); // will pick non-generic
void
F2(I1<U> a); // Valid
overload
void F2(I2<U> a);
}
class G2<U,V>
{
void F3(U u, V v); // Valid, but overload resolution for
void F3(V v, U u); // G2<int,int>.F3 will fail
void
F4(U u, I1<V> v); // Valid, but
overload resolution for
void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail
void
F5(U u1, I1<V> v2); // Valid overload
void F5(V v1, U u2);
void
F6(ref U u); // valid overload
void F6(out V v);
}
1.5.4 动态重载决策的编译时检查
即使动态绑定操作的重载决策在运行时发生,有时在编译时便能够知道将从中选择重载的函数成员的列表:
- 对于针对类型或其静态类型不是 dynamic 的值的方法调用(第 7.6.5.1 节),编译时将知道方法组中的可访问方法集。
- 对于对象创建表达式(第 7.6.10.1 节),编译时将知道类型中的可访问构造函数集。
- 对于索引器访问(第 7.6.6.2 节),编译时将知道接收器中的可访问索引器集。
在以上情况中,将对已知函数成员集中的每个成员执行有限的编译时检查,以了解成员是否已知在运行时绝不会调用。对于每个函数成员 F,将构成已修改形参和实参的列表:
- 首先,如果 F 是泛型方法并且提供了类型实参,则将用形参列表中的类型形参替换这些实参。但是,如果未提供类型实参,则不会发生此类替换。
- 然后,将省略其类型是开放式(即,包含类型形参,请参见第 4.4.2 节)的任何形参及其对应的形参。
若要 F 通过检查,必须满足下列所有条件:
- 依照第 7.5.3.1 节,F 的已修改形参的列表可应用于已修改实参的列表。
- 已修改形参的列表中的所有构造类型满足其约束(第 4.4.4 节)。
- 如果在上一步中替换了 F 的类型形参,则满足其约束。
- 如果 F 是静态方法,则方法组不得从其接收器在编译时已知将为变量还是值的 member-access 生成。
- 如果 F 是实例方法,则方法组不得从其接收器在编译时已知将为类型的 member-access 生成。
如果没有候选通过此测试,则产生编译时错误。
1.5.5 函数成员调用
本节描述在运行时发生的调用一个特定的函数成员的进程。这里假定绑定时进程已确定了要调用的特定成员(可能采用重载决策从一组候选函数成员中选出)。
为了描述调用进程,将函数成员分成两类:
- 静态函数成员。包括实例构造函数、静态方法、静态属性访问器和用户定义的运算符。静态函数成员总是非虚的。
- 实例函数成员。包括实例方法、实例属性访问器和索引器访问器。实例函数成员不是非虚的就是虚的,并且总是在特定的实例上调用。该实例由实例表达式计算,并可在函数成员内以 this(第 7.6.7 节)的形式对其进行访问。
函数成员调用的运行时处理包括以下步骤(其中 M 是函数成员,如果 M 是实例成员,则 EE 是实例表达式):
- 如果 M 是静态函数成员,则:
- 实参列表按照第 7.5.1 节中的说明进行计算。
- 调用 M。
- 如果 M 是在 value-type 中声明的实例函数成员,则:
- 计算 E。如果该计算导致异常,则不执行进一步的操作。
- 如果 E 没有被归类为一个变量,则创建一个与 E 同类型的临时局部变量,并将 E 的值赋给该变量。这样,E 就被重新归类为对该临时局部变量的一个引用。该临时变量在 M 中可以以 this 的形式被访问,但不能以任何其他形式访问。因此,仅当 E 是真正的变量时,调用方才可能观察到 M 对 this 所做的更改。
- 实参列表按照第 7.5.1 节中的说明进行计算。
- 调用 M。E 引用的变量成为 this 引用的变量。
- 如果 M 是在 reference-type 中声明的实例函数成员,则:
- 计算 E。如果该计算导致异常,则不执行进一步的操作。
- 实参列表按照第 7.5.1 节中的说明进行计算。
- 如果 E 的类型为 value-type,则执行装箱转换(第 4.3.1 节)以将 E 转换为 object 类型,并在下列步骤中,将 E 视为 object 类型。在这种情况下,M 只能是 System.Object 的成员。
- 检查 E 的值是否有效。如果 E 的值为 null,则将引发 System.NullReferenceException,并且不执行进一步的步骤。
- 要调用的函数成员实现按以下规则确定:
- 如果 E 的绑定时类型是接口,则调用的函数成员是 M 的实现,此实现由 E 引用的实例的运行时类型提供。确定此函数成员时,应用接口映射规则(第 13.4.4 节)确定由 M 引用的实例运行时类型提供的 E 实现。
- 否则,如果 M 是虚函数成员,则调用的函数成员是由 E 引用的实例运行时类型提供的 M 实现。确定此函数成员时,对于 E 引用的实例的运行时类型,应用“确定 M 的派生程度最大的实现”的规则(第 10.6.3 节)。
- 否则,M 是非虚函数成员,调用的函数成员是 M 本身。
- 调用在上一步中确定的函数成员实现。E 引用的对象成为 this 引用的对象。
1.5.5.1 已装箱实例上的调用
在下列情形中,可以通过 value-type 的已装箱实例来调用以该 value-type 实现的函数成员:
- 当该函数成员是从 object 类型继承的,且具有 override 修饰符,并通过 object 类型的实例表达式被调用时。
- 当函数成员是接口函数成员的实现并且通过 interface-type 的实例表达式被调用时。
- 当函数成员通过委托被调用时。
在这些情形中,将已装箱实例视为包含 value-type 的变量,并且此变量将在函数成员调用中成为 this 引用的变量。具体而言,这表示当调用已装箱实例的函数成员时,该函数成员可以修改已装箱实例中包含的值。
1.6 基本表达式
基本表达式包括最简单的表达式形式。
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
anonymous-object-creation-expression
typeof-expression
checked-expression
unchecked-expression
default-value-expression
anonymous-method-expression
基本表达式分为 array-creation-expression 和 primary-no-array-creation-expression。采用这种方式处理数组创建表达式(而不是将它与其他简单的表达式形式一起列出),使语法能够禁止可能的代码混乱,如
object o = new int[3][1];
被另外解释为
object o = (new int[3])[1];
1.6.1 文本
由 literal(第 2.4.4 节)组成的 primary-expression 属于值类别。
1.6.2 简单名称
simple-name 由一个标识符以及后跟的可选类型实参列表构成:
simple-name:
identifier type-argument-listopt
simple-name 的形式为 I 或 I<A1, ...,AK>,其中 I 是单个标识符,<A1, ..., AK> 是可选的 type-argument-list。如果未指定 type-argument-list 时,则可将 K 视为零。simple-name 的计算和分类方式如下:
- 如果 K 为零,simple-name 在某个 block 内出现,并且该 block(或包容 block)的局部变量声明空间(第 3.3 节)包含一个名为 I 的局部变量、形参或常量,则 simple-name 将引用该局部变量、形参或常量,并将归为变量或值类别。
- 如果 K 为零,并且 simple-name 出现在泛型方法声明体中,并且该声明包含名为 I 的类型形参,则 simple-name 将引用该类型形参。
- 否则,对于每个实例类型 T(第 10.3.1 节),从直接的包容类型声明的实例类型开始,对每个包容类或结构声明(如果有)的实例类型继续进行如下过程:
- 如果 K 为零,并且 T 的声明包含名为 I 的类型形参,则 simple-name 将引用该类型形参。
- 否则,如果在 T 中对具有 K 个类型实参的 I 进行成员查找(第 7.4 节)得到匹配项:
- 如果 T 为直接包容类或结构类型的实例类型,并且该查找标识了一个或多个方法,则结果是一个具有 this 的关联实例表达式的方法组。如果指定了类型实参列表,则将在调用泛型方法时使用它(第 7.6.5.1 节)。
- 否则,如果 T 为直接包容类或结构类型的实例类型,如果查找标识出一个实例成员,并且引用发生在实例构造函数、实例方法或实例访问器的 block 内,则结果与 this.I 形式的成员访问(第 7.6.4 节)相同。仅当 K 为零时才会发生这种情况。
- 否则,结果与 T.I 或 T.I<A1, ..., 形式的成员访问(第 7.6.4 节)相同。AK>.在此情况下,simple-name 引用实例成员将发生绑定时错误。
- 否则,对于每个命名空间 N,从出现 simple-name 的命名空间开始,依次继续每个包容命名空间(如果有),到全局命名空间为止,对下列步骤进行计算,直到找到实体:
- 如果 K 为零,并且 I 为 N 中的命名空间的名称,则:
- 如果出现 simple-name 的位置包含在 N 的命名空间声明中,并且该命名空间声明中包含将名称 I 与某个命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 simple-name 是不明确的,并将发生编译时错误。
- 否则,simple-name 引用 N 中名为 I 的命名空间。
- 否则,如果
N 包含一个具有名称 I 且有 K 个类型形参的可访问类型,则:
- 如果 K 为零,并且出现 simple-name 的位置包含在 N 的命名空间声明中,并且该命名空间声明中包含将名称 I 与某个命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 simple-name 是不明确的,并将发生编译时错误。
- 否则,namespace-or-type-name 引用利用给定类型实参构造的该类型。
- 否则,如果出现 simple-name 的位置包含在 N 的命名空间声明中:
- 如果 K 为零,并且该命名空间声明中包含一个将名称 I 与一个导入的命名空间或类型关联的 extern-alias-directive 或 using-alias-directive,则 simple-name 将引用该命名空间或类型。
- 否则,如果该命名空间声明的 using-namespace-directive 导入的命名空间中只包含一个名为 I 且有 K 个类型形参的类型,则 simple-name 将引用通过给定的类型实参构造的该类型。
- 否则,如果该命名空间声明的 using-namespace-directive 导入的命名空间中包含多个名为 I 且有 K 个类型形参的类型,则 simple-name 是不明确的,并将导致发生错误。
注意这整个步骤与 namespace-or-type-name(第 3.8 节)的处理中对应的步骤完全相同。
- 否则,simple-name 是未定义的,并将出现编译时错误。
1.6.2.1 块中的固定含义
对于表达式或声明符中以完整 simple-name 形式出现的每个给定标识符,在出现处最近的外层局部变量声明空间(第 3.3 节)内,表达式或声明符中以完整 simple-name 形式出现的每个其他同一标识符都必须引用相同的实体。该规则确保在给定的块、switch 块、for、foreach 或 using 语句或匿名函数中,名称的含义总是相同。
下面的示例
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.6.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.6.3 带括号的表达式
parenthesized-expression 由一个用括号括起来的 expression 组成。
parenthesized-expression:
(
expression )
通过计算括号内的 expression 来计算 parenthesized-expression。如果括号内的 expression 表示命名空间或类型,将发生编译时错误。否则,parenthesized-expression 的结果为所含 expression 的计算结果。
1.6.4 成员访问
member-access 的组成部分包括:一个 primary-expression、一个 predefined-type 或 qualified-alias-member,后面依次是一个“.”标记、一个 identifier 和一个 type-argument-list(可选)。
member-access:
primary-expression . identifier type-argument-listopt
predefined-type . identifier type-argument-listopt
qualified-alias-member . identifier type-argument-listopt
predefined-type: one of
bool byte char decimal double float int long
object sbyte short string uint ulong ushort
qualified-alias-member 产生式在第 9.7 节中定义。
member-access 的形式为 E.I 或 E.I<A1, ...,AK>,其中 E 是 primary-expression, I 是单个标识符,<A1, ..., AK> 是可选的 type-argument-list。如果未指定 type-argument-list 时,则可将 K 视为零。
member-access 类型为 dynamic 的 primary-expression是动态绑定的(第 7.2.2 节)。在这种情况下,编译器将成员访问归类为 dynamic 类型的属性访问。随后在运行时应用以下用于确定 member-access 含义的规则(使用 primary-expression 的运行时类型而不是编译时类型)。如果此运行时分类形成方法组,则成员访问必须为 invocation-expression 的 primary-expression。
member-access 的计算和分类方式如下:
- 如果 K 为零,E 是命名空间,并且 E 包含名为 I 的嵌套命名空间,则结果为该命名空间。
- 否则,如果 E 为命名空间,并且 E 包含具有名称为 I 且有 K 个类型形参的可访问类型,则结果为利用给定类型实参构造的该类型。
- 如果 E 是一个 predefined-type 或一个归类为类型的 primary-expression,E 不是类型形参,并且在 E 中对具有 K 个类型形参的 I 进行成员查找(第 7.4 节)得到匹配项,则 E.I 的计算和分类方式如下:
- 如果 I 标识一个类型,则结果为使用给定类型实参构造的该类型。
- 如果 I 标识一个或多个方法,则结果为一个没有关联的实例表达式的方法组。如果指定了类型实参列表,则将在调用泛型方法时使用它(第 7.6.5.1 节)。
- 如果 I 标识一个 static 属性,则结果为一个没有关联的实例表达式的属性访问。
- 如果 I 标识一个 static 字段,则:
- 如果该字段为 readonly 并且引用发生在声明该字段的类或结构的静态构造函数外,则结果为值,即 E 中静态字段 I 的值。
- 否则,结果为变量,即 E 中的静态字段 I。
- 如果 I 标识一个 static 事件,则:
- 如果引用发生在声明了该事件的类或结构内,并且事件不是用 event-accessor-declarations(第 10.8 节)声明的,则完全将 I 视为静态字段来处理 E.I。
- 否则,结果为没有关联的实例表达式的事件访问。
- 如果 I 标识一个常量,则结果为值,即该常量的值。
- 如果 I 标识枚举成员,则结果为值,即该枚举成员的值。
- 否则,E.I 是无效成员引用,并且会出现编译时错误。
- 如果 E 是类型为 T 的属性访问、索引器访问、变量或值,并且在 T 中对具有 K 个类型实参的 I 进行成员查找(第 7.4 节)时得到匹配项,则 E.I 的计算和分类方式如下:
- 首先,如果 E 为属性访问或索引器访问,则获取该属性访问或索引器访问的值(第 7.1.1 节),并将 E 重新归为值类别。
- 如果 I 标识一个或多个方法,则结果为具有 E 的关联实例表达式的方法组。如果指定了类型实参列表,则将在调用泛型方法时使用它(第 7.6.5.1 节)。
- 如果 I 标识实例属性,则结果为具有 E 的关联实例表达式的属性访问。
- 如果 T 为 class-type 并且 I 标识此 class-type 的一个实例字段,则:
- 如果 E 的值为 null,则将引发 System.NullReferenceException。
- 否则,如果字段为 readonly 并且引用发生在声明字段的类的实例构造函数外,则结果为值,即 E 引用的对象中字段 I 的值。
- 否则,结果为变量,即 E 引用的对象中的字段 I。
- 如果 T 为 struct-type 并且 I 标识此 struct-type 的实例字段,则:
- 如果 E 为值,或者如果字段为 readonly 并且引用发生在声明字段的结构的实例构造函数外,则结果为值,即 E 给定的结构实例中字段 I 的值。
- 否则,结果为变量,即 E 给定的结构实例中的字段 I。
- 如果 I 标识实例事件,则:
- 如果引用发生在声明事件的类或结构内,且事件不是用 event-accessor-declaration(第 10.8 节)声明的,并且引用未作为 += 或 -= 运算符的左侧内容出现,则完全将 I 视为实例字段来处理 E.I。
- 否则,结果为具有 E 的关联实例表达式的事件访问。
- 否则,将尝试将 E.I 当作扩展方法调用(第 7.6.5.2 节)来处理。如果处理失败,则表明 E.I 是无效成员引用,并将出现绑定时错误。
1.6.4.1 相同的简单名称和类型名称
在 E.I 形式的成员访问中,如果 E 为单个标识符,并且 E 可能有两种含义:作为 simple-name(第 7.6.2 节)的 E,作为 type-name(第 3.8 节)的 type-name。只要前者所标识的对象实体(无论是常量、字段、属性、局部变量或参数)所属的类型就是以后者命名的类型,则 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.6.4.2 语法多义性
simple-name(第 7.6.2 节)和 member-access(第 7.6.4 节)的产生式可能引起表达式的语法多义性。例如,语句:
F(G<A,B>(7));
可解释为用两个实参 G < A 和 B > (7) 调用 F。或者,也可以将它解释为用一个实参调用 F,该实参是使用两个类型实参和一个常规实参对泛型方法 G 的调用。
如果可将某个标记序列分析(在上下文中)为以 type-argument-list(第 7.6.2 节)结尾的 simple-name(第 7.6.4 节)、member-access(第 4.4.1 节)或 pointer-member-access(第 18.5.2 节),则会检查紧随结束 > 标记之后的标记。如果它是下列标记之一
( ) ] } : ; , . ? == != | ^
则将 type-argument-list 保留为 simple-name、member-access 或 pointer-member-access 的一部分,并丢弃该标记序列的其他任何可能的分析。否则,不将 type-argument-list 视为 simple-name、member-access 或 pointer-member-access 的一部分,即使不存在该标记序列的其他可能的分析。注意,在分析 namespace-or-type-name(第 3.8 节)中的 type-argument-list 时,将不应用这些规则。语句
F(G<A,B>(7));
将(按照此规则)被解释为使用一个实参对 F 进行调用,该实参是使用两个类型实参和一个常规实参对泛型方法 G 的调用。语句
F(G < A,
B > 7);
F(G < A, B >> 7);
都被解释为使用两个实参调用 F。语句
x = F < A > +y;
将被解释为小于运算符、大于运算符和一元加运算符,如同语句 x = (F < A) > (+y),而不是在带 type-argument-list 的 simple-name 后面跟着一个一元加运算符。在语句
x = y is C<T> + z;
中,标记 C<T> 被解释为带 type-argument-list 的 namespace-or-type-name。
1.6.5 调用表达式
invocation-expression 用于调用方法。
invocation-expression:
primary-expression ( argument-listopt )
如果至少符合以下条件之一,则 invocation-expression 是动态绑定的(第 7.2.2 节):
- primary-expression 具有编译时类型 dynamic。
- 可选 argument-list 中至少有一个实参具有编译时类型 dynamic,并且 primary-expression 没有委托类型。
在此情况下,编译器将 invocation-expression 归类为 dynamic 类型的值。随后在运行时应用以下用于确定 invocation-expression 的含义的规则(使用具有编译时类型 dynamic 的 primary-expression 和参数的运行时类型而不是编译时类型)。如果 primary-expression 没有编译时类型 dynamic,则方法调用将进行有限的编译时检查,如第 7.5.4 节所述。
invocation-expression 的 primary-expression 必须是方法组或 delegate-type 的值。如果 primary-expression 是方法组,则 invocation-expression 为方法调用(第 7.6.5.1 节)。如果 primary-expression 是 delegate-type 的值,则 invocation-expression 为委托调用(第 7.6.5.3 节)。如果 primary-expression 既非方法组亦非 delegate-type 的值,则会出现绑定时错误。
可选的 argument-list(第 7.5.1 节)列出的值或变量引用将在调用时传递给方法的参数。
invocation-expression 的计算结果按如下方式进行分类:
- 如果 invocation-expression 调用的方法或委托返回 void,则结果为 Nothing。Nothing 类别的表达式只能在 statement-expression(第 8.6 节)的上下文中使用或用作 lambda-expression(第 7.15 节)的体。否则会发生绑定时错误。
- 否则,结果是由方法或委托返回的类型的值。
1.6.5.1 方法调用
对于方法调用,invocation-expression 的 primary-expression 必须是方法组。方法组标识要调用的方法,或者标识从中选择要调用的特定方法的重载方法集。在后一种情形中,具体调用哪个方法取决于 argument-list 中的参数的类型所提供的上下文。
M(A) 形式(其中 M 是方法组并且可能包括 type-argument-list,A 是可选的 argument-list)的方法调用的绑定时处理包括以下步骤:
- 构造方法调用的候选方法集。对于与方法组 M 关联的每个方法 F:
- 如果 F 是非泛型的,则在满足以下条件时,F 是候选方法:
- M 没有类型实参列表,并且
- 对 A 来说,F 是适用的(第 7.5.3.1 节)。
- 如果 F 是泛型的,并且 M 没有类型实参列表,则在满足以下条件时,F 是候选方法:
- 类型推断(第 7.5.2 节)成功,为该调用推断出一个类型实参列表,并且
- 一旦使用推断出的类型实参替换对应的方法类型形参,则 F 的形参列表中的所有构造类型都满足它们的约束(第 4.4.4 节),并且对 A 来说,F 的形参列表是适用的(第 7.5.3.1 节)。
- 如果 F 是泛型的,并且 M 包含类型实参列表,则在满足以下条件时,F 是候选方法:
- F 具有的方法类型形参数目与类型实参列表中提供的数目相同,并且
- 一旦使用类型实参替换对应的方法类型形参,则 F 的形参列表中的所有构造类型都满足它们的约束(第 4.4.4 节),并且对 A 来说,F 的形参列表是适用的(第 7.5.3.1 节)。
- 候选方法集被减少到仅包含派生程度最大的类型中的方法:对于该集中的每个方法 C.F(其中 C 是声明了方法 F 的类型),将从该集中移除在 C 的基类型中声明的所有方法。此外,如果 C 是 object 以外的类类型,则从该集中移除在接口类型中声明的所有方法。(仅当该方法组是具有除 object 以外的有效基类和非空有效接口集的类型形参上的成员查找的结果时,后一条规则才有效。)
- 如果得到的候选方法集为空,则将放弃后续步骤中的其他处理,而尝试以扩展方法调用(第 7.6.5.2 节)的形式处理该调用。如果此操作失败,则不存在适用的方法,并将出现绑定时错误。
- 使用第 7.5.3 节中的重载决策规则确定候选方法集中的最佳方法。如果无法确定单个最佳方法,则该方法调用是不明确的,并发生绑定时错误。在执行重载解析时,将在使用类型实参(提供或推断出的)替换对应的方法类型形参之后考虑泛型方法的参数。
- 所选最佳方法的最终验证按如下方式执行:
- 该方法在方法组的上下文中进行验证:如果该最佳方法是静态方法,则方法组必须是从 simple-name或通过某个类型从 member-access 产生的。如果该最佳方法为实例方法,则方法组必须是从 simple-name、通过某个变量或值从 member-access 或从 base-access 产生的。如果两个要求都不满足,则发生绑定时错误。
- 如果该最佳方法是泛型方法,则根据泛型方法上声明的约束(第 4.4.4 节)检查类型实参(提供或推断出的)。如果任何类型实参不满足类型形参上的对应约束,则会发生绑定时错误。
通过以上步骤在绑定时选定并验证了方法后,将根据第 7.5.4 节中说明的函数成员调用规则处理实际的运行时调用。
上述决策规则的直观效果如下:为找到方法调用所调用的特定方法,从方法调用指示的类型开始,在继承链中一直向上查找,直到至少找到一个适用的、可访问的、非重写的方法声明。然后对该类型中声明的适用的、可访问的、非重写的方法集执行类型推断和重载决策,并调用由此选定的方法。如果找不到方法,则改为尝试以扩展方法调用的形式处理该调用。
1.6.5.2 扩展方法调用
在以下形式之一的方法调用(第 7.5.5.1 节)中
expr . identifier ( )
expr . identifier ( args )
expr . identifier < typeargs > ( )
expr . identifier < typeargs > ( args )
如果正常的调用处理找不到适用的方法,则将尝试以扩展方法调用的形式处理该构造。如果 expr 或任意 args 具有编译时类型 dynamic,将不应用扩展方法。
目标是查找最佳的 type-name C,以便可以进行相应的静态方法调用:
C . identifier ( expr )
C . identifier ( expr , args )
C . identifier < typeargs > ( expr )
C . identifier < typeargs > ( expr , args )
如果满足以下各项,则扩展方法 Ci.Mj 将符合条件:
- Ci 为非泛型、非嵌套类
- Mj 的名称为 identifier
- Mj 作为如上所示的静态方法应用于参数时是可访问且适用的
- 存在从 expr 到 Mj. 的第一个参数的类型的隐式标识、引用或装箱转换。
对 C 的搜索操作如下:
- 从最接近的封闭命名空间声明开始,接下来是每个封闭命名空间声明,最后是包含编译单元,搜索将连续进行以找到候选的扩展方法集:
- 如果给定的命名空间或编译单元直接包含具有适当扩展方法 Mj 的非泛型类型声明 Ci,则这些扩展方法的集合为候选集。
- 如果使用给定命名空间或编译单元中的命名空间指令导入的命名空间直接包含具有适当扩展方法 Mj 的非泛型类型声明 Ci,则这些扩展方法的集合为候选集。
- 如果在任何封闭命名空间声明或编译单元中都找不到候选集,则会出现编译时错误。
- 否则,对候选集应用重载决策(如第 7.5.3 节所述)。如果找不到一个最佳方法,则会出现编译时错误。
- C 是将最佳方法声明为扩展方法的类型。
如果将 C 用作目标,则将以静态方法调用(第 7.5.4 节)的形式处理该方法调用。
上述规则表示,实例方法优先于扩展方法,内部命名空间声明中可用的扩展方法优先于外部命名空间声明中可用的扩展方法,并且直接在命名空间中声明的扩展方法优先于通过 using 命名空间指令导入该命名空间的扩展方法。例如:
public static class E
{
public static void F(this object obj, int
i) { }
public
static void F(this object obj, string s) { }
}
class A { }
class B
{
public void F(int i) { }
}
class C
{
public void F(object obj) { }
}
class X
{
static void Test(A a, B b, C c) {
a.F(1); // E.F(object, int)
a.F("hello"); // E.F(object, string)
b.F(1); // B.F(int)
b.F("hello"); // E.F(object, string)
c.F(1); // C.F(object)
c.F("hello"); // C.F(object)
}
}
在该示例中,B 的方法优先于第一个扩展方法,而 C 的方法优先于这两个扩展方法。
public static class C
{
public static void F(this int i) {
Console.WriteLine("C.F({0})", i); }
public static void G(this int i) {
Console.WriteLine("C.G({0})", i); }
public static void H(this int i) {
Console.WriteLine("C.H({0})", i); }
}
namespace N1
{
public static class D
{
public static void F(this int i) {
Console.WriteLine("D.F({0})", i); }
public static void G(this int i) {
Console.WriteLine("D.G({0})", i); }
}
}
namespace N2
{
using N1;
public
static class E
{
public static void F(this int i) {
Console.WriteLine("E.F({0})", 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。
1.6.5.3 委托调用
对于委托调用,invocation-expression 的 primary-expression 必须是 delegate-type 的值。另外,将 delegate-type 视为与 delegate-type 具有相同的参数列表的函数成员,delegate-type 对于 invocation-expression 的 argument-list 必须是适用的(第 7.5.3.1 节)。
D(A) 形式(其中 D 是 delegate-type 的 primary-expression,A 是可选的 argument-list)的委托调用的运行时处理包括以下步骤:
- 计算 D。如果此计算导致异常,则不执行进一步的操作。
- 检查 D 的值是否有效。如果 D 的值为 null,则将引发 System.NullReferenceException,并且不执行进一步的步骤。
- 否则,D 是一个对委托实例的引用。对该委托的调用列表中的每个可调用实体,执行函数成员调用(第 7.5.4 节)。对于由实例和实例方法组成的可调用实体,用于调用的实例是包含在可调用实体中的实例。
1.6.6 元素访问
一个 element-access 包括一个 primary-no-array-creation-expression,再后接“[”标记、argument-list 和“]”标记。argument-list 由一个或多个 argument 组成,各实参之间用逗号分隔。
element-access:
primary-no-array-creation-expression [ argument-list ]
element-access 的 argument-list 不允许包含 ref 或 out 参数。
如果至少符合以下条件之一,则 element-access 是动态绑定的(第 7.2.2 节):
- primary-no-array-creation-expression 具有编译时类型 dynamic。
- argument-list 中至少有一个表达式具有编译时类型 dynamic,并且 primary-no-array-creation-expression 没有数组类型。
在此情况下,编译器将 element-access 归类为 dynamic 类型的值。随后在运行时应用以下用于确定 element-access 的含义的规则(使用具有编译时类型 dynamic 的 primary-no-array-creation-expression 和 argument-list 表达式的运行时类型而不是编译时类型)。如果 primary-no-array-creation-expression 没有编译时类型 dynamic,则元素访问将进行有限的编译时检查,如第 7.5.4 节所述。
如果 element-access 的 primary-no-array-creation-expression 是 array-type 的值,则该 element-access 是数组访问(第 7.6.6.1 节)。否则,该 primary-no-array-creation-expression 必须是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下,element-access 为索引器访问(第 7.6.6.2 节)。
1.6.6.1 数组访问
对于数组访问,element-access 的 primary-no-array-creation-expression 必须是 array-type 的值。此外,数组访问的 argument-list 不允许包含命名参数。argument-list 中表达式的个数必须与 array-type 的秩相同,并且每个表达式都必须属于 int、uint、long、ulong 类型,或者必须可以隐式转换为这些类型中的一种或多种。
数组访问的计算结果是数组的元素类型的变量,即由 argument-list 中表达式的值选定的数组元素。
P[A] 形式(其中 P 是 array-type 的 primary-no-array-creation-expression,A 是 argument-list)的数组访问运行时处理包括以下步骤:
- 计算 P。如果此计算导致异常,则不执行进一步的操作。
- argument-list 的索引表达式按从左到右的顺序计算。计算每个索引表达式后,执行到下列类型之一的隐式转换(第 6.1 节):int、uint、long、ulong。选择此列表中第一个存在相应隐式转换的类型。例如,如果索引表达式是 short 类型,则将执行到 int 的隐式转换,这是因为可以执行从 short 到 int 和从 short 到 long 的隐式转换。如果计算索引表达式或后面的隐式转换时导致异常,则不再进一步计算索引表达式,并且不再执行进一步的操作。
- 检查 P 的值是否有效。如果 P 的值为 null,则将引发 System.NullReferenceException,并且不执行进一步的步骤。
- 针对由 P 引用的数组实例的每个维度的实际界限,检查 argument-list 中每个表达式的值。如果一个或多个值超出了范围,则引发 System.IndexOutOfRangeException,并且不再执行进一步的操作。
- 计算由索引表达式给定的数组元素的位置,此位置将成为数组访问的结果。
1.6.6.2 索引器访问
对于索引器访问,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 的基类型中声明的所有符合下列条件的索引器组成:它们不是经 override 声明的,并且在当前上下文(第 3.5 节)中可以访问。
- 将该集缩减为那些适用的并且不被其他索引器隐藏的索引器。对该集中的每个索引器 S.I(其中 S 为声明索引器 I 的类型)应用下列规则:
- 如果 I 对于 A(第 7.5.3.1 节)不适用,则 I 从集中移除。
- 如果 I 对于 A(第 7.5.3.1 节)适用,则从该集中移除在 S 的基类型中声明的所有索引器。
- 如果 I 对于 A(第 7.5.3.1 节)适用并且 S 为非 object 的类类型,则从该集中移除在接口中声明的所有索引器。
- 如果结果候选索引器集为空,则不存在适用的索引器,并发生绑定时错误。
- 使用第 7.5.3 节中的重载决策规则确定候选索引器集中的最佳索引器。如果无法确定单个最佳索引器,则该索引器访问是不明确的,并发生绑定时错误。
- argument-list 的索引表达式按从左到右的顺序计算。索引器访问的处理结果是属于索引器访问类别的表达式。索引器访问表达式引用在上一步骤中确定的索引器,并具有 P 的关联实例表达式和 A 的关联参数列表。
根据索引器访问的使用上下文,索引器访问导致调用该索引器的 get-accessor 或 set-accessor。如果索引器访问是赋值的目标,则调用 set-accessor 以赋新值(第 7.17.1 节)。在其他所有情况下,调用 get-accessor 以获取当前值(第 7.1.1 节)。
1.6.7 this 访问
this-access 由保留字 this 组成。
this-access:
this
this-access 只能在实例构造函数、实例方法或实例访问器的 block 中使用。它具有下列含义之一:
- 当 this 在类的实例构造函数内的 primary-expression 中使用时,它属于值类别。此时,该值的类型是使用 this 的类实例类型(第 10.3.1 节),并且该值就是对所构造的对象的引用。
- 当 this 在类的实例方法或实例访问器内的 primary-expression 中使用时,它属于值类别。此时,该值的类型是使用 this 的类实例类型(第 10.3.1 节),并且该值就是对为其调用方法或访问器的对象的引用。
- 当 this 在结构的实例构造函数内的 primary-expression 中使用时,它属于变量类别。该变量的类型是使用 this 的结构实例类型(第 10.3.1 节),并且该变量表示的正是所构造的结构。结构实例构造函数的 this 变量的行为与结构类型的 out 参数完全一样,具体而言,这表示该变量在实例构造函数的每个执行路径中必须已明确赋值。
- 当 this 在结构的实例方法或实例访问器内的 primary-expression 中使用时,它属于变量类别。该变量的类型就是使用 this 的结构实例类型(第 10.3.1 节)。
- 如果方法或访问器不是迭代器(第 10.14 节),则 this 变量表示为其调用方法或访问器的结构,并且其行为与结构类型的 ref 参数完全相同。
- 如果方法或访问器是迭代器,则 this 变量表示为其调用方法或访问器的结构的 copy,并且其行为与结构类型的 value 参数完全相同。
在以上列出的上下文以外的上下文内的 primary-expression 中使用 this 是编译时错误。具体说就是不能在静态方法、静态属性访问器中或字段声明的 variable-initializer 中引用 this。
1.6.8 基访问
base-access 由保留字 base,后接一个“.”标记和一个标识符或一个用方括号括起来的 argument-list 组成:
base-access:
base
.
identifier
base
[
argument-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.5.4 节)调用哪个函数成员的规则有一些更改。确定调用哪一个函数成员的方法是,查找该函数成员相对于 B(而不是相对于 this 的运行时类型,在非基访问中通常如此)的派生程度最大的实现(第 10.6.3 节)。因此,在 virtual 函数成员的 override 中,可以使用 base-access 调用该函数成员的被继承了的实现。如果 base-access 引用的函数成员是抽象的,则发生绑定时错误。
1.6.9 后缀增量和后缀减量运算符
post-increment-expression:
primary-expression ++
post-decrement-expression:
primary-expression --
后缀增量或后缀减量运算符的操作数必须是属于变量、属性访问或索引器访问类别的表达式。该运算的结果是与操作数类型相同的值。
如果 primary-expression 具有编译时类型 dynamic,则运算符动态绑定(第 7.2.2 节),post-increment-expression 或 post-decrement-expression 具有编译时类型 dynamic,并且在运行时通过 primary-expression 的运行时类型应用以下规则。
如果后缀增量或减量运算的操作数是属性或索引器访问,则属性或索引器必须同时具有 get 和 set 访问器。如果不是这样,则发生绑定时错误。
将使用一元运算符重载决策(第 7.3.3 节)选择特定的运算符实现。以下类型存在预定义的 ++ 和 -- 运算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 以及任何枚举类型。预定义 ++ 运算符返回的结果值为操作数加上 1,预定义 -- 运算符返回的结果值为操作数减去 1。在 checked 上下文中,如果此加法或减法运算的结果在结果类型的范围之外,且结果类型为整型或枚举类型,则会引发 System.OverflowException。
x++ 或 x-- 形式的后缀增量或后缀减量运算的运行时处理包括以下步骤:
- 如果 x 属于变量:
- 计算 x 以产生变量。
- 保存 x 的值。
- 调用选定的运算符,将 x 的保存值作为参数。
- 运算符返回的值存储在由 x 的计算结果给定的位置中。
- x 的保存值成为运算结果。
- 如果 x 属于属性或索引器访问:
- 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),结果用于后面的对 get 和 set 访问器调用。
- 调用 x 的 get 访问器并保存返回的值。
- 调用选定的运算符,将 x 的保存值作为参数。
- 调用 x 的 set 访问器,将运算符返回的值作为其 value 参数。
- x 的保存值成为运算结果。
++ 和 -- 运算符也支持前缀表示法(第 7.7.5 节)。通常,x++ 或 x-- 的结果是运算“之前”x 的值,而 ++x 或 --x 的结果是运算“之后”x 的值。在任何一种情况下,运算后 x 本身都具有相同的值。
operator ++ 或 operator -- 的实现既可以用后缀表示法调用,也可以用前缀表示法调用。但是,不能让这两种表示法分别去调用该运算符的不同的实现。
1.6.10 new 运算符
new 运算符用于创建类型的新实例。
有三种形式的 new 表达式:
- 对象创建表达式用于创建类类型和值类型的新实例。
- 数组创建表达式用于创建数组类型的新实例。
- 委托创建表达式用于创建委托类型的新实例。
new 运算符表示创建类型的一个实例,但并非暗示要为它动态分配内存。具体而言,值类型的实例不要求在表示它的变量以外有额外的内存,因而,在使用 new 创建值类型的实例时不发生动态分配。
1.6.10.1 对象创建表达式
object-creation-expression 用于创建 class-type 或 value-type 的新实例。
object-creation-expression:
new
type ( argument-listopt )
object-or-collection-initializeropt
new
type
object-or-collection-initializer
object-or-collection-initializer:
object-initializer
collection-initializer
object-creation-expression 的 type 必须是 class-type、value-type 或 type-parameter。该 type 不能是 abstract class-type。
仅当 type 为 class-type 或 struct-type 时才允许使用可选的 argument-list(第 7.5.1 节)。
对象创建表达式可以省略构造函数参数列表和封闭括号,前提是该表达式中包括对象初始值设定项或集合初始值设定项。省略构造函数参数列表和封闭括号与指定空的参数列表等效。
对包括对象初始值设定项或集合初始值设定项的对象创建表达式的处理包括:首先处理实例构造函数,然后处理对象初始值设定项(第 7.6.10.2 节)或集合初始值设定项(第 7.6.10.3 节)指定的成员或元素初始化。
如果可选 argument-list 中的任意参数具有编译时类型 dynamic,则 object-creation-expression 动态绑定(第 7.2.2 节),并在运行时使用 argument-list 中具有编译时类型 dynamic 的那些参数的运行时类型应用以下规则。但是,对象创建进行有限的编译时检查,如第 7.5.4 节所述。
new T(A) 形式(其中 T 是 class-type 或 value-type,A 是可选 argument-list)的 object-creation-expression 的绑定时处理包括以下步骤:
- 如果 T 是 value-type 且 A 不存在:
- object-creation-expression 是默认构造函数调用。object-creation-expression 的结果是 T 类型的一个值,即在第 4.1.1 节中定义的 T 的默认值。
- 否则,如果 T 是 type-parameter 且 A 不存在:
- 如果还没有为 T 指定值类型约束或构造函数约束(第 10.1.5 节),则会出现绑定时错误。
- object-creation-expression 的结果是类型参数所绑定到的运行时类型的值,即调用该类型的默认构造函数所产生的结果。运行时类型可以是引用类型或值类型。
- 否则,如果 T 是 class-type 或 struct-type:
- 如果 T 是 abstract class-type,则会发生编译时错误。
- 使用第 7.5.3 节中的重载决策规则确定要调用的实例构造函数。候选实例构造函数集由 T 中声明的适用于 A(第 7.5.3.1 节)的所有可访问实例构造函数组成。如果候选实例构造函数集为空,或者无法标识单个最佳实例构造函数,则发生绑定时错误。
- object-creation-expression 的结果是 T 类型的值,即由调用在上面的步骤中确定的实例构造函数所产生的值。
- 否则,object-creation-expression 无效,并发生绑定时错误。
即使 object-creation-expression 是动态绑定的,编译时类型仍为 T。
new T(A) 形式(其中 T 是 class-type 或 struct-type,A 是可选 argument-list的 object-creation-expression 的运行时处理包括以下步骤:
- 如果 T 是 class-type:
- 为 T 类的一个新实例分配存储位置。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
- 新实例的所有字段都将初始化为它们的默认值(第 5.2 节)。
- 根据函数成员调用(第 7.5.4 节)的规则来调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,因而,可以从实例构造函数中用 this 来访问该实例。
- 如果 T 是 struct-type:
- 通过分配一个临时局部变量来创建类型 T 的实例。由于要求 struct-type 的实例构造函数为所创建的实例的每个字段明确赋值,因此不需要初始化此临时变量。
- 根据函数成员调用(第 7.5.4 节)的规则来调用实例构造函数。对新分配的实例的引用会自动传递给实例构造函数,因而,可以从实例构造函数中用 this 来访问该实例。
1.6.10.2 对象初始值设定项
对象初始值设定项 (object initializer) 为某个对象的零个或多个字段或属性指定值。
object-initializer:
{ member-initializer-listopt }
{ member-initializer-list , }
member-initializer-list:
member-initializer
member-initializer-list , member-initializer
member-initializer:
identifier = initializer-value
initializer-value:
expression
object-or-collection-initializer
对象初始值设定项包含一系列成员初始值设定项,它们括在“{”和“}”标记中并且用“,”分隔。每个成员初始值设定项必须命名所初始化的对象的可访问字段或属性,后接等号以及表达式或者对象初始值设定项或集合初始值设定项。如果对象初始值设定项对于同一个字段或属性包括多个成员初始值设定项,则会发生错误。对象初始值设定项无法引用它所初始化的新创建的对象。
在等号后面指定表达式的成员初始值设定项的处理方式与对字段或属性赋值(第 7.17.1 节)的方式相同。
在等号后面指定对象初始值设定项的成员初始值设定项是嵌套对象初始值设定项 (nested object initializer),即嵌入对象的初始化。嵌套对象初始值设定项中的赋值不是对字段或属性赋新值,这些赋值被视为对字段或属性的成员的赋值。嵌套对象初始值设定项不能应用于具有值类型的属性,也不能应用于具有值类型的只读字段。
在等号后面指定集合初始值设定项的成员初始值设定项是嵌入集合的初始化。初始值设定项中给出的元素不是将新集合赋给字段或属性,这些元素将被添加到字段或属性引用的集合中。字段或属性必须是符合第 7.6.10.3 节中指定的要求的集合类型。
下面的类表示一个具有两个坐标的点:
public class Point
{
int x, y;
public
int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y
= value; } }
}
可以使用下面的语句创建和初始化 Point 的实例:
Point a = new Point { X = 0, Y = 1 };
此语句与下面的语句等效
Point __a = new Point();
__a.X = 0;
__a.Y = 1;
Point a = __a;
其中,__a 是以其他方式不可见且不可访问的临时变量。下面的类表示通过两个点创建的一个矩形。
public class Rectangle
{
Point p1, p2;
public
Point P1 { get { return p1; } set { p1 = value; } }
public Point P2 { get { return p2; } set
{ p2 = value; } }
}
可以使用下面的语句创建和初始化 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’s 的构造函数分配以下两个嵌入式 Point 实例
public class Rectangle
{
Point p1 = new Point();
Point p2 = new Point();
public
Point P1 { get { return p1; } }
public Point P2 { get { return p2; } }
}
则以下构造可用于初始化嵌入的 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;
1.6.10.3 集合初始值设定项
集合初始值设定项指定集合中的元素。
collection-initializer:
{ element-initializer-list }
{ element-initializer-list , }
element-initializer-list:
element-initializer
element-initializer-list , element-initializer
element-initializer:
non-assignment-expression
{ expression-list }
expression-list:
expression
expression-list , expression
集合初始值设定项包含一系列元素初始值设定项,它们括在“{”和“}”标记中并且用“,”分隔。每个元素初始值设定项指定要添加到所初始化的集合对象中的元素,它由括在“{”和“}”标记中并且用“,”分隔的表达式列表组成。写入单个表达式元素初始值设定项时可以不使用大括号,但不能是赋值表达式,以避免与成员初始值设定项产生歧义。non-assignment-expression 产生式是在第 7.18 节中定义的。
下面是包括集合初始值设定项的对象创建表达式的一个示例:
List<int> digits = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
集合初始值设定项应用到的集合对象必须是实现 System.Collections.IEnumerable 的类型,否则会出现编译时错误。对于按顺序指定的每个元素,集合初始值设定项将调用目标对象的 Add 方法(将元素初始值设定项的表达式列表用作参数列表),从而对每个调用都应用正常重载决策。因此对于每个元素初始值设定项,集合对象必须包含适用的 Add 方法。
下面的类表示一个联系人,包括姓名和电话号码列表:
public class Contact
{
string name;
List<string> phoneNumbers = new
List<string>();
public string Name { get { return name; } set { name = value; } }
public
List<string> PhoneNumbers { get { return phoneNumbers; } }
}
可以使用如下语句创建和初始化 List<Contact>:
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 是以其他方式不可见且不可访问的临时变量。
1.6.10.4 数组创建表达式
array-creation-expression 用于创建 array-type 的新实例。
array-creation-expression:
new
non-array-type [ expression-list ]
rank-specifiersopt
array-initializeropt
new
array-type array-initializer
new
rank-specifier 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}}
第三种形式的数组创建表达式称为隐式类型化数组创建表达式 (implicitly typed array creation expression)。这种形式与第二种形式类似,不同的是数组的元素类型未显式指定,而是被确定为数组初始值设定项中表达式集的最通用类型(第 7.5.2.14 节)。对于多维数组(即其中的 rank-specifier 包含至少一个逗号的数组),此集由嵌套 array-initializer 中找到的所有 expression 组成。
数组初始值设定项的介绍详见第 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];
只创建单个对象(即一个二维数组)并在单个语句中完成分配。
下面是隐式类型化的数组创建表达式的示例:
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[]。也可以将其中某个元素强制转换为公共基类型,之后该类型将成为推断出的元素类型。
隐式类型化的数组创建表达式可以与匿名对象初始值设定项(第 7.6.10.6 节)组合,以创建匿名类型化的数据结构。例如:
var contacts = new[] {
new {
Name = "Chris Smith",
PhoneNumbers = new[] {
"206-555-0101", "425-882-8080" }
},
new {
Name = "Bob Harris",
PhoneNumbers = new[] {
"650-555-0199" }
}
};
1.6.10.5 委托创建表达式
delegate-creation-expression 用于创建 delegate-type 的新实例。
delegate-creation-expression:
new
delegate-type ( expression )
委托创建表达式的参数必须是属于编译时类型 dynamic 或 delegate-type 的方法组、匿名函数或值。如果参数是方法组,则它标识方法和(对于实例方法)为其创建委托的对象。如果实参是匿名函数,则它直接定义委托目标的形参和方法体。如果实参是值,则它标识要创建其副本的委托实例。
如果 expression 具有编译时类型 dynamic,则 delegate-creation-expression 动态绑定(第 7.2.2 节),并在运行时使用 expression 的运行时类型应用以下规则。否则在编译时应用这些规则。
new D(E) 形式(其中 D 是 delegate-type,E 是 expression)的 delegate-creation-expression 的绑定时处理包括以下步骤:
- 如果 E 为方法组,则委托创建表达式的处理方式与从 E 到 D 的方法组转换(第 6.6 节)方式相同。
- 如果 E 为匿名函数,则委托创建表达式的处理方式与从 E 到 D 的匿名函数转换(第 6.5 节)方式相同。
- 如果 E 是值,则 E 必须与 D 兼容(第 15.1 节),并且结果为对新创建的 D 类型委托的引用(引用的调用列表与 E 相同)。如果 E 与 D 不一致,则会发生编译时错误。
new D(E) 形式(其中 D 是 delegate-type,E 是 expression)的 delegate-creation-expression 的运行时处理包括以下步骤:
- 如果 E 为方法组,则委托创建表达式的计算方式与从 E 到 D 的方法组转换(第 6.6 节)方式相同。
- 如果 E 为匿名函数,则委托创建的计算方式与从 E 到 D 的匿名函数转换(第 6.5 节)方式相同。
- 如果 E 是 delegate-type 的值:
- 计算 E。如果此计算导致异常,则不执行进一步的操作。
- 如果 E 的值为 null,则将引发 System.NullReferenceException,并且不执行进一步的步骤。
- 为委托类型 D 的一个新实例分配存储位置。如果没有足够的可用内存来为新实例分配存储位置,则引发 System.OutOfMemoryException,并且不执行进一步的操作。
- 用与 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.6.10.6 匿名对象创建表达式
anonymous-object-creation-expression 用于创建匿名类型的对象。
anonymous-object-creation-expression:
new anonymous-object-initializer
anonymous-object-initializer:
{ member-declarator-listopt }
{ member-declarator-list , }
member-declarator-list:
member-declarator
member-declarator-list , member-declarator
member-declarator:
simple-name
member-access
base-access
identifier = expression
匿名对象初始值设定项声明一个匿名类型并返回该类型的一个实例。匿名类型是直接从 object 继承的无名类类型。匿名类型的成员是只读属性序列,这些属性从用于创建该类型实例的匿名对象初始值设定项推断。具体而言,以下形式的匿名对象初始值设定项
new { p1 = e1 , p2 = e2 , … pn = en }
声明一个以下形式的匿名类型
class __Anonymous1
{
private readonly T1 f1 ;
private readonly T2 f2 ;
…
private readonly Tn fn ;
public
__Anonymous1(T1 a1, T2 a2,…, Tn an) {
f1 = a1 ;
f2 = a2 ;
…
fn = an ;
}
public
T1 p1 { get { return f1 ; } }
public T2 p2 { get { return f2 ; } }
…
public Tn pn { get { return fn ; } }
public
override bool Equals(object __o) { … }
public override int GetHashCode() { … }
}
其中每个 Tx 都是对应表达式 ex 的类型。member-declarator 中使用的表达式必须具有类型。因此,member-declarator 中的表达式为 null 或匿名函数会产生编译时错误。表达式具有不安全类型也是编译时错误。
匿名类型及其 Equals 方法的参数的名称由编译器自动生成,不能在程序文本中引用。
在同一程序内,如果两个匿名对象初始值设定项以相同顺序指定具有相同名称和编译时类型的属性的序列,则它们会生成相同匿名类型的实例。
在下面的示例中
var p1 = new { Name = "Lawnmower",
Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;
最后一行的赋值是允许的,原因是 p1 和 p2 属于同一匿名类型。
匿名类型的 Equals 和 GetHashcode 方法将重写从 object 继承的方法,并根据属性的 Equals 和 GetHashcode 进行定义,以便当且仅当同一匿名类型的两个实例的所有属性都相等时,该两个实例才相等。
成员声明符可以缩写为简单名称(第 7.5.2 节)、成员访问(第 7.5.4 节)或基访问(第 7.6.8 节)。这称为投影初始值设定项 (projection initializer),且为具有相同名称的属性的声明和赋值的简写形式。具体而言,以下形式的成员声明符
identifier expr . identifier
分别完全等效于以下内容:
identifer = identifier identifier = expr . identifier
因此,在投影初始值设定项中,identifier 选择值以及将值赋予的字段或属性。从直观上看,投影初始值设定项项目不仅是值,而且是值的名称。
1.6.11 typeof 运算符
typeof 运算符用于获取某种类型的 System.Type \b \b 对象。
typeof-expression:
typeof (
type )
typeof
(
unbound-type-name )
typeof ( void )
unbound-type-name:
identifier generic-dimension-specifieropt
identifier ::
identifier
generic-dimension-specifieropt
unbound-type-name .
identifier
generic-dimension-specifieropt
generic-dimension-specifier:
< commasopt >
commas:
,
commas ,
typeof-expression 的第一种形式由 typeof 关键字后接带括号的 type 组成。这种形式的表达式的结果是与给定的类型对应的 System.Type 对象。任何给定的类型都只有一个 System.Type 对象。这意味着对于类型 T,typeof(T) == typeof(T) 始终为 true。type 不能为 dynamic。
typeof-expression 的第二种形式由 typeof 关键字后接带括号的 unbound-type-name 组成。unbound-type-name 与 type-name(第 3.8 节)非常类似,不同的是,unbound-type-name 包含 generic-dimension-specifier,而 type-name 则包含 type-argument-list。当 typeof-expression 的操作数为一系列同时满足 unbound-type-name 和 type-name 语法的标记(即该操作数既不包含 generic-dimension-specifier 也不包含 type-argument-list)时,可将标记序列视为 type-name。unbound-type-name 的含义按下述步骤确定:
- 通过将每个 generic-dimension-specifier 替换为与每个 type-argument-list 具有相同数目的逗号和关键字 object 的 type-argument,从而将标记序列转换为 type-name。
- 计算结果 type-name,同时忽略所有类型形参约束。
- unbound-type-name 解析为与结果构造类型关联的未绑定的泛型类型(第 4.4.3 节)。
typeof-expression 的结果是所产生的未绑定泛型类型的 System.Type 对象。
typeof-expression 的第三种形式由 typeof 关键字后接带括号的 void 关键字组成。这种形式的表达式的结果是一个表示“类型不存在”的 System.Type 对象。这种通过 typeof(void) 返回的类型对象与为任何类型返回的类型对象截然不同。这种特殊的类型对象在这样的类库中很有用:它允许在源语言中能仔细考虑一些方法,希望有一种方式以用 System.Type 的实例来表示任何方法(包括 void 方法)的返回类型。
typeof 运算符可以在类型形参上使用。结果为绑定到该类型形参的运行时类型的 System.Type 对象。typeof 运算符还可以在构造类型或未绑定的泛型类型上使用(第 4.4.3 节)。未绑定的泛型类型的 System.Type 对象与实例类型的 System.Type 对象不同。实例类型在运行时始终为封闭构造类型,因此其 System.Type 对象依赖于使用的运行时类型参数,而未绑定泛型类型没有类型参数。
下面的示例
using System;
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>) 的结果则依赖。
1.6.12 checked 和 unchecked 运算符
checked 和 unchecked 运算符用于控制整型算术运算和转换的溢出检查上下文。
checked-expression:
checked
(
expression )
unchecked-expression:
unchecked
(
expression )
checked 运算符在 checked 上下文中计算所包含的表达式,unchecked 运算符在 unchecked 上下文中计算所包含的表达式。除了在给定的溢出检查上下文中计算所包含的表达式外,checked-expression 或 unchecked-expression 表达式与 parenthesized-expression(第 7.6.3 节)完全对应。
也可以通过 checked 和 unchecked 语句(第 8.11 节)控制溢出检查上下文。
下列运算受由 checked 和 unchecked 运算符和语句所确定的溢出检查上下文影响:
- 预定义的 ++ 和 -- 一元运算符(第 7.6.9 节和第 7.7.5 节)(当操作数为整型时)。
- 预定义的 - 一元运算符(第 7.7.2 节)(当操作数为整型时)。
- 预定义的 +、-、* 和 / 二元运算符(第 7.8 节)(当两个操作数均为整型时)。
- 从一个整型到另一个整型或从 float 或 double 到整型的显式数值转换(第 6.2.1 节)。
当上面的运算之一产生的结果太大,无法用目标类型表示时,执行运算的上下文控制由此引起的行为:
- 在 checked 上下文中,如果运算发生在一个常量表达式(第 7.19 节)中,则发生编译时错误。否则,当在运行时执行运算时,引发 System.OverflowException。
- 在 unchecked 上下文中,计算的结果被截断,放弃不适合目标类型的任何高序位。
对于不用任何 checked 或 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.13 默认值表达式
默认值表达式用于获取某个类型的默认值(第 5.2 节)。通常,默认值表达式用于类型参数,因为可能并不知道类型参数是值类型还是引用类型。(不存在从 null 文本到类型参数的转换,除非类型参数已知为引用类型。)
default-value-expression:
default
(
type )
如果 default-value-expression 中的 type 在运行时计算为引用类型,则结果将为转换为该类型的 null。如果 default-value-expression 中的 type 在运行时计算为值类型,则结果为 value-type 的默认值(第 4.1.2 节)。
如果类型为引用类型或已知为引用类型(第 7.19 节)的类型参数,则 default-value-expression 为常量表达式(第 10.1.5 节)。此外,如果类型为以下值类型之一,则 default-value-expression 也为常量表达式:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool 或任何枚举类型。
1.6.14 匿名方法表达式
anonymous-method-expression 是用于定义匿名函数的两种方式之一。有关这些内容的进一步介绍详见第 7.15 节。
1.7 一元运算符
+、-、!、~、++、--、cast 和 await 运算符被称为一元运算符。
unary-expression:
primary-expression
+ unary-expression
- unary-expression
! unary-expression
~ unary-expression
pre-increment-expression
pre-decrement-expression
cast-expression
await-expression
如果 unary-expression 的操作数具有编译时类型 dynamic,则它是动态绑定的(第 7.2.2 节)。在此情况下,unary-expression 的编译时类型为 dynamic,并且会在运行时使用操作数的运行时类型进行下面所述的决策。
1.7.1 一元加运算符
对于 +x 形式的运算,应用一元运算符重载决策(第 7.3.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.7.2 一元减运算符
对于 –x 形式的运算,应用一元运算符重载决策(第 7.3.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.7.3 逻辑否定运算符
对于 !x 形式的运算,应用一元运算符重载决策(第 7.3.3 节)以选择特定的运算符实现。操作数转换为所选运算符的参数类型,结果类型是该运算符的返回类型。只存在一个预定义的逻辑否定运算符:
bool operator !(bool x);
此运算符计算操作数的逻辑否定:如果操作数为 true,则结果为 false。如果操作数为 false,则结果为 true。
1.7.4 按位求补运算符
对于 ~x 形式的运算,应用一元运算符重载决策(第 7.3.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) 的结果相同,但始终执行到 E 的转换,如同在 unchecked 上下文(第 7.6.12 节)中一样。
1.7.5 前缀增量和减量运算符
pre-increment-expression:
++ unary-expression
pre-decrement-expression:
-- unary-expression
前缀增量或减量运算的操作数必须是属于变量、属性访问或索引器访问类别的表达式。该运算的结果是与操作数类型相同的值。
如果前缀增量或减量运算的操作数是属性或索引器访问,则属性或索引器必须同时具有 get 和 set 访问器。如果不是这样,则发生绑定时错误。
将使用一元运算符重载决策(第 7.3.3 节)选择特定的运算符实现。以下类型存在预定义的 ++ 和 -- 运算符:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 以及任何枚举类型。预定义 ++ 运算符返回的结果值为操作数加上 1,预定义 -- 运算符返回的结果值为操作数减去 1。在 checked 上下文中,如果此加法或减法运算的结果在结果类型的范围之外,且结果类型为整型或枚举类型,则会引发 System.OverflowException。
++x 或 --x 形式的前缀增量或前缀减量运算的运行时处理包括以下步骤:
- 如果 x 属于变量:
- 计算 x 以产生变量。
- 调用选定的运算符,将 x 的值作为参数。
- 运算符返回的值存储在由 x 的计算结果给定的位置中。
- 运算符返回的值成为该运算的结果。
- 如果 x 属于属性或索引器访问:
- 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),结果用于后面的对 get 和 set 访问器调用。
- 调用 x 的 get 访问器。
- 调用选定的运算符,将 get 访问器返回的值作为参数。
- 调用 x 的 set 访问器,将运算符返回的值作为其 value 参数。
- 运算符返回的值成为该运算的结果。
++ 和 -- 运算符也支持后缀表示法(第 7.6.9 节)。通常,x++ 或 x-- 的结果是运算“之前”x 的值,而 ++x 或 --x 的结果是运算“之后”x 的值。在任何一种情况下,运算后 x 本身都具有相同的值。
operator ++ 或 operator -- 的实现既可以用后缀表示法调用,也可以用前缀表示法调用。但是,不能让这两种表示法分别去调用该运算符的不同的实现。
1.7.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.7 Await 表达式
await 运算符用于挂起封闭的异步函数的计算,直到由操作数表示的异步操作已完成。
await-expression:
await
unary-expression
await-expression 只允许在异步函数(第 10.14 节)体中使用。在最近的封闭异步函数内,await-expression 不能出现在以下位置:
- 嵌套(非异步)匿名函数内部
- try-statement 的 catch 或 finally 块中
- lock-statement 块内部
- 不安全上下文中
请注意,await-expression 不能出现在 query-expression 内的大多数位置,因为这些位置将在语法上转换为使用非异步 lambda 表达式。
在异步函数内部,await 不能用作标识符。因此,await 表达式和涉及标识符的各种表达式之间不存在语法多义性。在异步函数外部,await 充当正常标识符。
await-expression 的操作数称为任务。它表示不一定在计算 await-expression 时完成的异步操作。await 运算符的用途是挂起封闭式异步函数的执行,直到等待的任务完成,然后获取其结果。
1.7.7.1 可等待的表达式
await 表达式任务必须是可等待的。如果存在以下情况之一,则表达式 t 是可等待的:
- t is of compile time type dynamic
- t has an accessible instance or extension method called GetAwaiter with no parameters and no type parameters, and a return type A for which all of the following hold:
- A 实现了接口 System.Runtime.CompilerServices.INotifyCompletion(为简洁起见,以下简称为 INotifyCompletion)
- A 具有一个可访问的可读实例属性 IsCompleted,其类型为 bool
- A 具有一个不带参数和类型参数的可访问实例方法 GetResult
GetAwaiter 方法的用途是获取任务的 awaiter。类型 A 称为 await 表达式的 awaiter 类型。
IsCompleted 属性的用途是确定任务是否已完成。如果已完成,则无需挂起计算。
INotifyCompletion.OnCompleted 方法的用途是向任务注册“继续符”;即类型为 System.Action 的委托,任务完成时将调用该委托。
GetResult 方法的用途是获取任务完成后的结果。此结果可能会是“成功完成”,并可能带有结果值,也可能是一个异常(由 GetResult 方法引发)。
1.7.7.2 await 表达式的分类
表达式 await t 的分类方式与表达式 (t).GetAwaiter().GetResult() 相同。因此,如果 GetResult 的返回类型是 void,则可将 await-expression 归类为 Nothing。如果它具有非 void 返回类型 T,则可将 await-expression 归类为 T 类型的值。
1.7.7.3 await 表达式的运行时计算
在运行时,按以下方式计算表达式 await t:
- 通过计算表达式 (t).GetAwaiter() 来获取 awaiter a。
- 通过计算表达式 (a).IsCompleted 来获取 bool b。
- 如果 b 为 false,则计算将取决于 a 是否实现了接口 System.Runtime.CompilerServices.ICriticalNotifyCompletion(为简洁起见,以下简称 ICriticalNotifyCompletion)。此检查将在绑定时完成;即如果 a 具有编译时类型 dynamic,则在运行时完成,否则在编译时完成。让 r 表示恢复委托(第 10.14 节):
- 如果 a 未实现 ICriticalNotifyCompletion,则将计算表达式
((a) as INotifyCompletion).OnCompleted(r)。 - 如果 a 实现了 ICriticalNotifyCompletion,则将计算表达式
((a) as ICriticalNotifyCompletion).UnsafeOnCompleted(r)。 - 然后将挂起计算,并将控制返回给异步函数的当前调用方。
- 随后立即(如果 b 为 true)或在以后调用恢复委托时(如果 b 为 false)对表达式 (a).GetResult() 进行计算。如果它返回值,则该值是 await 表达式的结果。否则,结果为 Nothing。
接口方法 INotifyCompletion.OnCompleted 和 ICriticalNotifyCompletion.UnsafeOnCompleted 的 awaiter 实现应导致最多调用一次委托 r。否则,封闭式异步函数的行为将不确定。
1.8 算术运算符
*、/、%、+ 和 – 运算符称为算术运算符。
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,则表达式是动态绑定的(第 7.2.2 节)。在此情况下,表达式的编译时类型为 dynamic,并且会在运行时使用具有编译时类型 dynamic 的操作数的运行时类型进行下面所述的决策。
1.8.1 乘法运算符
对于 x * y 形式的运算,应用二元运算符重载决策(第 7.3.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.8.2 除法运算符
对于 x / y 形式的运算,应用二元运算符重载决策(第 7.3.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.8.3 余数运算符
对于 x % y 形式的运算,应用二元运算符重载决策(第 7.3.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.8.4 加法运算符
对于 x + y 形式的运算,应用二元运算符重载决策(第 7.3.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 的绝对值相同但符号相反,则 zz 为正零。如果 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);
这些二元 + 运算符的重载执行字符串串连。在字符串串联运算中,如果它的一个操作数为 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+15F;
Console.WriteLine("f = " +
f); // displays f = 1.23E+15
decimal d = 2.900m;
Console.WriteLine("d = " +
d); // displays d = 2.900
}
}
字符串串联运算符的结果是一个字符串,由左操作数的字符后接右操作数的字符组成。字符串串联运算符从不返回 null 值。如果没有足够的内存可用于分配得到的字符串,则可能引发 System.OutOfMemoryException。
- 委托组合。每个委托类型都隐式提供以下预定义运算符,其中 D 是委托类型:
D operator +(D x, D y);
当两个操作数均为某个委托类型 D 时,二元 + 运算符执行委托组合。(如果操作数具有不同的委托类型,则发生绑定时错误。)如果第一个操作数为 null,则运算结果为第二个操作数的值(即使此操作数也为 null)。否则,如果第二个操作数为 null,则运算结果为第一个操作数的值。否则,运算结果是一个新委托实例,该实例在被调用时调用第一个操作数,然后调用第二个操作数。有关委托组合的示例,请参见第 7.8.5 节和第 15.4 节。由于 System.Delegate 不是委托类型,因此不为它定义 operator +。
1.8.5 减法运算符
对于 x – y 形式的运算,应用二元运算符重载决策(第 7.3.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.10.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.9 移位运算符
<< 和 >> 运算符用于执行移位运算。
shift-expression:
additive-expression
shift-expression << additive-expression
shift-expression right-shift additive-expression
如果 shift-expression 的某个操作数具有编译时类型 dynamic,则表达式是动态绑定的(第 7.2.2 节)。在此情况下,表达式的编译时类型为 dynamic,并且会在运行时使用具有编译时类型 dynamic 的操作数的运行时类型进行下面所述的决策。
对于 x << count 或 x >> count 形式的运算,应用二元运算符重载决策(第 7.3.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 & 0x1F 计算出。
- 当 x 的类型为 long 或 ulong 时,位移计数由 count 的低序的 6 位给出。换言之,位移计数由 count & 0x3F 计算出。
如果计算位移计数的结果为零,则移位运算符只返回 x 的值。
移位运算从不会导致溢出,并且在 checked 和 unchecked 上下文中产生的结果相同。
当 >> 运算符的左操作数为有符号的整型时,该运算符执行算术右移位,在此过程中,操作数的最有效位(符号位)的值扩展到高序空位位置。当 >> 运算符的左操作数为无符号的整型时,该运算符执行逻辑右移位,在此过程中,高序空位位置总是设置为零。若要执行与由操作数类型确定的不同的移位运算,可以使用显式强制转换。例如,如果 x 是 int 类型的变量,则 unchecked((int)((uint)x >> y)) 运算执行 x 的逻辑右移位。
1.10 关系和类型测试运算符
==、!=、<、>、<=、>=、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.10.10 节和第 7.10.11 节中说明。
==、!=、<、>、<= 和 >= 运算符为比较运算符 (comparison operator)。
如果比较运算符的某个操作数为编译时类型 dynamic,则表达式是动态绑定的(第 7.2.2 节)。在此情况下,表达式的编译时类型为 dynamic,并且会在运行时使用具有编译时类型 dynamic 的操作数的运行时类型进行下面所述的决策。
对于 x op y 形式(其中 op 为比较运算符)的运算,应用重载决策(第 7.3.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.10.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.10.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 标准法则比较操作数:
- 如果两个操作数中的任何一个为 NaNN,则对于除 !=(对于此运算符,结果为 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 是可以用给定浮点格式表示的最小和最大正有限值。这样排序的显著特点是:
- 负零和正零被视为相等。
- 负无穷大被视为小于所有其他值,但等于其他负无穷大。
- 正无穷大被视为大于所有其他值,但等于其他正无穷大。
1.10.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);
这些运算符都比较两个 decimal 操作数的数值并返回一个 bool 值,该值指示特定的关系是 true 还是 false。各小数比较等效于使用 System.Decimal 类型的相应关系运算符或相等运算符。
1.10.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.10.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 是一个比较运算符)的计算结果与 (E)((U)x) op ((U)y) 的计算结果完全相同。换言之,枚举类型比较运算符只比较两个操作数的基础整数值。
1.10.6 引用类型相等运算符
预定义的引用类型相等运算符为:
bool operator ==(object x, object y);
bool operator !=(object x, object y);
这些运算符返回两个引用是相等还是不相等的比较结果。
由于预定义的引用类型相等运算符接受 object 类型的操作数,因此它们适用于所有那些没有为自己声明适用的 operator == 和 operator != 成员的类型。相反,任何适用的用户定义的相等运算符都有效地隐藏上述预定义的引用类型相等运算符。
预定义的引用类型相等运算符要求满足以下条件之一:
- 两个操作数均为已知的 reference-type 类型的值或文本 null。此外,存在从其中一个操作数的类型到另一个操作数的类型的显式引用转换(第 6.2.4 节)。
- 一个操作数是类型为 T 的值,其中 T 为 type-parameter,另一个操作数为文本 null。此外,T 不具有值类型约束。
除非满足以下这些条件之一,否则将发生绑定时错误。这些规则中值得注意的含义是:
- 使用预定义的引用类型相等运算符比较两个在绑定时已能确定是不相同的引用时,会导致绑定时错误。例如,如果操作数的绑定时类型是两种类类型 A 和 B,并且如果 A 和 B 都不是从对方派生的,则两个操作数不可能引用同一对象。因此,此运算被认为是绑定时错误。
- 预定义的引用类型相等运算符不允许比较值类型操作数。因此,除非结构类型声明自己的相等运算符,否则不可能比较该结构类型的值。
- 预定义的引用类型相等运算符从不会导致对它们的操作数执行装箱操作。执行此类装箱操作毫无意义,这是因为对新分配的已装箱实例的引用必将不同于所有其他引用。
- 如果将类型参数类型 T 的操作数与 null进行比较,并且 T 的运行时类型为值类型,则比较结果为 false。
下面的示例检查未受约束的类型形参类型的实参是否为 null。
class
C<T>
{
void F(T x) {
if (x == null) throw new
ArgumentNullException();
...
}
}
虽然 T 可能表示值类型,但是 x == null 构造是允许的,当 T 为值类型时,结果只是被定义为 false。
对于 x == y 或 x != y 形式的运算,如果存在任何适用的 operator == 或 operator !=,则运算符重载决策(第 7.3.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.10.7 节)。其余的比较全都输出 False,这是因为是在一个或两个操作数为 object 类型时选定预定义的引用类型相等运算符。
注意,以上技术对值类型没有意义。下面的示例
class Test
{
static void Main() {
int i = 123;
int j = 123;
System.Console.WriteLine((object)i ==
(object)j);
}
}
输出 False,这是因为强制转换创建对已装箱 int 值的两个单独实例的引用。
1.10.7 字符串相等运算符
预定义的字符串相等运算符为:
bool operator ==(string x, string y);
bool operator !=(string x, string y);
当下列条件中有一个为真时,两个 string 值被视为相等:
- 两个值都为 null。
- 两个值都是对字符串实例的非空引用,这两个字符串不仅具有相同的长度,而且在每个字符位置上的字符亦都彼此相同。
字符串相等运算符比较的是字符串值而不是对字符串的引用。当两个单独的字符串实例包含完全相同的字符序列时,字符串的值相等,但引用不相同。正如第 7.10.6 节中所描述的那样,引用类型相等运算符可用于比较字符串引用而不是字符串值。
1.10.8 委托相等运算符
预定义的委托相等运算符为:
bool operator ==(System.Delegate x, System.Delegate y);
bool operator !=(System.Delegate x, System.Delegate y);
两个委托实例按下面这样被视为相等:
- 如果两个委托实例中有一个为 null,则当且仅当它们都为 null 时相等。
- 具有不同运行时类型的委托永远不相等。
- 如果两个委托实例都具有调用列表(第 15.1) 节),则当且仅当它们的调用列表长度相同,并且一个实例的调用列表中的每项依次等于(如下面的定义)另一个的调用列表中的相应项时,这两个委托实例相等。
以下规则控制调用列表项的相等性:
- 如果两个调用列表项都引用同一静态方法,则这两项相等。
- 如果两个调用列表项都引用同一个目标对象(引用相等运算符定义的目标对象)上的同一个非静态方法,则这两个调用列表项相等。
- 允许(但不要求)具有相同被捕获外层变量实例集(可能为空集)且语义上相同的 anonymous-function-expression 计算生成的调用列表项相等。
1.10.9 相等运算符和 null
== 和 != 运算符允许一个操作数是可为 null 的类型的值,另一个是 null 文本,即使运算中不存在预定义或用户定义的运算符(未提升或提升形式)。
对于下面某个形式的操作
x == null null == x x != null null != x
其中 x 是可为 null 的类型的表达式,如果运算符重载决策(第 7.2.4 节)未能找到适用的运算符,则改为从 x 的 HasValue 属性计算结果。具体而言,前两种形式将转换为 !x.HasValue,后两种形式将转换为 x.HasValue。
1.10.10 is 运算符
is 运算符用于动态检查对象的运行时类型是否与给定类型兼容。E is T 运算(其中 E 为表达式,T 为类型)的结果是布尔值,表示 E 的类型是否可通过引用转换、装箱转换或取消装箱转换而成功转换为类型 T。使用类型实参替换了所有类型形参后,按如下方式计算该运算:
- 如果 E 是匿名函数,将发生编译时错误
- 如果 E 是方法组或 null 文本,或者如果 E 的类型是引用类型或可为 null 的类型并且 E 的值为 null,则结果为 false。
- 否则,根据下列规则让 D 表示 E 的动态类型:
- 如果 E 的类型为引用类型,则 D 为 E 引用的实例的运行时类型。
- 如果 E 的类型为可以为 null 的类型,则 D 为该可以为 null 的类型的基础类型。
- 如果 E 的类型为不可以为 null 值的类型,则 D 为 E 的类型。
- 该操作的结果取决于 D 和 T,具体如下:
- 如果 T 为引用类型,那么,在以下情况下结果为 true:D 和 T 为相同类型,或者 D 为引用类型并且存在从 D 到 T 的隐式引用转换,或者 D 为值类型并且存在从 D 到 T 的装箱转换。
- 如果 T 为可以为 null 的类型,那么,当 D 为 T 的基础类型时结果为 true。
- 如果 T 为不可以为 null 值的类型,那么,如果 D 和 T 为相同类型,则结果为 true。
- 否则,结果为 false。
请注意,用户定义的转换不在 is 运算符考虑之列。
1.10.11 as 运算符
as 运算符用于将一个值显式转换为一个给定的引用类型或可为 null 的类型。与强制转换表达式(第 7.7.6 节)不同,as 运算符从不引发异常。它采用的是:如果指定的转换不可能实施,则运算结果为 null。
在 E as T 形式的操作中,E 必须为表达式,T 必须为引用类型、已知为引用类型的类型参数或可以为 null 的类型。此外,下列条件中必须至少有一条成立,否则会发生编译时错误:
- 存在从 E 到 T 的以下类型转换:标识(第 6.1.1 节)、隐式可以为 null(第 6.1.4 节)、隐式引用(第 6.1.6 节)、装箱(第 6.1.7 节)、显式可以为 null(第 6.2.3 节)、显式引用(第 6.2.4 节)或取消装箱(第 6.2.5 节)转换。
- E 或 T 的类型为开放类型。
- E 为 null 文本。
如果 E 的编译时类型不是 dynamic,则运算 E as T 将生成与下面的计算相同的结果
E is T ? (T)(E) : (T)null
不同的只是:实际执行中 E 只计算一次。编译器应该优化 E as T 以最多执行一次动态类型检查,而不是上面的扩展隐含的两次动态类型检查。
如果 E 的编译时类型为 dynamic,则与强制转换运算符不同,as 运算符不是动态绑定的(第 7.2.2 节)。因此这种情况下的扩展为:
E is T ? (T)(object)(E) : (T)null
请注意,不能使用 as 运算符执行某些转换(如用户定义的转换),应改为使用强制转换表达式来执行这些转换。
在下面的示例中
class X
{
public
string F(object o) {
return o as string; // OK, string is a reference type
}
public
T G<T>(object o) where T: Attribute {
return o as T; // Ok, T has a class constraint
}
public
U H<U>(object o) {
return o as U; // Error, U is unconstrained
}
}
G 的类型参数 T 已知为引用类型,原因是它有类约束。但 H 的类型参数 U 不是;因此,不允许在 H 中使用 as 运算符。
1.11 逻辑运算符
&、^ 和 | 运算符称为逻辑运算符。
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,则表达式是动态绑定的(第 7.2.2 节)。在此情况下,表达式的编译时类型为 dynamic,并且会在运行时使用具有编译时类型 dynamic 的操作数的运行时类型进行下面所述的决策。
对于 x op y 形式的运算(其中 op 为一个逻辑运算符),应用重载决策(第 7.3.4 节)以选择一个特定的运算符实现。操作数转换为所选运算符的参数类型,结果的类型是该运算符的返回类型。
下列章节介绍了预定义的逻辑运算符。
1.11.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.11.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.11.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.4 可以为 null 的布尔逻辑运算符
可以为 null 的布尔类型 bool? 可表示三个值 true、false 和 null,并且在概念上类似于 SQL 中的布尔表达式的三值类型。为了确保针对 bool? 操作数的 & 和 | 运算符产生的结果与 SQL 的三值逻辑一致,提供了下列预定义运算符:
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
下表列出了这些运算符对 true、false 和 null 值的所有组合所产生的结果。
x |
y |
x & y |
x | y |
true |
true |
true |
true |
true |
false |
false |
true |
true |
null |
null |
true |
false |
true |
false |
true |
false |
false |
false |
false |
false |
null |
false |
null |
null |
true |
null |
true |
null |
false |
false |
null |
null |
null |
null |
null |
1.12 条件逻辑运算符
&& 和 || 运算符称为条件逻辑运算符。也称为“短路”逻辑运算符。
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 不为 false 时才计算 y。
- x || y 运算对应于 x | y 运算,但仅当 x 不为 true 时才计算 y。
如果条件逻辑运算符的某个操作数具有编译时类型 dynamic,则表达式是动态绑定的(第 7.2.2 节)。在此情况下,表达式的编译时类型为 dynamic,并且会在运行时使用具有编译时类型 dynamic 的操作数的运行时类型进行下面所述的决策。
x && y 或 x || y 形式的运算通过应用重载决策(第 7.3.4 节)来处理,就好比运算的书写形式为 x & y 或 x | y。然后,
- 如果重载决策未能找到单个最佳运算符,或者重载决策选择一个预定义的整数逻辑运算符,则发生绑定时错误。
- 否则,如果选定的运算符是一个预定义的布尔逻辑运算符(第 7.11.3 节)或可以为 null 的布尔逻辑运算符(第 7.11.4 节),则运算按第 7.12.1 节中所描述的那样进行处理。
- 否则,选定的运算符为用户定义的运算符,且运算按第 7.12.2 节中所描述的那样进行处理。
不可能直接重载条件逻辑运算符。不过,由于条件逻辑运算符按通常的逻辑运算符计算,因此通常的逻辑运算符的重载,在某些限制条件下,也被视为条件逻辑运算符的重载。第 7.12.2 节对此有进一步描述。
1.12.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.12.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 是否肯定为 false。如果 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 是否肯定为 true。然后,如果 x 肯定为真,则运算结果为先前为 x 计算的值。否则将计算 y,并对先前为 x 计算的值和为 y 计算的值调用选定的 operator | 以产生运算结果。
在这两个运算中,x 给定的表达式只计算一次,y 给定的表达式要么不计算,要么只计算一次。
有关实现了 operator true 和 operator false 的类型的示例,请参见第 11.4.2 节。
1.13 空合并运算符
?? 运算符称为空合并运算符。
null-coalescing-expression:
conditional-or-expression
conditional-or-expression ?? null-coalescing-expression
a ?? b 形式的空合并表达式要求 a 为可以为 null 的类型或引用类型。如果 a 为非 null,则 a ?? b 的结果为 a;否则,结果为 b。仅当 a 为 null 时,该操作才计算 b。
空合并运算符为右结合运算符,表示操作从右向左进行组合。例如,a ?? b ?? c 形式的表达式可以按 a ?? (b ?? c) 进行计算。概括地说,E1 ?? E2 ?? ... ?? EN 形式的表达式返回第一个非 null 的操作数,如果所有操作数都为 null,则返回 null。
表达式 a ?? b 的类型取决于对操作数可用的隐式转换。按照优先顺序,a ?? b 的类型为 A0、A 或 B,其中 A 是 a 的类型(如果 a 有类型),B 是 b 的类型(如果 b 有类型),A0 是 A 的基础类型(如果 A 是可以为 null 的类型)或 A(如果该项不是可以为 null 的类型)。具体而言,a ?? b 的处理过程如下:
- 如果 A 存在并且不是可以为 null 的类型或引用类型,将发生编译时错误。
- 如果 b 是动态表达式,则结果类型为 dynamic。在运行时,首先计算 a。如果 a 不为 null,则 aa 转换为动态类型,这成为结果。否则,计算 b,这成为结果。
- 否则,如果 A 存在并且是可以为 null 的类型,并且存在从 b 到 A0 的隐式转换,则结果类型为 A0。在运行时,首先计算 a。如果 a 不为 null,则 a 解包为类型 A0,这即是结果。否则,计算 b 并转换为类型 A0,这即是结果。
- 否则,如果 A 存在并且存在从 b 到 A 的隐式转换,则结果类型为 A。在运行时,首先计算 a。如果 a 不为 null,则 a 即是结果。否则,计算 b 并转换为类型 A,这即是结果。
- 否则,如果 A 存在并且是可为 null 的类型,b 具有类型 B 并且存在从 A0 到 B 的隐式转换,则结果类型为 B。在运行时,首先计算 a。如果 a 不为 null,则 a 将解包为类型 A0 并且转换为类型 B,这即是结果。否则,计算 b 并且 b 作为结果。
- 否则,如果 b 的类型为 B,并且存在从 a 到 B 的隐式转换,则结果类型为 B。在运行时,首先计算 a。如果 a 不为 null,则 a 将转换为类型 B,这即是结果。否则,计算 b 并且 b 作为结果。
- 否则,a 和 b 不兼容,并发生编译时错误。
1.14 条件运算符
?: 运算符称为条件运算符。有时,它也称为三元运算符。
conditional-expression:
null-coalescing-expression
null-coalescing-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 具有类型 X 且 y 具有类型 Y,则
- 如果存在从 X 到 Y 的隐式转换(第 6.1 节),但不存在从 Y 到 X 的隐式转换,则 Y 为条件表达式的类型。
- 如果存在从 Y 到 X 的隐式转换(第 6.1 节),但不存在从 X 到 Y 的隐式转换,则 X 为条件表达式的类型。
- 否则,无法确定条件表达式的类型,会发生编译时错误。
- 如果 x 和 y 中只有一个具有类型,并且 x 和 y 都可隐式转换为该类型,则该类型为条件表达式的类型。
- 否则,无法确定条件表达式的类型,会发生编译时错误。
b ? x : y 形式的条件表达式的运行时处理包括以下步骤:
- 首先,计算 b,并确定 b 的 bool 值:
- 如果存在从 b 的类型到 bool 的隐式转换,则执行该隐式转换以产生 bool 值。
- 否则,将调用由 b 的类型定义的 operator true 以生成 bool 值。
- 如果以上步骤产生的 bool 值为 true,则计算 x 并将其转换为条件表达式的类型,且这成为条件表达式的结果。
- 否则,计算 y 并将其转换为条件表达式的类型,且这成为条件表达式的结果。
1.15 匿名函数表达式
匿名函数 (anonymous function) 是表示“内联”方法定义的表达式。匿名函数本身及其内部没有值或类型,但可以转换为兼容委托或表达式树类型。匿名函数转换的计算取决于转换的目标类型:如果是委托类型,则转换计算为引用匿名函数所定义的方法的委托值。如果目标类型为表达式目录树类型,则转换将计算以对象结构形式表示方法结构的表达式目录树。
由于历史原因,有两种匿名函数句法风格,即 lambda-expression 和 anonymous-method-expression。对于几乎所有用途,lambda-expression 都比 anonymous-method-expression 更为简洁且更具表现力,但语言中仍保留后者以便向后兼容。
lambda-expression:
asyncopt
anonymous-function-signature =>
anonymous-function-body
anonymous-method-expression:
asyncopt
delegate
explicit-anonymous-function-signatureopt block
anonymous-function-signature:
explicit-anonymous-function-signature
implicit-anonymous-function-signature
explicit-anonymous-function-signature:
(
explicit-anonymous-function-parameter-listopt )
explicit-anonymous-function-parameter-list:
explicit-anonymous-function-parameter
explicit-anonymous-function-parameter-list
,
explicit-anonymous-function-parameter
explicit-anonymous-function-parameter:
anonymous-function-parameter-modifieropt type
identifier
anonymous-function-parameter-modifier:
ref
out
implicit-anonymous-function-signature:
( implicit-anonymous-function-parameter-listopt )
implicit-anonymous-function-parameter
implicit-anonymous-function-parameter-list:
implicit-anonymous-function-parameter
implicit-anonymous-function-parameter-list
,
implicit-anonymous-function-parameter
implicit-anonymous-function-parameter:
identifier
anonymous-function-body:
expression
block
=> 运算符与赋值 (=) 运算符优先级相同,并且向右关联。
具有 async 修饰符的匿名函数是一种异步函数,并遵循第 10.14 节中描述的规则。
lambda-expression 形式的匿名函数的参数可以显式或隐式类型化。在显式类型化参数列表中,每个参数的类型是显式声明的。在隐式类型化参数列表中,参数的类型是从匿名函数出现的上下文中推断的,具体而言,当匿名函数转换为兼容委托类型或表达式目录树类型时,该类型提供参数类型(第 6.5 节)。
在具有一个隐式类型化参数的匿名函数中,参数列表中可以省略括号。换言之,具有以下形式的匿名函数
( param ) => expr
可以简写为
param => expr
anonymous-method-expression 形式的匿名函数的参数列表是可选的。如果提供了参数,则参数必须显式类型化。如果未给出参数,则匿名函数可以转换为带有不含 out 参数的参数列表的委托。
除非匿名函数出现在不可到达的语句内,否则该匿名函数的 block 是可到达的(第 8.1 节)。
下面是一些匿名函数示例:
x => x + 1 // Implicitly typed, expression body
x => { return x + 1; } // Implicitly typed, statement body
(int x) => x + 1 // Explicitly typed, expression body
(int x) => { return x + 1; } // Explicitly typed, statement body
(x, y) => x * y // Multiple parameters
() => Console.WriteLine() // No parameters
async (t1,t2) => await t1 + await t2 // Async
delegate (int x) { return x + 1; } // Anonymous method expression
delegate { return 1 + 1; } // Parameter list omitted
lambda-expression 和 anonymous-method-expression 的行为除以下几点外是相同的:
- anonymous-method-expression 允许完全省略参数列表,从而可转换为具有任意值参数列表的委托类型。
- lambda-expression 允许省略和推断参数类型,而 anonymous-method-expression 要求显式声明参数类型。
- lambda-expression 的主体可以为表达式或语句块,而 anonymous-method-expression 的主体必须为语句块。
- 只有 lambda-expression 可具有到兼容的表达式树类型(第 4.6 节)的转换。
1.15.1 匿名函数签名
匿名函数的可选 anonymous-function-signature 定义匿名参数的形参的名称和类型(可选)。匿名函数的参数的范围是 anonymous-function-body。(第 3.7 节)anonymous-method-body 与参数列表(如果提供了)一起构成声明空间(第 3.3 节)。因此,如果匿名函数的某一参数的名称与范围包括 anonymous-method-expression 或 lambda-expression 的局部变量、局部常量或参数的名称匹配,则产生编译时错误。
如果匿名函数有 explicit-anonymous-function-signature,则兼容委托类型和表达式目录树类型的集合限制为具有相同顺序的相同参数类型和修饰符。与方法组转换(第 6.6 节)不同,匿名函数参数类型的逆变不受支持。如果匿名函数没有 anonymous-function-signature,则兼容的委托类型和表达式目录树类型的集将局限于那些没有 out 参数的委托类型和表达式目录树类型。
请注意,anonymous-function-signature 不能包含特性或参数数组。然而,anonymous-function-signature 可以与其参数列表包含参数数组的委托类型兼容。
另请注意,即使兼容,到表达式目录树类型的转换在编译时仍将失败(第 4.6 节)。
1.15.2 匿名函数体
匿名函数体(expression 或 block)遵循以下规则:
- 如果匿名函数包含签名,则可以在函数体中使用签名中指定的参数。如果匿名函数没有签名,则它可转换为带有参数的委托类型或表达式类型(第 6.5 节),但是这些参数在该函数体中不可访问。
- 除了在最近的封闭匿名函数签名(如果存在)中指定的 ref 或 out 参数外,该函数体访问其他 ref 或 out 参数将导致编译时错误。
- 当 this 的类型为结构类型时,该函数体访问 this 将导致编译时错误。无论访问是显式(如 this.x)还是隐式(如 x,其中 x 是该结构的实例成员),此规则都成立。此规则仅仅是禁止此类访问,但是不影响成员查找是否能找到该结构的成员。
- 函数体可以访问匿名函数的外层变量(第 7.15.5 节)。对某个外层变量的访问将引用该变量在计算 lambda-expression 或 anonymous-method-expression(第 7.15.6 节)时处于活动状态的实例。
- 该函数体包含目标在该函数体之外或该函数体之内所包含的匿名函数的 goto 语句、break 语句或 continue 语句时将导致编译时错误。
- 该函数体中的 return 语句从最近的封闭匿名函数调用中返回控制,而不是从封闭函数成员中返回。return 语句中指定的表达式必须与最近的封闭 lambda-expression 或 anonymous-method-expression 所转换为的委托类型或表达式目录树类型兼容(第 6.5 节)。
未显式指定除计算和调用 lambda-expression 或 anonymous-method-expression 以外,是否存在执行匿名函数块的任何其他方法。具体而言,编译器可选择通过合成一个或多个命名方法或类型来实现匿名函数。任何此类合成元素的名称都必须是为供编译器使用而保留的形式。
1.15.3 重载决策
参数列表中的匿名函数会参与类型推断和重载决策。有关确切规则,请参考第 7.5.2 节和第 7.5.3 节。
下面的示例演示匿名函数对重载决策的影响。
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;
...
}
void
ComputeSums() {
ItemList<Detail> orderDetails =
GetOrderDetails(...);
int totalUnits = orderDetails.Sum(d =>
d.UnitCount);
double orderTotal = orderDetails.Sum(d
=> d.UnitPrice * d.UnitCount);
...
}
在对 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 方法进行该调用。
1.15.4 匿名函数与动态绑定
匿名函数不能是动态绑定操作的接收器、参数或操作数。
1.15.5 外层变量
其范围包含 lambda-expression 或 anonymous-method-expression 的任何局部变量、值参数或参数数组称为匿名函数的外层变量 (outer variable)。在类的实例函数成员中,this 值被视为值参数,并且是该函数成员内包含的所有匿名函数的外层变量。
1.15.5.1 捕获的外层变量
某个外层变量由某个匿名函数引用时,称该外层变量已被该匿名函数捕获 (captured)。通常,局部变量的生存期仅限于该变量所关联的代码块或语句的执行期间(第 5.1.7 节)。但是,被捕获的外层变量的生存期将至少延长至从匿名函数创建的委托或表达式树可以被垃圾回收为止。
在下面的示例中
using System;
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
当局部变量或值参数由匿名函数捕获时,该局部变量或参数不再被视作固定变量(第 18.3 节),而是被视作可移动变量。因此,任何使用被捕获外层变量的地址的 unsafe 代码必须首先使用 fixed 语句固定该变量。
注意,与未捕获的变量不同,捕获的局部变量可以同时公开给多个执行线程。
1.15.5.2 局部变量实例化
当执行过程进入局部变量范围时,该变量视为被实例化 (instantiated)。例如,当调用下面的方法时,局部变量 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;
...
}
}
未捕获时,我们无法确切知道局部变量实例化的频率,因为实例化的生存期不是连续的,有可能每次实例化都只使用同一存储位置。但是,当匿名函数捕获到局部变量时,实例化的效果就会变得很明显。
下面的示例
using System;
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 的声明移到循环外时:
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;
}
输出为:
5
5
5
如果 for 循环声明了一个迭代变量,则该变量本身将被视为在该循环外部声明。因此,如果将该示例更改为捕获迭代变量本身:
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
result[i] = () => {
Console.WriteLine(i); };
}
return result;
}
则将仅捕获该迭代变量的一个实例,这会产生以下输出:
3
3
3
匿名函数委托可共享某些捕获的变量,但是又具有其他变量的不同实例。例如,如果 F 更改为
static D[] F() {
D[] result = new D[3];
int x = 0;
for (int i = 0; i < 3; i++) {
int y = 0;
result[i] = () => { Console.WriteLine("{0}
{1}", ++x, ++y); };
}
return result;
}
三个委托捕获相同的 x 实例,但捕获不同的 y 实例,输出为:
1 1
2 1
3 1
不同的匿名函数可捕获一个外层变量的同一个实例。在下面的示例中:
using System;
delegate void Setter(int value);
delegate int Getter();
class Test
{
static void Main() {
int x = 0;
Setter s = (int value) => { x =
value; };
Getter g = () => { return x; };
s(5);
Console.WriteLine(g());
s(10);
Console.WriteLine(g());
}
}
两个匿名函数捕获局部变量 x 的同一个实例,并且它们因此可通过该变量进行“通信”。该示例的输出为:
5
10
1.15.6 匿名函数表达式计算
匿名函数 F 必须始终直接或通过执行委托创建表达式 new D(F) 来转换为委托类型 D 或表达式目录树类型 E。此转换将确定匿名函数的结果,如第 6.5 节所述。
1.16 查询表达式
查询表达式 (query expression) 为查询提供一种类似于关系和分层查询语言(如 SQL 和 XQuery)的语言集成语法。
query-expression:
from-clause query-body
from-clause:
from typeopt identifier
in expression
query-body:
query-body-clausesopt
select-or-group-clause
query-continuationopt
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 typeopt identifier
in expression on expression equals expression
join-into-clause:
join typeopt identifier
in expression on expression equals expression into identifier
orderby-clause:
orderby orderings
orderings:
ordering
orderings , ordering
ordering:
expression ordering-directionopt
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 子句都是一个生成器,该生成器将引入一个包括序列 (sequence) 的元素的范围变量 (range variable)。每个 let 子句都会引入一个范围变量,以表示通过前一个范围变量计算的值。每个 where 子句都是一个筛选器,用于从结果中排除项。每个 join 子句都将指定的源序列键与其他序列的键进行比较,以产生匹配对。每个 orderby 子句都会根据指定的条件对各项进行重新排序。而最后的 select 或 group 子句根据范围变量来指定结果的表现形式。最后,可以使用 into 子句来“连接”查询,方法是将某一查询的结果视为后续查询的生成器。
1.16.1 查询表达式的多义性
查询表达式包含许多“上下文关键字”,即在给定的上下文中有特殊含义的标识符。具体而言,这些关键字包括:from、where、join、on、equals、into、let、orderby、ascending、descending、select、group 和 by。为避免在查询表达式中将这些标识符作为关键字或简单名称混合使用,从而造成多义性,所以当这些标识符出现在查询表达式内的任何位置时,都将它们视为关键字。
为此,查询表达式是以“from identifier”开头后接除“;”、“=”或“,”之外的任何标记的任何表达式。
为了将这些字词用作查询表达式中的标识符,可以为其加上前缀“@”(第 2.4.2 节)。
1.16.2 查询表达式转换
C# 语言不指定查询表达式的执行语义。而是将查询表达式转换为遵循查询表达式模式(第 7.16.3 节)的方法调用。具体而言,查询表达式将转换为对具有以下名称的方法的调用:Where、Select、SelectMany、Join、GroupJoin、OrderBy、OrderByDescending、ThenBy、ThenByDescending、GroupBy 和 Cast。这些方法应该有特定的签名和结果类型,如第 7.16.3 节所述。这些方法可以是所查询对象的实例方法或对象外部的扩展方法,它们实现查询的实际执行。
从查询表达式到方法调用的转换是一种句法映射,在执行任何类型绑定或重载决策之前发生。该转换可以保证在句法上正确,但不能保证生成语法正确的 C# 代码。转换查询表达式后,以常规方法调用的方式处理生成的方法调用,而这进而可能暴露错误,例如在方法不存在、参数类型错误或方法为泛型方法且类型推断失败这些情况下。
查询表达式的处理方式为:重复应用以下转换,直到不可能进一步缩减。转换按应用顺序列出:每一部分都假设前面部分的转换已彻底执行,一旦某个部分彻底执行,之后在同一查询表达式的处理过程中便不再重新访问该部分。
不允许对查询表达式中的范围变量进行赋值。但允许 C# 实现在某些时候可以不实施此限制,因为对于此处介绍的句法转换方案,有些时候可能根本无法实施此限制。
某些转换使用由 * 指示的透明标识符注入范围变量。透明标识符的特殊属性将在之后进一步讨论(请参见第 7.16.2.7 节)。
1.16.2.1 带继续符的 select 和 groupby 子句
带继续符的查询表达式
from … into x …
转换为
from x in ( from … ) …
后面几节中的转换假定查询中没有 into 延续部分。
下面的示例
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() })
1.16.2.2 显式范围变量类型
显式指定范围变量类型的 from 子句
from T x in e
转换为
from x in ( e ) . Cast < T > ( )
显式指定范围变量类型的 join 子句
join T x in e on k1 equals k2
转换为
join x in ( e ) . Cast < T > ( ) on k1 equals k2
以下部分的转换假定查询没有显式范围变量类型。
下面的示例
from Customer c in customers
where c.City == "London"
select c
转换为
from c in customers.Cast<Customer>()
where c.City == "London"
select c
其最终转换为
customers.
Cast<Customer>().
Where(c => c.City == "London")
显式范围变量类型对于查询实现非泛型 IEnumerable 接口的集合很有用,但对于实现泛型 IEnumerable<T> 接口的集合没什么用处。如果 customers 属于 ArrayList 类型,则在上面的示例中即会如此。
1.16.2.3 退化查询表达式
以下形式的查询表达式
from x in e select x
转换为
( e ) . Select ( x => x )
下面的示例
from c in customers
select c
转换为
customers.Select(c => c)
退化查询表达式是平常选择源的元素的查询表达式。后面的转换阶段会移除由其他转换步骤引入的退化查询,方法是用其源替换这些退化查询。然而,确保查询表达式的结果永不为源对象本身非常重要,因为这样会向查询的客户端暴露源的类型和标识符。因此,此步骤可通过在源上显式调用 Select 来保护直接以源代码形式写入的简并查询。然后,由 Select 实施者及其他查询操作员确保这些方法永远不会返回源对象本身。
1.16.2.4 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 子句且后接除 select 以外的任何子句的查询表达式
from x1 in e1
from x2 in e2
…
转换为
from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new
{ x1 , x2 } )
…
带有 let 子句的查询表达式
from x in e
let y = f
…
转换为
from * in ( e ) . Select ( x => new { x , y = f } )
…
带有 where 子句的查询表达式
from x in e
where f
…
转换为
from x in ( e ) . Where ( x => f )
…
带有 join 子句(不含 into)且后接 select 子句的查询表达式
from x1 in e1
join x2 in e2 on k1 equals k2
select v
转换为
( e1 ) . Join( e2 , x1 => k1 , x2 => k2 , ( x1 , x2 ) => v )
带有 join 子句(不含 into)且后接除 select 子句之外的其他内容的查询表达式
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)且后接除 select 子句之外的其他内容的查询表达式
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 })
…
带有 orderby 子句的查询表达式
from x in e
orderby k1 , k2 , … , kn
…
转换为
from x in ( e ) .
OrderBy ( x => k1 ) .
ThenBy ( x => k2 ) .
… .
ThenBy ( x => kn )
…
如果排序子句指定 descending 方向指示器,则将改为生成对 OrderByDescending 或 ThenByDescending 的调用。
下面的转换假定在每个查询表达式中没有 let、where、join 或 orderby 子句,并且最多只有一个初始 from 子句。
下面的示例
from c in customers
from o in c.Orders
select new { c.Name, o.OrderID, o.Total }
转换为
customers.
SelectMany(c => c.Orders,
(c,o) => new { c.Name, o.OrderID, o.Total }
)
下面的示例
from 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 是编译器生成的以其他方式不可见且不可访问的标识符。
下面的示例
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 是编译器生成的以其他方式不可见且不可访问的标识符。
下面的示例
from c in customers
join o in orders on c.CustomerID equals o.CustomerID
select new { c.Name, o.OrderDate, o.Total }
转换为
customers.Join(orders, c => c.CustomerID,
o => o.CustomerID,
(c, o) => new { c.Name, o.OrderDate,
o.Total })
下面的示例
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 是编译器生成的以其他方式不可见且不可访问的标识符。
下面的示例
from o in
orders
orderby o.Customer.Name, o.Total descending
select o
具有最终转换
orders.
OrderBy(o => o.Customer.Name).
ThenByDescending(o => o.Total)
1.16.2.5 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”)
1.16.2.6 Groupby 子句
以下形式的查询表达式
from x in e group v by k
转换为
( e ) . GroupBy ( x => k , x => v )
除了当 v 为标识符 x 时,转换为
( e ) . GroupBy ( x => k )
下面的示例
from c in customers
group c.Name by c.Country
转换为
customers.
GroupBy(c => c.Country, c => c.Name)
1.16.2.7 透明标识符
某些转换使用由 * 指示的透明标识符注入范围变量。透明标识符不是合适的语言功能;它们在查询表达式转换过程中仅作为中间步骤存在。
当查询转换注入透明标识符时,进一步的转换步骤将透明标识符传播到匿名函数和匿名对象初始值设定项中。在这些上下文中,透明标识符具有以下行为:
- 当透明标识符作为某个匿名函数中的参数出现时,关联匿名类型的成员自动处于该匿名函数的函数体范围内。
- 当带透明标识符的成员位于范围内时,该成员的成员也位于范围内。
- 当透明标识符作为匿名对象初始值设定项中的成员声明符出现时,会引入一个具有透明标识符的成员。
在上面所述的转换步骤中,透明标识符始终与匿名类型一起引入,目的是以一个对象的成员的形式捕获多个范围变量。允许 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 { *,
p }).
Select(* => 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, p }).
Select(z => new { z.y.x.c.Name, z.y.x.o.OrderDate, z.p.ProductName })
其中 x、y 和 z 是编译器生成的以其他方式不可见且不可访问的标识符。
1.16.3 查询表达式模式
查询表达式模式 (Query expression pattern) 建立了一种方法模式,类型可以实现该模式来支持查询表达式。因为查询表达式通过句法映射转换为方法调用,所以类型在如何实现查询表达式模式方面具有很大灵活性。例如,该模式的方法可以以实例方法或扩展方法的形式实现,因为这两种方法具有相同的调用语法,而且方法可以请求委托或表达式树,因为匿名函数可以转换为这两者。
支持查询表达式模式的泛型类型 C<T> 的建议形式如下所示。使用泛型类型是为了演示参数和结果类型之间的正确关系,不过也可以为非泛型类型实现该模式。
delegate R Func<T1,R>(T1 arg1);
delegate R Func<T1,T2,R>(T1 arg1, T2 arg2);
class C
{
public C<T> Cast<T>();
}
class C<T> : C
{
public C<T>
Where(Func<T,bool> predicate);
public C<U> Select<U>(Func<T,U> selector);
public
C<V> SelectMany<U,V>(Func<T,C<U>> selector,
Func<T,U,V> resultSelector);
public
C<V> Join<U,K,V>(C<U> inner, Func<T,K>
outerKeySelector,
Func<U,K> innerKeySelector,
Func<T,U,V> resultSelector);
public
C<V> GroupJoin<U,K,V>(C<U> inner, Func<T,K>
outerKeySelector,
Func<U,K> innerKeySelector,
Func<T,C<U>,V> resultSelector);
public O<T> OrderBy<K>(Func<T,K> keySelector);
public O<T> OrderByDescending<K>(Func<T,K> keySelector);
public C<G<K,T>> GroupBy<K>(Func<T,K> keySelector);
public
C<G<K,E>> GroupBy<K,E>(Func<T,K> keySelector,
Func<T,E> elementSelector);
}
class O<T> : C<T>
{
public O<T>
ThenBy<K>(Func<T,K> keySelector);
public
O<T> ThenByDescending<K>(Func<T,K> keySelector);
}
class G<K,T> : C<T>
{
public K Key { get; }
}
上述方法使用泛型委托类型 Func<T1, R> 和 Func<T1, T2, R>,不过也可以使用在参数和结果类型中具有相同关系的其他委托或表达式目录树类型。
请注意 C<T> 和 O<T> 之间的建议关系,该关系可确保 ThenBy 和 ThenByDescending 方法只能用于 OrderBy 或 OrderByDescending 的结果。同时请注意 GroupBy 结果的推荐形式 - 一系列序列,其中每个内部序列都有一个附加的 Key 属性。
System.Linq 命名空间为实现 System.Collections.Generic.IEnumerable<T> 接口的任何类型提供了一个查询运算符模式的实现。
1.17 赋值运算符
赋值运算符为变量、属性、事件或索引器元素赋新值。
assignment:
unary-expression
assignment-operator expression
assignment-operator:
=
+=
-=
*=
/=
%=
&=
|=
^=
<<=
right-shift-assignment
赋值的左操作数必须是属于变量、属性访问、索引器访问或事件访问类别的表达式。
= 运算符称为简单赋值运算符。它将右操作数的值赋予左操作数给定的变量、属性或索引器元素。简单赋值运算符的左操作数一般不可以是一个事件访问(第 10.8.1 节中描述的例外)。简单赋值运算符的介绍详见第 7.17.1 节。
除 = 运算符以外的赋值运算符称为复合赋值运算符 (compound assignment operator)。这些运算符对两个操作数执行指示的运算,然后将结果值赋予左操作数指定的变量、属性或索引器元素。复合赋值运算符的介绍详见第 7.17.2 节。
以事件访问表达式作为左操作数的 += 和 -= 运算符称为事件赋值运算符。当左操作数是事件访问时,其他赋值运算符都是无效的。事件赋值运算符的介绍详见第 7.17.3 节。
赋值运算符为向右关联,即此类运算从右到左分组。例如,a = b = c 形式的表达式可以按 a = (b = c) 进行计算。
1.17.1 简单赋值
= 运算符称为简单赋值运算符。
如果简单赋值的左操作数为 E.P 或 E[Ei] 形式,其中 E 具有编译时类型 dynamic,则赋值是动态绑定的(第 7.2.2 节)。在此情况下,赋值表达式的编译时类型为 dynamic,并且会在运行时基于 E 的运行时类型进行下面所述的决策。
在简单赋值中,右操作数必须为可以隐式转换为左操作数所属类型的表达式。运算将右操作数的值赋予左操作数指定的变量、属性或索引器元素。
简单赋值表达式的结果是赋予左操作数的值。结果的类型与左操作数相同,且始终为值类别。
如果左操作数为属性或索引器访问,则该属性或索引器必须具有 set 访问器。如果不是这样,则发生绑定时错误。
x = y 形式的简单赋值的运行时处理包括以下步骤:
- 如果 x 属于变量:
- 计算 x 以产生变量。
- 计算 y,必要时还需通过隐式转换(第 6.1 节)将其转换为 x 的类型。
- 如果 x给定的变量是 reference-type 的数组元素,则执行运行时检查以确保为 y 计算的值与以 x 为其元素的那个数组实例兼容。如果 y 为 null,或存在从 y 引用的实例的实际类型到包含 x 的数组实例的实际元素类型的隐式引用转换(第 6.1.6 节),则检查成功。否则,将引发 System.ArrayTypeMismatchException。
- y 的计算和转换后所产生的值存储在 x 的计算所确定的位置中。
- 如果 x 属于属性或索引器访问:
- 计算与 x 关联的实例表达式(如果 x 不是 static)和参数列表(如果 x 是索引器访问),结果用于后面的对和 set 访问器调用。
- 计算 y,必要时还需通过隐式转换(第 6.1 节)将其转换为 x 的类型。
- 使用针对 y 计算的值作为 value 参数调用 x 的 set 访问器。
如果存在从 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.6.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.17.2 复合赋值
如果复合赋值的左操作数为 E.P 或 E[Ei] 形式,其中 E 具有编译时类型 dynamic,则赋值为动态绑定的(第 7.2.2 节)。在此情况下,赋值表达式的编译时类型为 dynamic,并且会在运行时基于 E 的运行时类型进行下面所述的决策。
x op= y 形式的运算是这样来处理的:应用重载决策(第 7.3.4 节),就好比运算的书写形式为 (x) op y。让 R 成为选定运算符的返回类型,以及 x 的 T 类型。然后,
- 如果存在从类型 R 的表达式到类型 T 的隐式转换,则将按 x = (T)((x) op y) 计算操作,但是只计算 x 一次。
- 否则,如果选定运算符为预定义的运算符,如果 R 可显式转换为 T,并且 y 可隐式转换为 T,或者运算符是移位运算符,则将按 x = (T)((x) op y) 计算操作,但是只计算 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 类型时,预定义的运算符可用作复合运算符。甚至当两个参数都为这些类型之一时,预定义的运算符也产生 intint类型的结果,详见第 7.3.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
每个错误的直观理由是对应的简单赋值也发生错误。
这还意味着复合赋值运算支持提升运算。在下面的示例中
int? i = 0;
i += 1; // Ok
使用了提升运算符 +(int?,int?)。
1.17.3 事件赋值
如果 += 或 -= 运算符的左操作数属于事件访问类别,则表达式按下面这样计算:
- 计算事件访问的实例表达式(如果有)。
- 计算 += 或 -= 运算符的右操作数,如果需要,通过隐式转换(第 6.1 节)转换为左操作数的类型。
- 调用该事件的事件访问器,所需的参数列表由右操作数(经过计算和必要的转换后)组成。如果运算符为 +=,则调用 add 访问器;如果运算符为 -=,则调用 remove 访问器。
事件赋值表达式不产生值。因此,事件赋值表达式只在 statement-expression(第 8.6 节)的上下文中是有效的。
1.18 表达式
expression 为 non-assignment-expression 或 assignment。
expression:
non-assignment-expression
assignment
non-assignment-expression:
conditional-expression
lambda-expression
query-expression
1.19 常量表达式
constant-expression 是在编译时可以完全计算出结果的表达式。
constant-expression:
expression
常量表达式必须为 null 文本或以下某种类型的值:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、object、string 或任何枚举类型。常量表达式中仅允许下列构造:
- 文本(包括 null 文本)。
- 对类和结构类型的 const 成员的引用。
- 对枚举类型的成员的引用。
- 对 const 参数或局部变量的引用
- 带括号的子表达式,其自身是常量表达式。
- 强制转换表达式(前提是目标类型为以上列出的类型之一)。
- checked 和 unchecked 表达式
- 默认值表达式
- 预定义的一元运算符 +、–、! 和 ~。
- 预定义的二元运算符 +、–、*、/、%、<<、>>、&、|、^、&&、||、==、!=、<、>、<= 和 >=(前提是每个操作数都为上面列出的类型)。
- 条件运算符 ?:。
常量表达式中允许下列转换:
- 标识转换
- 数值转换
- 枚举转换
- 常量表达式转换
- 隐式和显式引用转换,条件是转换的源是计算结果为 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.19 节)。
常量表达式出现在以下列出的上下文中。在这些上下文中,如果无法在编译时充分计算表达式,则发生编译时错误。
- 常量声明(第 10.4 节)。
- 枚举成员声明(第 14.3 节)。
- 形参表的默认参数(第 10.6.1 节)
- switch 语句的 case 标签(第 8.7.2 节)。
- goto case 语句(第 8.9.3 节)。
- 包含初始值设定项的数组创建表达式(第 7.6.10.4 节)中的维度长度。
- 特性(第 17 章)。
只要常量表达式的值在目标类型的范围内,隐式常量表达式转换(第 6.1.9 节)就允许将 int 类型的常量表达式转换为 sbyte、 byte、short、ushort、uint 或 ulong。
1.20 布尔表达式
boolean-expression 为产生 bool 类型结果的表达式,产生方式或者是直接产生,或者是通过在如下指定的某些上下文中应用 operator true 产生。
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.14 节)的控制条件表达式遵守与 boolean-expression 相同的规则,但由于运算符优先级的缘故,被归为 conditional-or-expression。
必须有 boolean-expression E 才能产生 bool 类型的值,如下所示:
- 如果 E 可隐式转换为 bool,则在运行时应用该隐式转换。
- 否则,使用一元运算符重载决策(第 7.3.3 节)查找运算符 true 对 E 的唯一最佳实现,并在运行时应用该实现。
- 如果未找到此类运算符,则发生绑定时错误。
第 11.4.2 节中的 DBBool 结构类型提供了一个实现了 operator true 和 operator false 的类型的示例。