C# Language Specification 1.2 之二 词法结构

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

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

block:
{   statement-listopt   }

是以下产生式的简短形式

block:
{   }
{   statement-list   }

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

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

real-type-suffix: 下列之一
F  f  D  d  M  m

是以下产生式的简短形式

real-type-suffix:
F
f
D
d
M
m

1.2.2 词法文法

C# 的词法文法在第 2.32.3.3 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:
回车符 (U+000D)
换行符 (U+000A)
回车符 (U+000D) 后跟换行符 (U+000A)
下一行符 (U+0085)
行分隔符 (U+2028)
段落分隔符 (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:
new-line-character 外的任何 Unicode 字符

new-line-character:
回车符 (U+000D)
换行符 (U+000A)
下一行符 (U+0085)
行分隔符 (U+2028)
段落分隔符 (U+2029)

delimited-comment::
/*   delimited-comment-textopt   asterisks   /

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

delimited-comment-section::
not-asterisk
asterisks   not-slash

asterisks::
*
asterisks  
*

not-asterisk:
* 外的任何 Unicode

not-slash:
/ 外的任何 Unicode 字符

注释不嵌套。字符序列 /* */ // 注释中没有任何特殊含义字符序列 // /* 在带分隔符的注释中没有任何特殊含义。

在字符和字符串内不处理注释。

在以下示例中

/* 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:
任何含 Unicode Zs 的字符
水平制表符 (U+0009)
垂直制表符 (U+000B)
换页符 (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 标准附件 15 推荐的规则但以下情况除外允许将下划线用作初始字符这是 C 编程语言的传统),允许在标识符中使用 Unicode 转义序列以及允许@字符作为前缀以使关键字能够用作标识符。

identifier:
available-identifier
@   identifier-or-keyword

available-identifier:
不是 keyword identifier-or-keyword

identifier-or-keyword:
identifier-start-character   identifier-part-charactersopt

identifier-start-character:
letter-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:
LuLlLtLmLo Nl Unicode 字符
表示类 LuLlLtLmLo Nl 的字符的 unicode-escape-sequence

combining-character:
Mn Mc Unicode 字符
表示类 Mn Mc 的字符的 unicode-escape-sequence

decimal-digit-character:
Nd Unicode 字符
表示类 Nd 的字符的 unicode-escape-sequence

connecting-character: 
Pc Unicode 字符
表示类 Pc 的字符的 unicode-escape-sequence

formatting-character: 
Cf Unicode 字符
表示类 Cf 的字符的 unicode-escape-sequence

有关上面提到的 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:  下列之一
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

在文法中的某些位置特定的标识符有特殊的含义但不是关键字。例如在属性声明中,“getset标识符有特殊的含义 10.6.2 。在这些位置决不允许使用 getset 以外的标识符,因此这种用法不会与将这些字用作标识符冲突。

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 整数

整数用于编写类型 intuintlong 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:  下列之一
0  1  2  3  4  5  6  7  8  9

integer-type-suffix:  下列之一
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:  下列之一
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.

·         如果该整数带有后缀 ULUluLulLULulU lu则它属于 ulong 类型。

如果整数表示的值超出了 ulong 类型的范围则将发生编译时错误。

作为书写的风格样式),建议在书写类型 long 的文本时使用L而不是l”,因为字母l容易与数字1混淆。

为允许尽可能小的 int long 值写为十进制整数有下面两个规则

·         当具有值 2147483648 (231) 且没有 integer-type-suffix 的一个 decimal-integer-literal 作为标记紧接在一元负运算符标记 7.6.2 后出现时结果为具有值 −2147483648 (−231) int 类型常量。在所有其他情况下,这样的 decimal-integer-literal 属于 uint 类型。

·         当具有值 9223372036854775808 (263) 的一个 decimal-integer-literal没带 integer-type-suffix或带有 integer-type-suffix L l作为一个标记紧跟在一个一元负运算符标记 7.6.2 后出现时结果是具有值 −9223372036854775808 (−263) long 类型的常量。在所有其他情况下这样的 decimal-integer-literal 属于 ulong 类型。

1.4.4.3 实数

实数用于编写类型 floatdouble 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:  下列之一
+  -

real-type-suffix:  下列之一
F  f  D  d  M  m

如果未指定 real-type-suffix则实数的类型为 double。否则实数类型后缀确定实数的类型如下所示

·         F f 为后缀的实数的类型为 float。例如,实数 1f1.5f1e10f123.456F 的类型都是 float

·         D d 为后缀的实数的类型为 double。例如,实数 1d1.5d1e10d123.456D 的类型都是 double

·         M m 为后缀的实数的类型为 decimal。例如,实数 1m1.5m1e10m123.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:
' (U+0027)\ (U+005C) new-line-character 外的任何字符

simple-escape-sequence:  下列之一
\'  \"  \\  \0  \a  \b  \f  \n  \r  \t  \v

hexadecimal-escape-sequence:
\x   hex-digit   hex-digitopt   hex-digitopt   hex-digitopt

character 中跟在反斜线字符 (\) 后面的字符必须是以下字符之一'"\0abfnrtuUxv。否则,就会发生编译时错误。

十六进制转义序列表示单个 Unicode 字符它的值由\x后接十六进制数组成。

如果一个字符表示的值大于 U+FFFF则将发生编译时错误。

字符中的 Unicode 字符转义序列 2.4.1 必须在 U+0000 U+FFFF 的范围内。

一个简单转义序列表示一个 Unicode 字符编码,详见下表。

 

转义序列

字符名称

Unicode 编码

\'

单引号

0x0027

\"

双引号

0x0022

\\

反斜杠

0x005C

\0

0x0000

\a

警报

0x0007

\b

退格符

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:
" (U+0022)\ (U+005C) 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:
" 外的任何字符

quote-escape-sequence:
""

regular-string-literal-character 中跟在反斜杠字符 (\) 后面的字符必须是以下字符之一'"\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 是跨多行的逐字字符串。引号之间的字符(包括空白,如换行符等)也逐字符保留。

由于十六进制转义序列可以包含数目可变的十六进制数字因此字符串 "\x123" 只包含一个具有十六进制值 123 的字符。若要创建一个包含具有十六进制值 12 的字符,后跟一个字符 3 的字符串,可以改写为 "\x00123""\x12" + "3"

string-literal 的类型为 string

每个字符串不一定产生新的字符串实例。当根据字符串相等运算符 7.9.7 确认为相等的两个或更多个字符串出现在同一个程序集中时这些字符串引用相同的字符串实例。例如

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

产生的输出为 True这是因为两个字符串引用相同的字符串实例。

1.4.4.6 空文本

null-literal:
null

null-literal 的类型为 null 类型。

1.4.5 运算符和标点符号

有若干种运算符和标点符号。运算符在表达式中用于描述涉及一个或多个操作数的运算。例如,表达式 a + b 使用 + 运算符添加两个操作数 ab。标点符号用于分组和分隔。

operator-or-punctuator:  下列之一
{    }      [      ]      (      )      .      ,      :      ;
+    -      *      /      %      &      |      ^      !      ~
=    <      >      ?      ++     --     &&     ||     <<      >>
==       !=     <=     >=     +=     -=     *=     /=     %=     &=
|=       ^=     <<= >>= ->

1.5 预处理指令

预处理指令提供按条件跳过源文件中的节、报告错误和警告条件以及描绘源代码的不同区域的能力。使用术语“预处理指令”只是为了与 C C++ 编程语言保持一致。在 C# 中没有单独的预处理步骤;预处理指令按词法分析阶段的一部分处理。

pp-directive:
pp-declaration
pp-conditional
pp-line
pp-diagnostic
pp-region

下面是可用的预处理指令

·         #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

预处理指令总是占用源代码中的单独一行并且总是以 # 字符和预处理指令名称开头。# 字符的前面以及 # 字符与指令名称之间可以出现空白符。

包含 #define#undef#if#elif#else#endif #line 指令的源代码行可以用单行注释结束。在包含预处理指令的源行上不允许使用带分隔符的注释(/* */ 样式的注释)。

预处理指令既不是标记也不是 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:
true false 外的任何 identifier-or-keyword

条件编译符号具有两种可能的状态已定义 (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.15 相同唯一的例外是在这里唯一可引用的用户定义实体是条件编译符号。

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:
# 外的任何 input-character

按照语法的规定条件编译指令必须写成集的形式集的组成依次为一个 #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-section 进行处理除了预处理指令节中的源代码不必一定要符合词法文法不从节中的源代码生成任何词法标记节中的预处理指令必须在词法上正确但不另外处理。在按 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 行指令

行指令可用于改变编译器在输出如警告和错误中报告的行号和源文件名称。

行指令最通用于从某些其他文本输入生成 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:
" 外的任何 input-character

当不存在 #line 指令时编译器在它的输出中报告真实的行号和源文件名称。#line 指令最通用于从某些其他文本输入生成 C# 源代码的元编程工具。当处理的 #line 指令包含不是 defaultline-indicator 时,编译器将该指令“后面”的行视为具有给定的行号(如果指定了,还包括文件名)。

#line default 指令消除前面所有 #line 指令的影响。编译器报告后续行的真实行信息就像尚未处理任何 #line 指令一样。

#line hidden 指令对错误信息中报告的文件号和行号无效但对源代码级调试确实有效。调试时#line hidden 指令和后面的 #line 指令不是 #line hidden之间的所有行都没有行号信息。在调试器中逐句执行代码时,将全部跳过这些行。

注意file-name 与常规字符串的不同之处在于不处理转义字符;“\字符在 file-name 中只是表示一个普通的反斜杆字符。

转载于:https://www.cnblogs.com/gfsoft/archive/2006/08/03/466826.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值