1. 词法结构

1.1 程序

C# 程序 (program) 由一个或多个源文件 (source file) 组成,源文件的正式名称是编译单元 (compilation unit)(第 9.1 节)。源文件是有序的 Unicode 字符序列。源文件与文件系统中的文件通常具有一对一的对应关系,但这种对应关系不是必需的。为实现可移植性的最大化,建议这些文件在文件系统中应按 UTF-8 编码规范编码。

从概念上讲,程序的编译分三个步骤:

  1. 转换,这一步将用特定字符指令系统和编码方案编写的文件转换为 Unicode 字符序列。
  2. 词法分析,这一步将 Unicode 输入字符流转换为标记流。
  3. 句法分析,这一步将标记流转换为可执行代码。

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 组成。换言之,定义是递归的,语句列表由一个或多个语句组成。

一个符号若以下标“”作其后缀,就表明该符号是可选的。产生式:

block:
{  
statement-list   }

是以下产生式的简短形式:

block:
{   }
{  
statement-list   }

它定义了一个 block,此块由一个用“{”和“}”标记括起来的可选 statement-list 组成。

可选项通常在单独的行上列出,但是当有许多可选项时,可以在单行上给定的扩展列表之后加上短语“之一”。这只是在单独一行上列出每个可选项的简短形式。例如,产生式:

real-type-suffix:  one of

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-section

input-section:
input-section-part
input-section   input-section-part

input-section-part:
input-elements   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-characters

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-text  
asterisks   /

delimited-comment-text:
delimited-comment-section
delimited-comment-text  
delimited-comment-section

delimited-comment-section:
/
asterisks  
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-characters

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-suffix

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-suffix
0X   hex-digits  
integer-type-suffix

hex-digits:
hex-digit
hex-digits   hex-digit

hex-digit:  one of

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 (2) 且没有 integer-type-suffix 的一个 decimal-integer-literal 作为标记紧接在一元负运算符标记(第 7.7.2 节)后出现时,结果为具有值 −2147483648 (−2) 的 int 类型常量。在所有其他情况下,这样的 decimal-integer-literal 属于 uint 类型。
  • 当具有值 9223372036854775808 (2)
    的一个 decimal-integer-literal(没带 integer-type-suffix,或带有 integer-type-suffix L 或 l)作为一个标记紧跟在一个一元负运算符标记(第 7.7.2 节)后出现时,结果是具有值 −9223372036854775808 (−2) 的 long 类型的常量。在所有其他情况下,这样的 decimal-integer-literal 属于 ulong 类型。

1.4.4.3 实数

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

real-literal:
decimal-digits   .   decimal-digits   exponent-part   real-type-suffix
.  
decimal-digits   exponent-part   real-type-suffix
decimal-digits   exponent-part   real-type-suffix
decimal-digits   real-type-suffix

exponent-part:
e  
sign  
decimal-digits
E   sign   decimal-digits

sign:  one of

-

real-type-suffix:  one of

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-digit   hex-digit   hex-digit

在 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-characters   "

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-characters   "

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:
whitespace  
pp-or-expression   whitespace

pp-or-expression:
pp-and-expression
pp-or-expression   whitespace   ||  
whitespace  
pp-and-expression

pp-and-expression:
pp-equality-expression
pp-and-expression   whitespace   &&  
whitespace  
pp-equality-expression

pp-equality-expression:
pp-unary-expression
pp-equality-expression   whitespace   ==  
whitespace  
pp-unary-expression
pp-equality-expression   whitespace   !=  
whitespace  
pp-unary-expression

pp-unary-expression:
pp-primary-expression
!  
whitespace  
pp-unary-expression

pp-primary-expression:
true
false
conditional-symbol
(  
whitespace  
pp-expression   whitespace   )

当在预处理表达式中引用时,已定义的条件编译符号具有布尔值 true,未定义的条件编译符号具有布尔值 false。

预处理表达式的计算总是产生一个布尔值。预处理表达式的计算规则与常量表达式(第 7.19 节)相同,唯一的例外是:在这里,唯一可引用的用户定义实体是条件编译符号。

1.5.3 声明指令

声明指令用于定义或取消定义条件编译符号。

pp-declaration:
whitespace   #   whitespace   define  
whitespace  
conditional-symbol   pp-new-line
whitespace   #   whitespace   undef  
whitespace  
conditional-symbol   pp-new-line

pp-new-line:
whitespace  
single-line-comment  
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-sections   pp-else-section   pp-endif

pp-if-section:
whitespace   #   whitespace   if  
whitespace   pp-expression   pp-new-line  
conditional-section

pp-elif-sections:
pp-elif-section
pp-elif-sections   pp-elif-section

pp-elif-section:
whitespace   #   whitespace   elif  
whitespace   pp-expression   pp-new-line  
conditional-section

pp-else-section:
whitespace   #   whitespace   else  
pp-new-line   conditional-section

pp-endif:
whitespace   #   whitespace   endif  
pp-new-line

conditional-section:
input-section
skipped-section

skipped-section:
skipped-section-part
skipped-section   skipped-section-part

skipped-section-part:
skipped-characters  
new-line
pp-directive

skipped-characters:
whitespace  
not-number-sign   input-characters

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:
whitespace   #   whitespace   error  
pp-message
whitespace   #   whitespace   warning  
pp-message

pp-message:
new-line
whitespace   input-characters   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-section   pp-end-region

pp-start-region:
whitespace   #   whitespace   region  
pp-message

pp-end-region:
whitespace   #   whitespace   endregion  
pp-message

区域不具有任何附加的语义含义;区域旨在由程序员或自动工具用来标记源代码中的节。#region 或 #endregion 指令中指定的消息同样不具有任何语义含义;它只是用于标识区域。匹配的 #region 和 #endregion 指令可能具有不同的 pp-message。

区域的词法处理:

#region
...
#endregion

与以下形式的条件编译指令的词法处理完全对应:

#if true
...
#endif

1.5.7 行指令

行指令可用于变更编译器在输出(如警告和错误)中报告的行号和源文件名称,以及调用方信息特性(第 17.4.4 节)所使用的行号和源文件名称。

行指令最常用于从某些其他文本输入生成 C# 源代码的元编程工具。

pp-line:
whitespace   #   whitespace   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:
whitespace   #   whitespace   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   whitespace   ,  
whitespace  
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
}
}

04-14 22:21