C# Language Specification 5.0 (翻譯)第二章 詞法結構


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)來表示。

詞法產生式的第一行被定義為非終結符號的名稱,而后緊跟着一個冒號 :。緊接這一行之后的連續幾行相同縮進風格的是由非終結符號或終結符號構成的擴展。比方說下面這個詞法產生式:

while-statement

上面定義的這個 while-statement 包含了一個 while 標記,之后分別跟着 (boolean-expression)embedded-statement。當有超過一個非終結符擴展時,將它們交替列出,每一個擴展獨占一行。如下面這個詞法產生式:

statement-list

上面定義的 statement-list 包含了一個 statement 以及一個緊隨其后的 statement-list。換句話說,所定義的 statement-list 是遞歸的,它包含至少一個 statement

下標 “opt” 被用於標記可選符號(optional symbol)。下面這個詞法產生式,

block

是下面這個詞法產生式的縮寫形式:

block

它定義了一個區塊,用大括號標記 {...} 包裹起一個可選的 statement-list

像這種每行一個進行列舉選項的形式,也可以通過使用 one of 將這些擴展列表寫進一行里。也就是說這是一種在一行中顯示這些選項的縮寫形式。比方說下面這個詞法產生式,

real-type-suffix-one-of

它的縮寫形式如下:

real-type-suffix

詞法文法

關於詞法文法(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)。

input

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# 源文件的字符分割為多行。

new-line

為了與增加有 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)。單行注釋以字符 // 起始、延續並終於該行之尾。分割注釋始於字符 /* 並止於字符 */,分割注釋支持跨行。

comment

注釋不可嵌套(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)。

whitespace


標記

有以下幾種標記(tokens):標識符(identifiers)、關鍵字(keywords)、文本(literals)、操作符(operators)和標點符號(punctuators)。空白(white space)與注釋(comments)並非標記,它們只是標記的分隔符。

token

Unicode 字符轉義序列

Unicode 字符轉義序列(Unicode character escape sequence)表示 Unicode 字符。Unicode 字符轉義序列處理於標識符(第二章第4.2節)、字符文本(character literals,第二章第4.4.4節)以及正則字符串文本(regular string literals,第二章第4.4.5節)內。除此以外,Unicode 字符轉義不會在其他任何地方被處理(比如在構成操作符、標點符或關鍵字時)。

unicode-escape-sequence

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. @ 符號可以用於關鍵字的前綴以便使其可為標識符。

identifier

上面所提及的 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)的保留字符序列,除開以 @ 開頭外,其它關鍵字不能用作標識符。

keyword

在文法(grammar)的一些地方,指定的識別符有着指定的含義,但這些並不是關鍵字。這些識別符有時用作「上下文關鍵字(contextual keywords)」。比方說在一個屬性聲明中,getset 標識符有着指定含義(見第十章第 7.2 節),而其它的標識符則不能用在這個地方,所以在這個地方將這些詞當作標識符使用並不會發生沖突。在其它情況下,標識符 var 隱式聲明了局部變量(第八章第 5.1 節),上下文關鍵字可能與聲明名稱相沖突[3]。在這種情況下,聲明名的優先級將高於用作上下文關鍵字的標識符。

文本

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

literal

布爾值

布爾值文本(boolean literal)有 truefalse 兩種值。

boolean-literal

boolean-literal 的類型是 bool(布爾值)。

整數

整數文本(integer literals)被用於寫作 int、uint、long 和 ulong 類型的值整數文本有兩個可能的形式:十進制(decimal)和十六進制(hexadecimal)。

integer-literal

