C# 语言规范_版本5.0 (第2章 词法结构)
1. 词法结构
1.1 程序
C# 程序 (program) 由一个或多个源文件 (source file) 组成,源文件的正式名称是编译单元 (compilation unit)(第 9.1 节)。源文件是有序的 Unicode 字符序列。源文件与文件系统中的文件通常具有一对一的对应关系,但这种对应关系不是必需的。为实现可移植性的最大化,建议这些文件在文件系统中应按 UTF-8 编码规范编码。
从概念上讲,程序的编译分三个步骤:
- 转换,这一步将用特定字符指令系统和编码方案编写的文件转换为 Unicode 字符序列。
- 词法分析,这一步将 Unicode 输入字符流转换为标记流。
- 句法分析,这一步将标记流转换为可执行代码。
1.2 文法
本规范采用两种文法 (grammar) 来表示 C# 编程语言的语法 (syntax)。词法文法 (lexical grammar)(第 2.2.2 节)规定怎样将 Unicode 字符组合成行结束符、空白、注释、标记和预处理指令等。句法文法 (syntactic grammar)(第 2.2.3 节)规定如何将那些由词法文法产生的标记组合成 C# 程序。
1.2.1 文法表示法
词法文法和句法文法用文法产生式 (grammar production) 来表示。每个文法产生式定义一个非结束符号和它可能的扩展(由非结束符或结束符组成的序列)。在文法产生式中,non-terminal 符号显示为斜体,而 terminal 符号显示为等宽字体。
文法产生式的第一行是该产生式所定义的非结束符号的名称,后跟一个冒号。每个后续的缩进行列出一个可能的扩展,它是以非结束符或结束符组成的序列的形式给出的。例如,产生式:
while-statement:
while
(
boolean-expression ) embedded-statement
定义了一个 while-statement,它是这样构成的:由标记 while 开始,后跟标记“(”、boolean-expression、标记“)”,最后是一个 embedded-statement。
当有不止一个可能的非结束符号扩展时,列出这些可能的扩展(每个扩展单独占一行)。例如,产生式:
statement-list:
statement
statement-list statement
定义一个 statement-list,它或仅含有一个 statement,或由一个 statement-list 和随后跟着的一个 statement 组成。换言之,定义是递归的,语句列表由一个或多个语句组成。
一个符号若以下标“opt”作其后缀,就表明该符号是可选的。产生式:
block:
{
statement-listopt }
是以下产生式的简短形式:
block:
{ }
{
statement-list }
它定义了一个 block,此块由一个用“{”和“}”标记括起来的可选 statement-list 组成。
可选项通常在单独的行上列出,但是当有许多可选项时,可以在单行上给定的扩展列表之后加上短语“之一”。这只是在单独一行上列出每个可选项的简短形式。例如,产生式:
real-type-suffix: one of
F
f D d
M m
是以下产生式的简短形式:
real-type-suffix:
F
f
D
d
M
m
1.2.2 词法文法
C# 的词法文法在第 2.3、2.4 和 2.5 节中介绍。词法文法的结束符号为 Unicode 字符集的字符,并且词法文法指定如何组合字符以构成标记(第 2.4 节)、空白(第 2.3.3 节)、注释(第 2.3.2 节)和预处理指令(第 2.5 节)。
C# 程序中的每个源文件都必须符合词法文法的 input 产生式(第 2.3 节)。
1.2.3 句法文法
本章后面的章节和附录介绍 C# 的句法文法。句法文法的结束符号是由词法文法定义的标记,句法文法指定如何组合这些标记以构成 C# 程序。
C# 程序中的每个源文件都必须符合句法文法的 compilation-unit 产生式(第 9.1 节)。
1.3 语法分析
input 产生式定义 C# 源文件的词法结构。C# 程序中的每个源文件都必须符合此词法文法产生式。
input:
input-sectionopt
input-section:
input-section-part
input-section input-section-part
input-section-part:
input-elementsopt new-line
pp-directive
input-elements:
input-element
input-elements input-element
input-element:
whitespace
comment
token
C# 源文件的词法结构由五个基本元素组成:行结束符(第 2.3.1节)、空白(第 2.3.3 节)、注释(第 2.3.2 节)、标记(第 2.4 节)和预处理指令(第 2.5 节)。在这些基本元素中,只有标记在 C# 程序的句法文法(第 2.2.3 节)中具有重要意义。
对 C# 源文件的词法处理就是将文件缩减成标记序列,该序列然后即成为句法分析的输入。行结束符、空白和注释可用于分隔标记,预处理指令可导致跳过源文件中的某些节,除此之外这些词法元素对 C# 程序的句法结构没有任何影响。
当有若干词法文法产生式与源文件中的一个字符序列匹配时,词法处理总是构成尽可能最长的词法元素。例如,字符序列 // 按单行注释的开头处理,这是因为该词法元素比一个 / 标记要长。
1.3.1 行结束符
行结束符将 C# 源文件的字符划分为行。
new-line:
Carriage return character (U+000D)
Line feed character (U+000A)
Carriage return character (U+000D) followed by
line feed character (U+000A)
Next line character (U+0085)
Line separator character (U+2028)
Paragraph separator character (U+2029)
为了与添加文件尾标记的源代码编辑工具兼容,并能够以正确结束的行序列的形式查看源文件,下列转换按顺序应用到 C# 程序中的每个源文件:
- 如果源文件的最后一个字符为 Control-Z 字符 (U+001A),则删除此字符。
- 如果源文件非空并且源文件的最后一个字符不是回车符 (U+000D)、换行符 (U+000A)、行分隔符 (U+2028) 或段落分隔符 (U+2029),则将在源文件的结尾添加一个回车符 (U+000D)。
1.3.2 注释
支持两种形式的注释:单行注释和带分隔符的注释。单行注释 (Single-line comment) 以字符 // 开头并延续到源行的结尾。带分隔符的注释 (Delimited comment) 以字符 /* 开头,以字符 */ 结束。带分隔符的注释可以跨多行。
comment:
single-line-comment
delimited-comment
single-line-comment:
//
input-charactersopt
input-characters:
input-character
input-characters input-character
input-character:
Any Unicode character except a new-line-character
new-line-character:
Carriage return character (U+000D)
Line feed character (U+000A)
Next line character (U+0085)
Line separator character (U+2028)
Paragraph separator character (U+2029)
delimited-comment:
/*
delimited-comment-textopt
asterisks /
delimited-comment-text:
delimited-comment-section
delimited-comment-text
delimited-comment-section
delimited-comment-section:
/
asterisksopt
not-slash-or-asterisk
asterisks:
*
asterisks *
not-slash-or-asterisk:
Any Unicode character except / or *
注释不嵌套。字符序列 /* 和 */ 在 // 注释中没有任何特殊含义,字符序列 // 和 /* 在带分隔符的注释中没有任何特殊含义。
在字符和字符串内不处理注释。
下面的示例
/* 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");
}
}
演示了若干单行注释。
1.3.3 空白
空白被定义为任何含 Unicode 类 Zs 的字符(包括空白字符)以及水平制表符、垂直制表符和换页符。
whitespace:
Any character with Unicode
class Zs
Horizontal tab character (U+0009)
Vertical tab character (U+000B)
Form feed character (U+000C)
1.4 标记
有几类标记:标识符、关键字、文本、运算符和标点符号。空白和注释不是标记,但它们可充当标记的分隔符。
token:
identifier
keyword
integer-literal
real-literal
character-literal
string-literal
operator-or-punctuator
1.4.1 Unicode 字符转义序列
Unicode 字符转义序列表示一个 Unicode 字符。Unicode 字符转义序列在标识符(第 2.4.2 节)、字符(第 2.4.4.4 节)和规则字符串(第 2.4.4.5 节)中处理。不在其他任何位置处理 Unicode 字符转义(例如,在构成运算符、标点符号或关键字时)。
unicode-escape-sequence:
\u
hex-digit hex-digit hex-digit
hex-digit
\U
hex-digit hex-digit hex-digit
hex-digit hex-digit hex-digit
hex-digit hex-digit
Unicode 转义序列表示由“\u”或“\U”字符后面的十六进制数字构成的单个 Unicode 字符。由于 C# 在字符和字符串值中使用 Unicode 代码点的 16 位编码,因此从 U+10000 到 U+10FFFF 的 Unicode 字符不能在字符中使用,在字符串中则用一个 Unicode 代理项对来表示。不支持代码数据点在 0x10FFFF 以上的 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());
}
}
1.4.2 标识符
本节给出的标识符规则完全符合 Unicode 标准附件 31 推荐的规则,但以下情况除外:允许将下划线用作初始字符(这是 C 编程语言的传统),允许在标识符中使用 Unicode 转义序列,以及允许“@”字符作为前缀以使关键字能够用作标识符。
identifier:
available-identifier
@
identifier-or-keyword
available-identifier:
An identifier-or-keyword that is not a keyword
identifier-or-keyword:
identifier-start-character
identifier-part-charactersopt
identifier-start-character:
letter-character
_ (the underscore
character U+005F)
identifier-part-characters:
identifier-part-character
identifier-part-characters
identifier-part-character
identifier-part-character:
letter-character
decimal-digit-character
connecting-character
combining-character
formatting-character
letter-character:
A Unicode character of classes Lu, Ll, Lt, Lm, Lo, or
Nl
A unicode-escape-sequence representing a character
of classes Lu, Ll, Lt, Lm, Lo, or Nl
combining-character:
A Unicode character of classes Mn or Mc
A unicode-escape-sequence representing a character
of classes Mn or Mc
decimal-digit-character:
A Unicode character of the class Nd
A unicode-escape-sequence representing a character
of the class Nd
connecting-character:
A Unicode character of the class Pc
A unicode-escape-sequence representing a character
of the class Pc
formatting-character:
A Unicode character of the class Cf
A unicode-escape-sequence representing a character
of the class Cf
有关上面提到的 Unicode 字符类的信息,请参见《Unicode 标准 3.0 版》的第 4.5 节。
有效标识符的示例包括“identifier1”、“_identifier2”和“@if”。
符合规范的程序中的标识符必须遵循由“Unicode 标准化格式 C”(按“Unicode 标准附录 15”中的定义)定义的规范格式。当遇到非“标准化格式 C”格式的标识符时,怎样处理它可由 C 的具体实现确定,但是不要求诊断。
使用前缀“@”可以将关键字用作标识符,这在与其他编程语言建立接口时很有用。字符 @ 并不是标识符的实际组成部分,因此在其他语言中可能将此标识符视为不带前缀的正常标识符。带 @ 前缀的标识符称作逐字标识符 (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 转义符,因此标记“cl\u0061ss”是标识符,与“@class”标识符相同。
两个标识符如果在按顺序实施了下列转换后相同,则被视为相同:
- 如果使用了前缀“@”,移除它。
- 将每个 unicode-escape-sequence 转换为它的对应 Unicode 字符。
- 移除所有 formatting-character。
包含两个连续下划线字符 (U+005F) 的标识符被保留供具体实现使用。例如,一个实现可以设置它自己的以两个下划线开头的扩展关键字。
1.4.3 关键字
关键字 (keyword) 是类似标识符的保留的字符序列,不能用作标识符(以 @ 字符开头时除外)。
keyword: one of
abstract as base bool break
byte case catch char checked
class const continue decimal default
delegate do double else enum
event explicit extern false finally
fixed float for foreach goto
if implicit in int interface
internal is lock long namespace
new null object operator out
override params private protected public
readonly ref return sbyte sealed
short sizeof stackalloc static string
struct switch this throw true
try typeof uint ulong unchecked
unsafe ushort using virtual void
volatile while
在文法中的某些位置,特定的标识符有特殊的含义,但不是关键字。这类标识符有时称为“上下文关键字”。例如,在属性声明中,“get”和“set”标识符有特殊的含义(第 10.7.2 节)。在这些位置从不允许使用 get 或 set 之外的标识符,因此此用法与这些词用作标识符并不冲突。在其他情况下,如对于隐式类型化局部变量声明(第 8.5.1 节)中的标识符“var”,上下文关键字可能与声明的名称冲突。在这类情况下,声明的名称优先于将标识符用作上下文关键字。
1.4.4 文本
文本 (literal) 是一个值的源代码表示形式。
literal:
boolean-literal
integer-literal
real-literal
character-literal
string-literal
null-literal
1.4.4.1 布尔值
有两个布尔文本值:true 和 false。
boolean-literal:
true
false
boolean-literal 的类型是 bool。
1.4.4.2 整数
整数文本用于编写类型为 int、uint、long 和 ulong 的值。整数有两种可能的形式:十进制和十六进制。
integer-literal:
decimal-integer-literal
hexadecimal-integer-literal
decimal-integer-literal:
decimal-digits integer-type-suffixopt
decimal-digits:
decimal-digit
decimal-digits decimal-digit
decimal-digit: one of
0 1
2 3 4
5 6 7
8 9
integer-type-suffix: one of
U u
L l UL
Ul uL ul
LU Lu lU lu
hexadecimal-integer-literal:
0x hex-digits
integer-type-suffixopt
0X hex-digits
integer-type-suffixopt
hex-digits:
hex-digit
hex-digits hex-digit
hex-digit: one of
0
1 2 3
4 5 6
7 8 9
A B C
D E F
a b c
d e f
一个整数的类型按下面的方式确定:
- 如果该整数没有后缀,则它属于以下所列的类型中第一个能够表示其值的那个类型:int、uint、long、ulong。
- 如果该文本带有后缀 U 或 u,则它属于以下所列的类型中第一个能够表示其值的那个类型:uint 和 ulong。
- 如果该文本带有后缀 L 或 l,则它属于以下所列的类型中第一个能够表示其值的那个类型:long 和 ulong。
- 如果该文本带有后缀 UL、Ul、uL、ul、LU、Lu、lU 或 lu,它的类型为 ulong。
如果整数表示的值超出了 ulong 类型的范围,则将发生编译时错误。
从书写风格(样式)的角度出发,建议在书写类型 long 的文本时使用“L”而不是“l”,因为字母“l”容易与数字“1””混淆。
为允许尽可能小的 int 和 long 值写为十进制整数,有下面两个规则:
- 当具有值 2147483648 (231) 且没有 integer-type-suffix 的一个 decimal-integer-literal 作为标记紧接在一元负运算符标记(第 7.7.2 节)后出现时,结果为具有值 −2147483648 (−231) 的 int 类型常量。在所有其他情况下,这样的 decimal-integer-literal 属于 uint 类型。
- 当具有值 9223372036854775808 (263) 的一个 decimal-integer-literal(没带 integer-type-suffix,或带有 integer-type-suffix L 或 l)作为一个标记紧跟在一个一元负运算符标记(第 7.7.2 节)后出现时,结果是具有值 −9223372036854775808 (−263) 的 long 类型的常量。在所有其他情况下,这样的 decimal-integer-literal 属于 ulong 类型。
1.4.4.3 实数
实数文本用于编写类型为 float、double 和 decimal 的值。
real-literal:
decimal-digits . decimal-digits exponent-partopt real-type-suffixopt
.
decimal-digits exponent-partopt real-type-suffixopt
decimal-digits exponent-part real-type-suffixopt
decimal-digits real-type-suffix
exponent-part:
e
signopt
decimal-digits
E signopt decimal-digits
sign: one of
+
-
real-type-suffix: one of
F
f D d
M m
如果未指定 real-type-suffix,则实数的类型为 double。否则,实数类型后缀确定实数的类型,如下所示:
- 以 F 或 f 为后缀的实数文本的类型为 float。例如,文本 1f、1.5f、1e10f 和 123.456F 的类型都是 float。
- 以 D 或 d 为后缀的实数文本的类型为 double。例如,文本 1d、1.5d、1e10d 和 123.456D 的类型都是 double。
- 以 M 或 m 为后缀的实数文本的类型为 decimal。例如,文本 1m、1.5m、1e10m 和 123.456M 的类型都是 decimal。此实数通过取精确值转换为 decimal 值,如果有必要,用银行家舍入法(第 4.1.7 节)舍入为最接近的可表示值。保留该实数的所有小数位数,除非值被舍入或者值为零(在后一种情况中,符号和小数位数为 0)。因此,实数 2.900m 经分析后将形成这样的一个小数:符号为 0、系数为 2900,小数位数为 3。
如果一个给定的实数不能用指定的类型表示,则会发生编译时错误。
使用 IEEE“就近舍入”模式确定类型 float 或 double 的实数的值。
注意在实数中,小数点后必须始终是十进制数字。例如,1.3F 是实数,但 1.F 不是。
1.4.4.4 字符
字符表示单个字符,通常由置于引号中的一个字符组成,如 'a'。
character-literal:
'
character '
character:
single-character
simple-escape-sequence
hexadecimal-escape-sequence
unicode-escape-sequence
single-character:
Any character
except ' (U+0027), \ (U+005C), and new-line-character
simple-escape-sequence: one of
\' \"
\\ \0 \a
\b \f \n
\r \t \v
hexadecimal-escape-sequence:
\x
hex-digit hex-digitopt hex-digitopt hex-digitopt
在 character 中跟在反斜杠字符 (\) 之后的字符必须是以下字符之一:'、"、\、0、a、b、f、n、r、t、u、U、x 和 v。其他情况下,将发生编译时错误。
十六进制转义序列表示单个 Unicode 字符,它的值由“\x”后接十六进制数组成。
如果一个字符表示的值大于 U+FFFF,则将发生编译时错误。
字符中的 Unicode 字符转义序列(第 2.4.1 节)必须在 U+0000 到 U+FFFF 的范围内。
一个简单转义序列表示一个 Unicode 字符编码,详见下表。
转义序列 |
字符名称 |
Unicode 编码 |
\' |
单引号 |
0x0027 |
\" |
双引号 |
0x0022 |
\\ |
反斜杠 |
0x005C |
\0 |
Null |
0x0000 |
\a |
警报 |
0x0007 |
\b |
Backspace |
0x0008 |
\f |
换页符 |
0x000C |
\n |
换行符 |
0x000A |
\r |
回车 |
0x000D |
\t |
水平制表符 |
0x0009 |
\v |
垂直制表符 |
0x000B |
character-literal 的类型为 char。
1.4.4.5 字符串
C# 支持两种形式的字符串:常规字符串 (regular string literal) 和原义字符串 (verbatim string literal)。
正则字符串由包含在双引号中的零个或多个字符组成(如 "hello"),并且可以包含简单转义序列(如表示制表符的 \t)、十六进制转义序列和 Unicode 转义序列。
原义字符串由 @ 字符后跟开始的双引号字符、零个或多个字符以及结束的双引号字符组成。一个简单的示例就是 @"hello"。在原义字符串中,分隔符之间的字符逐字解释,唯一的例外是 quote-escape-sequence。具体而言,在原义字符串中不处理简单转义序列以及十六进制和 Unicode 转义序列。原义字符串可以跨多行。
string-literal:
regular-string-literal
verbatim-string-literal
regular-string-literal:
"
regular-string-literal-charactersopt "
regular-string-literal-characters:
regular-string-literal-character
regular-string-literal-characters
regular-string-literal-character
regular-string-literal-character:
single-regular-string-literal-character
simple-escape-sequence
hexadecimal-escape-sequence
unicode-escape-sequence
single-regular-string-literal-character:
Any character
except " (U+0022), \ (U+005C), and new-line-character
verbatim-string-literal:
@"
verbatim-string-literal-charactersopt "
verbatim-string-literal-characters:
verbatim-string-literal-character
verbatim-string-literal-characters
verbatim-string-literal-character
verbatim-string-literal-character:
single-verbatim-string-literal-character
quote-escape-sequence
single-verbatim-string-literal-character:
Any character
except "
quote-escape-sequence:
""
在 regular-string-literal-character 中跟在反斜杠字符 (\) 之后的字符必须是以下字符之一:'、"、\、0、a、b、f、n、r、t、u、U、x、v。其他情况下,将发生编译时错误。
下面的示例
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 是跨多行的原义字符串。引号之间的字符(包括空白,如换行符等)也逐字符保留。
由于十六进制转义序列可以包含数目可变的十六进制数字,因此字符串 "\x123" 只包含一个具有十六进制值 123 的字符。若要创建一个包含具有十六进制值 12 的字符,后跟一个字符 3 的字符串,可以改写为 "\x00123" 或 "\x12" + "3"。
string-literal 的类型为 string。
每个字符串文本不一定产生新的字符串实例。当根据字符串相等运算符(第 7.10.7 节)确认为相等的两个或更多个字符串出现在同一个程序中时,这些字符串引用相同的字符串实例。例如,
class Test
{
static void Main() {
object a = "hello";
object b = "hello";
System.Console.WriteLine(a == b);
}
}
产生的输出为 True,这是因为两个字符串引用相同的字符串实例。
1.4.4.6 null 文本
null-literal:
null
可以将 null-literal 隐式转换为引用类型或可以为 null 的类型。
1.4.5 运算符和标点符号
有若干种运算符和标点符号。运算符在表达式中用于描述涉及一个或多个操作数的运算。例如,表达式 a + b 使用 + 运算符添加两个操作数 a 和 b。标点符号用于分组和分隔。
operator-or-punctuator: one of
{ } [ ] ( ) . , : ;
+ - * / % & | ^ ! ~
= < > ? ?? :: ++ -- && ||
-> == != <= >= += -= *= /= %=
&= |= ^= << <<= =>
right-shift:
>|>
right-shift-assignment:
>|>=
right-shift 和 right-shift-assignment 产生式中的竖线用来表示:和采用句法文法的其他产生式不同,在标记之间不允许有任何类型的字符(甚至不允许空白)。为了能正确处理 type-parameter-list(第 10.1.3 节),要对这些产生式进行特殊处理。
1.5 预处理指令
预处理指令提供按条件跳过源文件中的节、报告错误和警告条件,以及描绘源代码的不同区域的能力。使用术语“预处理指令”只是为了与 C 和 C++ 编程语言保持一致。在 C# 中没有单独的预处理步骤;预处理指令按词法分析阶段的一部分处理。
pp-directive:
pp-declaration
pp-conditional
pp-line
pp-diagnostic
pp-region
pp-pragma
下面是可用的预处理指令:
- #define 和 #undef,分别用于定义和取消定义条件编译符号(第 2.5.3 节)。
- #if、#elif、#else 和 #endif,用于按条件跳过源代码中的节(第 2.5.4 节)。
- #line,用于控制行号(在发布错误和警告信息时使用)(第 2.5.7 节)。
- #error 和 #warning,分别用于发出错误和警告(第 2.5.5 节)。
- #region 和 #endregion,用于显式标记源代码中的节(第 2.5.6 节)。
- #pragma,用于为编译器指定可选的上下文信息(第 2.5.8 节)。
预处理指令总是占用源代码中的单独一行,并且总是以 # 字符和预处理指令名称开头。# 字符的前面以及 # 字符与指令名称之间可以出现空白符。
包含 #define、#undef、#if、#elif、#else、#endif、#line 或 #endregion 指令的源代码行可以用单行注释结束。在包含预处理指令的源行上不允许使用带分隔符的注释(/* */ 样式的注释)。
预处理指令既不是标记,也不是 C# 句法文法的组成部分。但是,可以用预处理指令包含或排除标记序列,并且可以以这种方式影响 C# 程序的含义。例如,编译后,程序:
#define A
#undef B
class C
{
#if A
void F() {}
#else
void G() {}
#endif
#if B
void H() {}
#else
void I() {}
#endif
}
产生与下面的程序完全相同的标记序列:
class C
{
void F() {}
void I() {}
}
因此,尽管上述两个程序在词法分析中完全不同,但它们在句法分析中是相同的。
1.5.1 条件编译符号
#if、#elif、#else 和 #endif 指令提供的条件编译功能是通过预处理表达式(第 2.5.2 节)和条件编译符号来控制的。
conditional-symbol:
Any identifier-or-keyword except true or false
条件编译符号具有两种可能的状态:已定义 (defined) 或未定义 (undefined)。在源文件词法处理开始时,条件编译符号除非已由外部机制(如命令行编译器选项)显式定义,否则是未定义的。当处理 #define 指令时,在该指令中指定的条件编译符号在那个源文件中成为已定义的符号。此后,该符号就一直保持已定义的状态,直到处理一条关于同一符号的 #undef 指令,或者到达源文件的结尾。这意味着一个源文件中的 #define 和 #undef 指令对同一程序中的其他源文件没有任何影响。
当在预处理表达式中引用时,已定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false。不要求在预处理表达式中引用条件编译符号之前显式声明它们。相反,未声明的符号只是未定义的,因此具有值 false。
条件编译符号的命名空间与 C# 程序中的所有其他命名实体截然不同。只能在 #define 和 #undef 指令以及预处理表达式中引用条件编译符号。
1.5.2 预处理表达式
预处理表达式可以出现在 #if 和 #elif 指令中。在预处理表达式中允许使用 !、==、!=、&& 和 || 运算符,并且可以使用括号进行分组。
pp-expression:
whitespaceopt
pp-or-expression whitespaceopt
pp-or-expression:
pp-and-expression
pp-or-expression whitespaceopt ||
whitespaceopt
pp-and-expression
pp-and-expression:
pp-equality-expression
pp-and-expression whitespaceopt &&
whitespaceopt
pp-equality-expression
pp-equality-expression:
pp-unary-expression
pp-equality-expression whitespaceopt ==
whitespaceopt
pp-unary-expression
pp-equality-expression whitespaceopt !=
whitespaceopt
pp-unary-expression
pp-unary-expression:
pp-primary-expression
!
whitespaceopt
pp-unary-expression
pp-primary-expression:
true
false
conditional-symbol
(
whitespaceopt
pp-expression whitespaceopt )
当在预处理表达式中引用时,已定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false。
预处理表达式的计算总是产生一个布尔值。预处理表达式的计算规则与常量表达式(第 7.19 节)相同,唯一的例外是:在这里,唯一可引用的用户定义实体是条件编译符号。
1.5.3 声明指令
声明指令用于定义或取消定义条件编译符号。
pp-declaration:
whitespaceopt # whitespaceopt define
whitespace
conditional-symbol pp-new-line
whitespaceopt # whitespaceopt undef
whitespace
conditional-symbol pp-new-line
pp-new-line:
whitespaceopt
single-line-commentopt
new-line
对 #define 指令的处理使给定的条件编译符号成为已定义的符号(从跟在指令后面的源代码行开始)。类似地,对 #undef 指令的处理使给定的条件编译符号成为未定义的符号(从跟在指令后面的源代码行开始)。
源文件中的任何 #define 和 #undef 指令都必须出现在源文件中第一个 token(第 2.4 节)的前面,否则将发生编译时错误。直观地讲,#define 和 #undef 指令必须位于源文件中所有“实代码”的前面。
示例:
#define Enterprise
#if
Professional || Enterprise
#define Advanced
#endif
namespace
Megacorp.Data
{
#if Advanced
class PivotTable {...}
#endif
}
是有效的,这是因为 #define 指令位于源文件中第一个标记(namespace 关键字)的前面。
下面的示例产生编译时错误,因为 #define 指令在实代码后面出现:
#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
1.5.4 条件编译指令
条件编译指令用于按条件包含或排除源文件中的某些部分。
pp-conditional:
pp-if-section pp-elif-sectionsopt pp-else-sectionopt pp-endif
pp-if-section:
whitespaceopt # whitespaceopt if
whitespace pp-expression pp-new-line
conditional-sectionopt
pp-elif-sections:
pp-elif-section
pp-elif-sections pp-elif-section
pp-elif-section:
whitespaceopt # whitespaceopt elif
whitespace pp-expression pp-new-line
conditional-sectionopt
pp-else-section:
whitespaceopt # whitespaceopt else
pp-new-line conditional-sectionopt
pp-endif:
whitespaceopt # whitespaceopt endif
pp-new-line
conditional-section:
input-section
skipped-section
skipped-section:
skipped-section-part
skipped-section skipped-section-part
skipped-section-part:
skipped-charactersopt
new-line
pp-directive
skipped-characters:
whitespaceopt
not-number-sign input-charactersopt
not-number-sign:
Any input-character except #
按照语法的规定,条件编译指令必须写成集的形式,集的组成依次为:一个 #if 指令、一个或多个 #elif 指令(或没有)、一个或多个 #else 指令(或没有)和一个 #endif 指令。指令之间是源代码的条件节。每节代码直接位于它前面的那个指令控制。条件节本身可以包含嵌套的条件编译指令,前提是这些指令构成完整的指令集。
pp-conditional 最多只能选择它所包含的 conditional-section 之一去做通常的词法处理:
- 按顺序计算 #if 和 #elif 指令的 pp-expression,直到得出 true 值。如果表达式的结果为 true,则选择对应指令的 conditional-section。
- 如果所有 pp-expression 的结果都为 false 并且存在 #else 指令,则选择 #else 指令的 conditional-section。
- 否则不选择任何 conditional-section。
所选的 conditional-section(如果有)作为正常 input-section 进行处理:节中包含的源代码必须符合词法文法;标记由节中的源代码生成;并且节中的预处理指令具有规定的效果。
剩余的 conditional-section(如果有)作为 skipped-sections 进行处理:除了预处理指令,节中的源代码不必一定要符合词法文法;不从节中的源代码生成任何词法标记;节中的预处理指令必须在词法上正确,但不另外处理。在按 skipped-section 处理的 conditional-section 中,任何嵌套的 conditional-section(包含在嵌套的 #if...#endif 和 #region...#endregion 构造中)也按skipped-section 处理。
下面的示例阐释如何嵌套条件编译指令:
#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
}
}
但请注意,即使是在源代码的跳过节中,也要求预处理指令在词法上正确。
当预处理指令出现在多行输入元素的内部时,不作为预处理指令处理。例如,程序:
class Hello
{
static void Main() {
System.Console.WriteLine(@"hello,
#if Debug
world
#else
Nebraska
#endif
");
}
}
输出结果为:
hello,
#if Debug
world
#else
Nebraska
#endif
在特殊情况下,处理的预处理指令集合可有取决于 pp-expression 的计算结果。示例:
#if X
/*
#else
/* */ class Q { }
#endif
总是生成同样的标记流 (class Q { }),不管是否定义了 X。如果定义了 X,由于多行注释的缘故,只处理 #if 和 #endif 指令。如果未定义 X,则这三个指令(#if、#else、#endif)都是指令集的组成部分。
1.5.5 诊断指令
诊断指令用于显式生成错误信息和警告消息,这些信息的报告方式与其他编译时错误和警告相同。
pp-diagnostic:
whitespaceopt # whitespaceopt error
pp-message
whitespaceopt # whitespaceopt warning
pp-message
pp-message:
new-line
whitespace input-charactersopt new-line
示例:
#warning Code review needed before check-in
#if Debug
&& Retail
#error A build can't be both debug and
retail
#endif
class Test {...}
总是产生一个警告(“Code review needed before check-in”),如果同时定义条件符号 Debug 和 Retail,则产生一个编译时错误(“A build can't be both debug and retail”)。请注意,pp-message 可以包含任意文本;具体而言,它可以包含格式不正确的标记,比如单词 can’t 中的单引号。
1.5.6 区域指令
区域指令用于显式标记源代码的区域。
pp-region:
pp-start-region conditional-sectionopt pp-end-region
pp-start-region:
whitespaceopt # whitespaceopt region
pp-message
pp-end-region:
whitespaceopt # whitespaceopt endregion
pp-message
区域不具有任何附加的语义含义;区域旨在由程序员或自动工具用来标记源代码中的节。#region 或 #endregion 指令中指定的消息同样不具有任何语义含义;它只是用于标识区域。匹配的 #region 和 #endregion 指令可能具有不同的 pp-message。
区域的词法处理:
#region
...
#endregion
与以下形式的条件编译指令的词法处理完全对应:
#if true
...
#endif
1.5.7 行指令
行指令可用于变更编译器在输出(如警告和错误)中报告的行号和源文件名称,以及调用方信息特性(第 17.4.4 节)所使用的行号和源文件名称。
行指令最常用于从某些其他文本输入生成 C# 源代码的元编程工具。
pp-line:
whitespaceopt # whitespaceopt line
whitespace line-indicator pp-new-line
line-indicator:
decimal-digits whitespace file-name
decimal-digits
default
hidden
file-name:
"
file-name-characters "
file-name-characters:
file-name-character
file-name-characters
file-name-character
file-name-character:
Any input-character except "
当不存在 #line 指令时,编译器在它的输出中报告真实的行号和源文件名称。当处理的 #line 指令包含不是 default 的 line-indicator 时,编译器将该指令后面的行视为具有给定的行号(如果指定了,还包括文件名)。
#line default 指令消除前面所有 #line 指令的影响。编译器报告后续行的真实行信息,就像尚未处理任何 #line 指令一样。
#line hidden 指令对错误信息中报告的文件号和行号无效,但对源代码级调试确实有效。调试时,#line hidden 指令和后面的 #line 指令(不是 #line hidden)之间的所有行都没有行号信息。在调试器中逐句执行代码时,将全部跳过这些行。
注意,file-name 与正则字符串的不同之处在于前者不处理转义字符;“\”字符在 file-name 中只是表示一个普通的反斜杠字符。
1.5.8 Pragma 指令
#pragma 预处理指令用来向编译器指定可选的上下文信息。#pragma 指令中提供的信息永远不会更改程序语义。
pp-pragma:
whitespaceopt # whitespaceopt pragma
whitespace pragma-body pp-new-line
pragma-body:
pragma-warning-body
C# 提供 #pragma 指令以控制编译器警告。此语言将来的版本可能包含更多的 #pragma 指令。为了确保与其他 C# 编译器的互操作性,Microsoft C# 编译器对于未知的 #pragma 指令不会发出编译错误;但是这类指令确实会生成警告。
1.5.8.1 Pragma warning
#pragma warning 指令用于在编译后续程序文本的过程中禁用或恢复所有或特定的一部分警告消息tex。
pragma-warning-body:
warning
whitespace warning-action
warning
whitespace warning-action whitespace
warning-list
warning-action:
disable
restore
warning-list:
decimal-digits
warning-list whitespaceopt ,
whitespaceopt
decimal-digits
省略了警告列表的 #pragma warning 指令将影响所有警告。包含警告列表的 #pragma warning 指令只影响该列表中列出的警告。
#pragma warning disable 指令将禁用所有警告或给定的一组警告。
#pragma warning restore 指令将所有警告或指定警告恢复为在编译单元的开始处有效的状态。请注意,如果在外部禁用了特定的警告,则 #pragma warning restore(无论是恢复所有警告还是恢复特定警告)将不会重新启用该警告。
下面的示例演示的是使用 #pragma warning 临时禁用在通过 Microsoft C# 编译器中的警告编号引用已过时的成员时报告的警告。
using System;
class Program
{
[Obsolete]
static void Foo() {}
static void Main() {
#pragma warning disable 612
Foo();
#pragma warning restore 612
}
}