C# Language Specification 5.0 (翻译)第二章 词法结构

时间:2023-11-24 22:26:08

C# Language Specification 5.0 (翻译)第二章 词法结构

程序

C# 程序(program)由至少一个源文件(source files)组成,其正式称谓为编译单元(compilation units)[1]。每个源文件都是有序的 Unicode 字符序列。源文件通常与文件系统内的相应文件具有一对一关系,但这种相关性并非必须因素。为尽最大可能确保可移植性,推荐文件系统中的文件编码为 UTF-8 编码规范。

从理论上来说,程序编译由三步骤组成:

  1. 转换(transformation),将文件中的特定字符编码方案转换为 Unicode 字符序列;
  2. 词法分析(lexical analysis),将 Unicode 字符流转换为标记(token)流;
  3. 语法分析(syntactic analysis),将标记流转换为可执行代码。

文法

本规范提出两种文法(grammars)来表示 C# 的语法(syntax)。词法文法(lexical grammar)定义了 Unicode 字符是如何组成行结束符(line terminators)、空白(white space)、注释(comments)、标记(tokens)和预处理指令(pre-processing directives)的,关于这一方面请详见本规范第二章第 2.2 节;句法文法(syntactic grammar)则定义了标记符号是如何从词法文法(lexical grammar)转换而来并组成了 C# 程序的,这个则将在本规范第二章第 2.3 节详述。

文法表示法

词法文法(lexical grammar)和句法文法(syntactic grammar)均通过使用文法产生式(grammar productions)来表示。每个文法产生式都定义了一个非终结符号(non-terminal symbol)及其可能的扩展(扩展由非终结符号与终结符号组成)。在文法产生式中,非终结符号被用斜体显示,而终结符号则使用等宽字体(fixed-width font)来表示。

词法产生式的第一行被定义为非终结符号的名称,而后紧跟着一个冒号 :。紧接这一行之后的连续几行相同缩进风格的是由非终结符号或终结符号构成的扩展。比方说下面这个词法产生式:

C# Language Specification 5.0 (翻译)第二章 词法结构

上面定义的这个 while-statement 包含了一个 while 标记,之后分别跟着 (boolean-expression)embedded-statement。当有超过一个非终结符扩展时,将它们交替列出,每一个扩展独占一行。如下面这个词法产生式:

C# Language Specification 5.0 (翻译)第二章 词法结构

上面定义的 statement-list 包含了一个 statement 以及一个紧随其后的 statement-list。换句话说,所定义的 statement-list 是递归的,它包含至少一个 statement

下标 “opt” 被用于标记可选符号(optional symbol)。下面这个词法产生式,

C# Language Specification 5.0 (翻译)第二章 词法结构

是下面这个词法产生式的缩写形式:

C# Language Specification 5.0 (翻译)第二章 词法结构

它定义了一个区块,用大括号标记 {...} 包裹起一个可选的 statement-list

像这种每行一个进行列举选项的形式,也可以通过使用 one of 将这些扩展列表写进一行里。也就是说这是一种在一行中显示这些选项的缩写形式。比方说下面这个词法产生式,

C# Language Specification 5.0 (翻译)第二章 词法结构

它的缩写形式如下:

C# Language Specification 5.0 (翻译)第二章 词法结构

词法文法

关于词法文法(lexical grammar)的内容请参见第二章第三节、第四节和第五节。词法文法的终结符号(terminal symbols)使用 Unicode 字符集,并具体详述了字符是如何相互组合(combined)以构成标记(token,第二章第四节)、空白(white space,第二章第3.3节)以及预处理指令(pre-processing directives,第二章第五节)的。

每个 C# 程序的源文件都必须遵循词法文法的 input 产生式(见第二章第三节)。

句法文法

句法文法(syntactic grammar)在本章以及之后的附录部分进行介绍。句法文法的终结符号(terminal symbols)由词法文法定义为标记,而句法文法则指定了这些标记是如何相互组合并构成 C# 程序的。

每一个 C# 程序的源文件都必须遵循句法文法 compilation-unit 产生式(见第九章第一节)。


词法分析

input 产生式定义了 C# 源文件的词法结构。每个源文件都必须遵循这个词法文法产生式(lexical grammar production)。

C# Language Specification 5.0 (翻译)第二章 词法结构

C# 源文件的词法结构由行终结符(Line terminators,第二章第3.1节)、空白(white space,第二章第3.3节)、注释(comments,第二章第3.2节)、标记(tokens,第二章第四节)以及预处理指令(pre-processing directives,第二章第五节)这五种基础元素(basic elements)组成。对于这些基础元素而言,只有标记(tokens)才是对句法文法有意义的(关于这一点,详见第二章第2.3节)。

C# 源文件的词法处理包括简化文件为标记序列以便能输入并进行句法分析。行终结符、空白和注释用于分割每一个标记,而预处理指令能导致跳过源文件的某些区段,然而这些词法元素(lexical elements)并不会影响到 C# 程序的句法结构。

当多个词法文法产生式匹配到同一个源文件字符序列,词法处理会尽力构成最长的词法元素。字符序列 // 被处理为单行注释的开头,因为其词法元素比单个斜杠 / 标记要长。

行终结符

行终结符(line terminators)将 C# 源文件的字符分割为多行。

C# Language Specification 5.0 (翻译)第二章 词法结构

为了与增加有 EOF 标记(end-of-file markers)的源代码编辑工具兼容,以及确保能以正确的行终结序列查看源代码,下列转换(transformations)将按顺序应用于 C# 程序的每一个源文件:

  • 如果源代码的最后一个字符是 Control-Z(U+001A),此字符将被删除;
  • 如果源文件是非空(non-empty)的且最后一个字符不是回车符 carriage-return character(U+000D)、换行符 line feed(U+000A)、行分隔符 line separator(U+2028)或段落分隔符 paragraph separator(U+2029)的话,回车符 carriage-return character(U+000D)将被添加到源文件的最后位置。

注释

支持两种形式的注释:单行注释(single-line comments)分割注释(delimited comments)。单行注释以字符 // 起始、延续并终于该行之尾。分割注释始于字符 /* 并止于字符 */,分割注释支持跨行。

C# Language Specification 5.0 (翻译)第二章 词法结构

注释不可嵌套(nest)。字符序列 /**/ 置于 // 内部亦或是字符序列 ///* 置于分割注释内部均毫无意义。置于字符与字符串内部的注释不会被处理。在例子

/* Hello, world program
This program writes “hello, world” to the console
*/
class Hello
{
static void Main() {
System.Console.WriteLine("hello, world");
}
}

中,包含一个跨行注释。在例子

// Hello, world program
// This program writes “hello, world” to the console
//
class Hello // any name will do for this class
{
static void Main() { // this method must be named "Main"
System.Console.WriteLine("hello, world");
}
}

中则展示了多个单行注释。

空白

空白(white space)的定义包含了所有 Unicode Zs 集字符[2](包括空白字符 space character)、水平制表符(horizontal tab character)、垂直制表符(vertical tab character)和换页符(form feed character)。

C# Language Specification 5.0 (翻译)第二章 词法结构


标记

有以下几种标记(tokens):标识符(identifiers)、关键字(keywords)、文本(literals)、操作符(operators)和标点符号(punctuators)。空白(white space)与注释(comments)并非标记,它们只是标记的分隔符。

C# Language Specification 5.0 (翻译)第二章 词法结构

Unicode 字符转义序列

Unicode 字符转义序列(Unicode character escape sequence)表示 Unicode 字符。Unicode 字符转义序列处理于标识符(第二章第4.2节)、字符文本(character literals,第二章第4.4.4节)以及正则字符串文本(regular string literals,第二章第4.4.5节)内。除此以外,Unicode 字符转义不会在其他任何地方被处理(比如在构成操作符、标点符或关键字时)。

C# Language Specification 5.0 (翻译)第二章 词法结构

Unicode 转义序列用以 \u\U 开头、续接一个十六进制数的字符形式来表示单个 Unicode 字符。因 C# 字符与字符串使用 16 位(16-bit)编码 Unicode 字符点,故区间在 U+10000U+10FFFF 之间的 Unicode 字符不能在 C# 字符中使用;在字符串中则使用一个代理对(surrogate pair)来表示。不支持代码点在 0x10FFFF 以上的 Unicode 字符。

Unicode 字符序列不允许多次转换,比如字符串文本 \u005Cu005C 等于 \u005C 而不是 \(Unicode 值 \u005C 的字符是 \)。

class Class1
{
static void Test(bool \u0066) {
char c = '\u0066';
if (\u0066)
System.Console.WriteLine(c.ToString());
}
}

在上面例子中我们多次使用了 \u0066,它是字母 f 的转义字符序列,所以整个程序等价于:

class Class1
{
static void Test(bool f) {
char c = 'f';
if (f)
System.Console.WriteLine(c.ToString());
}
}

标识符

区段(section)内的标识符规则与 Unicode 规范附录 31 所推荐的一致,除了以下情况:

0. 下划线 _ 允许用作初始字符(initial character,C 语言的一贯做法);

  1. Unicode 转义序列可以出现在标识符内;
  2. @ 符号可以用于关键字的前缀以便使其可为标识符。

C# Language Specification 5.0 (翻译)第二章 词法结构

上面所提及的 Unicode 字符集(Unicode character classes)仅供参考,具体请参见 Unicode 规范(版本 3.0,第 4.5 节)。

有效的标识符包括 identifier1_identifier2@if

在一个合格的程序中的标识符耶必须符合 Unicode Normalization Form C 的规范(定义于 Unicode 规范附录 15)。当遇到一个不符合上述规范的标识符时,(如何处理)可由实现自行具体定义(implementation-defined),但不强制要求诊断(diagnostic)。

添加有前缀 @ 的关键字(keywords)可以成为一个标识符(identifiers),此举在与其他编程语言配合时尤为有用。字符 @ 并非标识符的实际组成部分,故其它语言可将其(标识符)视为一个不带前缀的普通标识符。带有前缀 @ 的标识符被称为逐字标识符(verbatim identifier)。允许将 @ 用作非关键字的标识符之前缀,然此种写法强烈不推荐

举例。

class @class
{
public static void @static(bool @bool) {
if (@bool)
System.Console.WriteLine("true");
else
System.Console.WriteLine("false");
}
}
class Class1
{
static void M() {
cl\u0061ss.st\u0061tic(true);
}
}

所定义名叫 class 的类拥有一个名叫 static 的静态方法,其参数又被命名为 bool。由于 Unicode 转义不允许出现在关键字(keywords)内,所以标记 cl\u0061ss 是一个标识符(identifier),就如标识符 @class 那般。

若两个标识符按以下顺序应用转换方法后完全相同(identical),则其可被认定为相同:

  • 如果用了前缀 @,移除之;
  • unicode-escape-sequence 转换为其所对应之 Unicode 字符;
  • 移除所有 formatting-characters

标识符为实现保留了带有连续两个下划线字符(U+005F)__(以便供其使用),比方说实现可以自己设计以两个下划线开头的关键词扩展。

关键字

关键字(keyword)是类似标识符(identifier-like)的保留字符序列,除开以 @ 开头外,其它关键字不能用作标识符。

C# Language Specification 5.0 (翻译)第二章 词法结构

在文法(grammar)的一些地方,指定的识别符有着指定的含义,但这些并不是关键字。这些识别符有时用作「上下文关键字(contextual keywords)」。比方说在一个属性声明中,getset 标识符有着指定含义(见第十章第 7.2 节),而其它的标识符则不能用在这个地方,所以在这个地方将这些词当作标识符使用并不会发生冲突。在其它情况下,标识符 var 隐式声明了局部变量(第八章第 5.1 节),上下文关键字可能与声明名称相冲突[3]。在这种情况下,声明名的优先级将高于用作上下文关键字的标识符。

文本

文本(literal)[4]是源代码值的表示形式。

C# Language Specification 5.0 (翻译)第二章 词法结构

布尔值

布尔值文本(boolean literal)有 truefalse 两种值。

C# Language Specification 5.0 (翻译)第二章 词法结构

boolean-literal 的类型是 bool(布尔值)。

整数

整数文本(integer literals)被用于写作 int、uint、long 和 ulong 类型的值整数文本有两个可能的形式:十进制(decimal)和十六进制(hexadecimal)。

C# Language Specification 5.0 (翻译)第二章 词法结构

整数文本允许的类型如下:

  • 如果文本没有后缀(suffix),那么它将表示为 int、uint、long 和 ulong 中第一个能表示其值的类型;
  • 如果文本后缀为 U 或 u,那么它将表示为 uint 和 ulong 中第一个能表示其值的类型;
  • 如果文本后缀为 L 或 l,那么它将表示为 long 和 ulong 中第一个能表示其值的类型;
  • 如果文本后缀为 UL、Ul、uL、ul、LU、Lu、lU 或 lu,其类型为 ulong。

如果整数文本所表示的值超出了 ulong 类型的界限,会出现「编译时(compile-time)错误」。

从书写风格(与规范)的角度来说,当该文本可被写为 long 类型时建议使用 L 来代替 l,因为字母 l 和数字 1 外观几乎无法分辨。

为了确保最小的 int 和 long 值能被写为十进制整数文本,存在下面这两条规则:

  • decimal-integer-literal 之值为 2147483648(231)且无 integer-type-suffix 标记、前面又紧挨着一元负运算符(unary minus operator)标记(第七章第 7.2 节)时,其结果为 -2147483648(-231),在所有其它情况下,这个 decimal-integer-literal 的类型将是 uint 的。
  • decimal-integer-literal 之值为 9223372036854775808(263)且无 integer-type-suffixinteger-type-suffix L 或者 l 标记、前面又紧挨着一个一元负运算标记(a unary minus operator token,第七章第 7.2 节)时,其结果为 -9223372036854775808(-263),在其它情况下,这个 decimal-integer-literal 的类型将是 ulong 的。

实数

实数文本用于书写 float、double 和 decimal 类型的值。

C# Language Specification 5.0 (翻译)第二章 词法结构

如果没有指定 real-type-suffix,实数文本的类型是 double。不然,实数文本将用实数的类型后缀来确定其类型,遵照以下规则:

  • Ff 为其后缀者,其实数文本之类型为 float。如:1f1.5f1e10f123.456F
  • Dd 为其后缀者,其实数文本之类型为 double。如:1d11.5d1e10d123.456D
  • Mm 为其后缀者,其实数文本之类型为 decimal。如:1m1.5m1e10m123.456M。此实数通过获取其精确值并转换为 decimal 类型,如果必要的话则还会用「四舍六入五成双」规则(banker's rounding,又称银行进位法,见第四章第 1.7 节)将其值转换为最接近的可表示的值。期间实数的所有小数位均会被保留,除非其值已被舍入(rounded)或为零(后者的符号和小数位都将为 0)。因此,实数文本 2.900m 经解析(parse)后会变成符号为 0、系数为 2900、小数位为 3 (with sign 0, coefficient 2900, and scale 3)的 decimal 值。

如果特定的实数文本不能表示为一个指定类型,则会出现「编译时(compile-time)错误」。

单精度(float)或双精度(double)实数文本的值应使用 IEEE 的「就近舍入(round to nearest)」模式。

注意,在一个实数文本内,小数点后面的小数是必须留着的。比方说,1.3F 是一个实数文本但 1.F 不是。

字符

字符文本(character literal)表示单个字符,且通常由一个包裹在两个单引号 ' 之间的字符组成,比方说 'a'

C# Language Specification 5.0 (翻译)第二章 词法结构

跟在反斜杠字符 \ 之后的字符必须是下列字符中的一个,否则会出现「编译时(compile-time)错误」:'"\0abfnrtuUxv

十六进制转义序列(A hexadecimal escape sequence)表示单个 Unicode 字符,用 \x 后面跟着一个十六进制数的形式表示。

如果一个字符文本的值大于 U+FFFF,会出现「编译时(compile-time)错误」。

字符文本中的 Unicode 字符转义序列(第二章第4.1节)必须在 U+0000U+FFFF 区间内。

单个转义序列(simple escape sequence)表示一个 Unicode 字符编码,如下表所述:

C# Language Specification 5.0 (翻译)第二章 词法结构

character-literal 的类型是字符(char)。

字符串

C# 提供了两种字符串文本:正则字符串文本(regular string literals)原义字符串文本(verbatim string literals)

正则字符串文本包括由在两个双引号 " 之间的零至多个字符(如 "hello")、能被置于两个简单转义序列(诸如制表符(tab character)的 \t)之间以及十六进制(hexadecimal)转义序列和 Unicode 转义序列等组成。

原义字符串文本包括由在一个 @ 字符后面跟着一个开门双引号字符、零至多个字符以及一个关门双引号字符(closing double-quote character)组成,比方说 @"hello"。在原义字符串文本中,分隔符之间的字符被逐字解读(interpreted verbatim),除了遇到 quote-escape-sequence。尤其是单个转义序列、十六进制转义序列 Unicode 转义序列不会在原义字符串文本内被处理,同时原义字符串文本可以跨行(span multiple lines)。

C# Language Specification 5.0 (翻译)第二章 词法结构

反斜杠 \ 后跟着一个字符,这个组合如果在正则字符串文本字符(regular-string-literal-character)中的话,那么这个组合必须是下列项中的一项,否则会出现「编译时(compile-time)错误」:'"\0abfnrtuUxv。下面例子

string a = "hello, world";                 // hello, world
string b = @"hello, world"; // hello, world
string c = "hello \t world"; // hello world
string d = @"hello \t world"; // hello \t world
string e = "Joe said \"Hello\" to me"; // Joe said "Hello" to me
string f = @"Joe said ""Hello"" to me"; // Joe said "Hello" to me
string g = "\\\\server\\share\\file.txt"; // \\server\share\file.txt
string h = @"\\server\share\file.txt"; // \\server\share\file.txt
string i = "one\r\ntwo\r\nthree";
string j = @"one
two
three";

展示了一些原义字符串文本。最后一个字符串文本中,j 是一个跨行原义字符串文本。在两个引号之间的字符(包括空白和换行符)都逐字保留。

因为十六进制转义序列(hexadecimal escape sequence)可用于表示十六进制数字,在字符串文本 "\x123" 中包含了一个值为「十六进制 123」的单个字符。如果要创建一个包含「十六进制 12」值并在其后跟上一个字符 3,那么可以写成 "\x00123""\x12" + "3"

字符串文本 string-literal 的类型是字符串(string)。

每个字符串文本不一定创建一个新的 string 实例。当出现在同一程序内的两个甚至更多个字符串文本将通过字符串相等操作符(equality operator)判断为相等时,这些字符串文本引用相同的 string 实例。举例来说,下面这段代码

class Test
{
static void Main() {
object a = "hello";
object b = "hello";
System.Console.WriteLine(a == b);
}
}

这段代码将输出「True」,这是因为它们(两个文本)都引用了同一个 string 实例。

空 null

C# Language Specification 5.0 (翻译)第二章 词法结构

null-literal 能隐式地转换为一个引用类型或一个可空类型。

操作符与标点符

操作符与标点符有好几种类型。操作符用在表达式内,用于描述操作调用中一个或多个操作数。比方说表达式 a + b 使用了操作符 + 去把两个操作数 ab 加起来。标点符则用来分组和分隔。

C# Language Specification 5.0 (翻译)第二章 词法结构

right-shiftright-shift-assignment 产生式中的竖线 | (vertical bar)用来表示在标记之间不允许有任何类型的字符(包括空白),这一点不像句法文法中的其他标点符。这些标点符号都被特别处理,以便能正确处理 type-parameter-lists(第十章第 1.3 节)。


预处理指令

预处理指令提供了判断源码略过区段(skip sections)、汇报警告或错误、明确描述源码区域(regions)的能力。术语预处理指令(pre-processing directives)的用法与 C 和 C++ 的一样。在 C# 中没有独立的预处理步骤(pre-processing step),预处理指令被用于词法分析阶段(lexical analysis phase)。

C# Language Specification 5.0 (翻译)第二章 词法结构

下面列举了可用的预处理指令:

  • #define#undef 分别用于定义和取消定义条件编译符(conditional compilation symbols,第二章第 5.3 节);
  • #if#elif#else#endif 用于有条件地判断源代码略过区段(skip sections,第二章第 5.4 节);
  • #line 用于控制输出到错误信息或警告信息的行号(第二章第 5.7 节);
  • #error#warning 分别用于发布错误和警告(第二章第 5.5 节);
  • #region#endregion 用于显式标记源代码的区段(第二章第 5.6 节);
  • #pragma 用于给编译器指定可选的上下文信息(第二章第 5.8 节)。

预处理指令一贯在源码中独占一行,并总以字符 # 开头,后面紧跟一个预处理指令名。空白可以出现在 # 字符的前面以及 # 字符与指令名之间。

#define#undef#if#elif#else#endif#line#endregion 指令所在的源行可以以单行注释结尾,但不允许使用分割注释(delimited comments,注释以 /* ... */ 之形式)。

预处理指令不是标记,也不是 C# 的句法文法的组成部分。但预处理指令能被用于引入或排除标记序列并可以此种方式影响到 C# 程序之含义。例如,当我们对下面这段程序进行编译时,

#define A
#undef B
class C
{
#if A
void F() {}
#else
void G() {}
#endif
#if B
void H() {}
#else
void I() {}
#endif
}

所产生的标记序列(sequence of tokens)等于下面这段:

class C
{
void F() {}
void I() {}
}

因此,上述两例中尽管它们的词法(lexically)是迥异的,但它们的句法(syntactically)是一致(identical)的。

条件编译符号

#if#elif#else#endif 指令提供的条件编译功能受控于预处理表达式(pre-processing expressions,第二章第5.2节)和条件编译符号(conditional compilation symbols)。

C# Language Specification 5.0 (翻译)第二章 词法结构

条件编译符号有已定义(defined)未定义(undefined)这两种状态。在源文件的词法分析刚开始的时候,条件编译符号是未定义状态(除非它已被显式地被外部机制(external mechanism,诸如命令行编译选项)所定义的)。当 #define 指令被处理,指令中的命名的条件编译符号将会在源文件中定义。符号将保持被定义状态直到直到相同符号的 #undef 指令(具有相同符号的成对指令)被处理或到达源文件的结尾,这也就意味着在同一源文件中的 #define#undef 指令将不会影响到同程序中的其它源文件。

当它被引用在预处理表达式(pre-processing expression)内,已定义的条件编译符号有一个 true 值,未被定义的条件编译符则是一个 false 值。不强制要求在预处理表达式之前显式声明条件编译符,相反,未声明的符号只是未定义的而已,它依旧有个 false 值。

条件编译符号的命名空间明确且独立于其它的命名实体,条件编译符号只能被用在 #define#undef 指令之间或在预处理表达式内。

预处理表达式

预处理表达式(Pre-processing expressions)可以出现在 #if#endif 指令之间。操作符 !==!=&& 以及 || 都可以放进预处理表达式内,括号 (...) 可以用于分组(grouping)。

C# Language Specification 5.0 (翻译)第二章 词法结构

当引用了一个预处理表达式,已定义的条件编译符号将有个 true 的布尔值,而未定义的条件编译符号则有个 false 的布尔值。

预处理表达式的计算结果总是一个布尔值,其计算规则则与常量表达式(constant expression,第七章第十九节)是一样的,唯一的例外是此处唯一可引用的用户自定义实体(user-defined entities)是条件编译符。

声明指令

声明指令被用于定义(define)或取消定义(undefine)条件编译符号(conditional compilation symbols)。

C# Language Specification 5.0 (翻译)第二章 词法结构

经过 #define 指令的处理,所给定的条件编译符号将被定义(从该指令之后的源码行处开始生效)。同样地,#undef 指令的处理也会导致所给定的条件编译符号变为「未定义」(同样从该指令之后的源码行处开始生效)。

源码文件中所有 #define#undef 指令都必须出现在源文件中第一个标记(the first token,token 见第二章第四节)之前。否则将会出现「编译时(compile-time)错误」。从直觉的角度来说,#define#undef 指令都必须在源文件中所有的真实代码(real code)之前出现。打比方来说这个例子

#define Enterprise
#if Professional || Enterprise
#define Advanced
#endif
namespace Megacorp.Data
{
#if Advanced
class PivotTable {...}
#endif
}

是有效的,因为 #define 指令都在源文件的第一个标记(关键字 namespace)之前出现。下面的例子将导致一个「编译时错误(compile-time error)」,因为 #define 指令在真实代码(real code)之后:

#define A
namespace N
{
#define B
#if B
class Class1 {}
#endif
}

#define 指令能定义一个已被定义的条件编译符号,不需要 #undef 指令对该符号进行介入。下面这个例子定义了条件编译符号 A,然后对其重复定义。

#define A
#define A

#undef 能「取消定义」一个条件编译符号,即便这个符号尚未被定义。在下例中我们将定义一个名叫 A 的条件编译符号,然后对其两次取消定义。第二次使用 #undef 指令是不会生效的,但依旧是合法的(不会报错)。

#define A
#undef A
#undef A

条件编译指令

条件编译指令可以有选择地包含或排除源文件。

C# Language Specification 5.0 (翻译)第二章 词法结构

如上面语法中所指出那般,条件编译指令必须被写以由一组有序包含了 #if 指令、零或多个 #elif 指令、零或多个 #else 指令以及一个 #endif 指令所组成的集合的形式。在两个指令之间是可选源代码区段(section)。每一个区段都由上述指令所控制。可选区段内部可嵌套另一组条件编译指令——当然前提是这组指令集必须构成一个完整的指令集。

pp-conditional 将至多选择其所包含的一个 conditional-sections 区段并进行普通词法处理(normal lexical processing)流程:

  • #if#endif 指令的 pp-expressions 将有序计算直至遇到 true 结果。如果表达式为 true,则相关指令的 conditional-section 区段将会被选中(selected);
  • 如果所有的 pp-expressions 都为 false,但同时又有个 #else 指令存在,那么 #else 指令的 conditional-section 区段将会被选中;
  • 否则的话,不选中任何 conditional-section 区段。

如果选中了 conditional-section 区段,那么它将被处理为一个普通的 input-section 区段:在这个区段内的源代码必须符合词法文法、标记由此区段内的源码产生、此区段内的其他预处理指令拥有规定的效果。

如果还有剩下的 conditional-sections 区段,那么它们将处理为 skipped-sections 区段:除了预处理指令,区段内的源代码不会被要求符合词法文法、也不会有标记由这些区段内的源码所产生、这些区段内的预处理指令必须词法正确(lexically correct),但它们不会被处理。其内部嵌套的 conditional-section 区段也会被处理为 skipped-section 区段,所有嵌套的 conditional-sections(包括在嵌套 #if...#endif#region...#endregion 结构内的代码)区段都会被处理为 skipped-sections 区段。

下面举了一个关于条件编译指令是如何嵌套的例子:

#define Debug       // Debugging on
#undef Trace // Tracing off
class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#if Trace
WriteToLog(this.ToString());
#endif
#endif
CommitHelper();
}
}

除了预处理指令,被跳过的代码区段并不会受到词法分析的影响。比方说下面这段代码,尽管 #else 位于没有被关闭的注释内,但它依旧有效:

#define Debug        // Debugging on
class PurchaseTransaction
{
void Commit() {
#if Debug
CheckConsistency();
#else
/* Do something else
#endif
}
}

但是需要我们注意的是,即使它们位于源码中被跳过的区段内,预处理指令的词法依旧必须正确。

在多行输入元素(multi-line input elements)内出现的预处理指令(Pre-processing directives)是不会被执行的。举个例子,下面这段程序

class Hello
{
static void Main() {
System.Console.WriteLine(@"hello,
#if Debug
world
#else
Nebraska
#endif
");
}
}

的输出结果是:

hello,
#if Debug
world
#else
Nebraska
#endif

在这个古怪的例子中,这组预处理指令(pre-processing directives)的结果依赖于对 pp-expression 的计算,例如:

#if X
/*
#else
/* */ class Q { }
#endif

不管 X 是否已被定义,上述代码总会生成相同的标记流(class Q { })。如果 X 已被定义,因为多行注释的存在,只会处理 #if#endif 指令。如果 X 未被定义,那么这三个指令(#if#else#endif)都将是指令集的一部分。

诊断指令

诊断指令用于显式地产生错误(error)消息和警告(warning)信息,就如同其它在编译时出现的「编译时(compile-time)错误」和「编译时(compile-time)警告」。

C# Language Specification 5.0 (翻译)第二章 词法结构

例如:

#warning Code review needed before check-in
#if Debug && Retail
#error A build can't be both debug and retail
#endif
class Test {...}

上述代码将产生一个警告(warning)「Code review needed before check-in」,同时如果条件符号(conditional symbols)中的 Debug 和 Retail 被同时定义,那么还将产生一个「编译时(compile-time)错误(error)」「A build can’t be both debug and retail」。注意 pp-message 能包含任意文字——准确地说它不需要符合语法规则(well-formed)标记,就如 can't 中的引号那样。

区域指令

区域指令(region directives)通常用于显式标记源代码的区域(regions)。

C# Language Specification 5.0 (翻译)第二章 词法结构

区域(region)不具有任何语义含义,区域旨在于由程序员或由自动化工具来标记的一个源代码区段。在 #region#endregion 指令所指定的信息也是毫无任何语义含义的,它仅仅用于识别不同的区域(region)。相匹配的 #region#endregion 指令可以具有不同的 pp-messages

词法是这样处理一个区域(region)的:

#region
//...
#endregion

这与条件编译指令(conditional compilation directive)的形式非常类似:

#if true
//...
#endif

行指令

行指令(line directives)能用于修改由编译器输出的(诸如警告或错误之类的)报告中的行号(line numbers)和源文件名(source file names)信息,以及在调用者(caller)的信息特性(info attributes)中所使用的行号与源文件名(见第十七章第4.4节)。

通过在元编程(meta-programming)工具使用行指令,可以对从其它文本输入中生成相应的源代码。

C# Language Specification 5.0 (翻译)第二章 词法结构

当没有 #line 指令出现时,编译器将输出报告真正的行号和源文件名。当处理一个包含非默认行指示符(line-indicator)的 #line 指令时,编译器将会把该指令之后的行当做给定行号的行(当然,如果指定了文件名的话,还将包括这个文件名)。

#line default 指令会逆转之前的 #line 指令的作用。编译器汇报真实行信息给行序列,精确如无 #line 指令那般。

#line hidden 指令对错误信息中所汇报的文件与行号无影响,但却会影响到源级调试(source level debugging)。当你调试时,所有位于 #line hidden 指令至随后的 #line 指令(注意不是 #line hidden 指令)之间的行将无行号信息。当调试器(debugger)跳过这些代码时,这些行将被整体跳过。

注意,与正则字符串文本(regular string literal)不同,file-name 不处理转义字符——file-name 中的 \ 字符只是表示一个普通的反斜杠字符 \ 罢了。

编译指示指令

#pragma 预处理指令可用于具体地配置编译器可选上下文信息。这些由 #pragma 指令所提供的信息不会改变程序语义(program semantics)。

C# Language Specification 5.0 (翻译)第二章 词法结构

C# 所提供的 #pragma 指令可以控制编译器警告(warnings),语言的后续版本则将包含更多 #pragma 指令。为确保它与其它 C# 编译器的互操作性(interoperability),微软 C# 编译器不会在编译时对未知的 #pragma 指令报错(errors),顶多给出警告(warnings)。

编译指示警告

#pragma warning 指令通常用于在编译期间对随后的程序编码禁用(disable)或恢复(restore)指定的某条或全部的警告信息。

C# Language Specification 5.0 (翻译)第二章 词法结构

用于忽略警告列表的 #pragma warning 指令将对所有的警告生效。包含了警告列表的 #pragma warning 指令只对列表指定的警告生效。

#pragma warning disable 指令禁用所有的或一个给定集合的警告。

#pragma warning restore 指令会把所有的或一个给定集合的警告恢复到在编译单元开头之处的有效状态。需要注意,如果一条指定的警告从外部被禁用(disabled externally),那么 #pragma warning restore 指令不会重新启用(re-enable)那条(指定的一条或所有条)警告。

在下例中我们展示了当我们引用了一个过时的(obsoleted)成员时,通过微软 C# 编译器的警告编号(warning number),我们是如何使用 #pragma warning 指令来暂时禁用汇报警告功能的。

using System;
class Program
{
[Obsolete]
static void Foo() {}
static void Main() {
#pragma warning disable 612
Foo();
#pragma warning restore 612
}
}

[1] 编译单元的具体信息请查阅本系列的第九章第一节。

[2] Unicode Zs 集:SpaceSeparator,指示字符是空白字符,它不具有标志符号,但不是控制或格式字符, 完整信息点击此处查看

[3] 此处原文误将「contextual」写成了「contectual」,译者在此注明。

[4] 也称「字面值」或「字面量」

修订历史

0. 2015/07/08,完稿;

  1. 2015/07/14,第一次修订。

__EOF__

C# Language Specification 5.0 翻译计划