整數文本允許的類型如下:

  • 如果文本沒有后綴(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 類型的值。

real-literal

如果沒有指定 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'

character-literal

跟在反斜杠字符 \ 之后的字符必須是下列字符中的一個,否則會出現「編譯時(compile-time)錯誤」:'"\0abfnrtuUxv

十六進制轉義序列(A hexadecimal escape sequence)表示單個 Unicode 字符,用 \x 后面跟着一個十六進制數的形式表示。

如果一個字符文本的值大於 U+FFFF,會出現「編譯時(compile-time)錯誤」。

字符文本中的 Unicode 字符轉義序列(第二章第4.1節)必須在 U+0000U+FFFF 區間內。

單個轉義序列(simple escape sequence)表示一個 Unicode 字符編碼,如下表所述:

character-literal

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)。

string-literal

反斜杠 \ 后跟着一個字符,這個組合如果在正則字符串文本字符(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

null-literal

null-literal 能隱式地轉換為一個引用類型或一個可空類型。

操作符與標點符

操作符與標點符有好幾種類型。操作符用在表達式內,用於描述操作調用中一個或多個操作數。比方說表達式 a + b 使用了操作符 + 去把兩個操作數 ab 加起來。標點符則用來分組和分隔。

operator-or-punctuator

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)。

pp-directive

下面列舉了可用的預處理指令:

  • #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)。

conditional-symbol

條件編譯符號有已定義(defined)未定義(undefined)這兩種狀態。在源文件的詞法分析剛開始的時候,條件編譯符號是未定義狀態(除非它已被顯式地被外部機制(external mechanism,諸如命令行編譯選項)所定義的)。當 #define 指令被處理,指令中的命名的條件編譯符號將會在源文件中定義。符號將保持被定義狀態直到直到相同符號的 #undef 指令(具有相同符號的成對指令)被處理或到達源文件的結尾,這也就意味着在同一源文件中的 #define#undef 指令將不會影響到同程序中的其它源文件。

當它被引用在預處理表達式(pre-processing expression)內,已定義的條件編譯符號有一個 true 值,未被定義的條件編譯符則是一個 false 值。不強制要求在預處理表達式之前顯式聲明條件編譯符,相反,未聲明的符號只是未定義的而已,它依舊有個 false 值。

條件編譯符號的命名空間明確且獨立於其它的命名實體,條件編譯符號只能被用在 #define#undef 指令之間或在預處理表達式內。

預處理表達式

預處理表達式(Pre-processing expressions)可以出現在 #if#endif 指令之間。操作符 !==!=&& 以及 || 都可以放進預處理表達式內,括號 (...) 可以用於分組(grouping)。

pp-expression

當引用了一個預處理表達式,已定義的條件編譯符號將有個 true 的布爾值,而未定義的條件編譯符號則有個 false 的布爾值。

預處理表達式的計算結果總是一個布爾值,其計算規則則與常量表達式(constant expression,第七章第十九節)是一樣的,唯一的例外是此處唯一可引用的用戶自定義實體(user-defined entities)是條件編譯符。

聲明指令

聲明指令被用於定義(define)或取消定義(undefine)條件編譯符號(conditional compilation symbols)。

pp-declaration

經過 #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

條件編譯指令

條件編譯指令可以有選擇地包含或排除源文件。

pp-conditional

如上面語法中所指出那般,條件編譯指令必須被寫以由一組有序包含了 #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)警告」。

pp-diagnostic

例如:

#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)。

pp-region

區域(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)工具使用行指令,可以對從其它文本輸入中生成相應的源代碼。

pp-line

當沒有 #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)。

pp-pragma

C# 所提供的 #pragma 指令可以控制編譯器警告(warnings),語言的后續版本則將包含更多 #pragma 指令。為確保它與其它 C# 編譯器的互操作性(interoperability),微軟 C# 編譯器不會在編譯時對未知的 #pragma 指令報錯(errors),頂多給出警告(warnings)。

編譯指示警告

#pragma warning 指令通常用於在編譯期間對隨后的程序編碼禁用(disable)或恢復(restore)指定的某條或全部的警告信息。

pragma-warning-body

用於忽略警告列表的 #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 翻譯計划


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM