預處理指令
什么是預處理指令
源代碼指定了程序的定義,預處理指令(preprocessor directive)指示編譯器如何處理源代碼。例如,在某些情況下,我們可能希望編譯器忽略一部分代碼,而在其他情況下,我們可能希望代碼被編譯。預處理指令給了我們這樣的選項。
在C和C++中有實際的預處理階段,此時預處理程序遍歷源代碼並且為之后的編譯階段准備文本輸出流,在C#中沒有實際的預處理程序。“預處理”指令由編譯器來處理,而這個術語保留了下來。
基本規則
下面是預處理指令的最重要的一些語法規則。
- 預處理指令必須和C#代碼在不同的行
- 與C#語句不同,預處理指令不需要以分號結尾
- 包含預處理指令的每一行必須以#字符開始
- 在#字符前可以有空格
- 在#字符和指令之間可以有空格
- 允許行尾注釋
- 在預處理指令所在的行不允許分隔符注釋
這里的一些示例闡釋了這些規則:
#define PremiumVersion //沒有分號 正確 #define BudgetVersion //前面有空格 正確 # define MediumVersion //中間有空格 正確 不允許分隔符注釋 ↓ #define PremiumVersion /* all bells & whistles */ 錯誤 #define BudgetVersion // Stripped-down version 允許行尾注釋 正確
#define和#undef指令
編譯符號是只有兩種可能狀態的標識符,要么被定義,要么未被定義。編譯符號有如下特性。
- 它可以是除了true或false以外的任何標識符,包括C#關鍵字,以及在C#代碼中聲明的標識符,這兩者都是可以的
- 它沒有值。與C和C++不同,它不表示字符串
如上表所示:
#define
指令聲明一個編譯符號#undef
指令取消定義一個編譯符號
#define PremiumVersion #define EconomyVersion … #undef PremiumVersion
#define
和#undef
指令只能用在源文件的第一行,也就是任何C#代碼之前使用。在C#代碼開始后,#define
和#undef
指令就不能再使用。
using System; // C#代碼的第1行 #define PremiumVersion //錯誤 namespace Eagle { #define PremiumVersion //錯誤 …
編譯符號的范圍被限制於單個源文件。只要編譯符號在任何C#代碼之前,重復定義已存在的編譯符號也是允許的。
#define AValue #define BValue #define AValue //重復定義
條件編譯
條件編譯允許我們根據某個編譯符號是否被定義標注一段代碼被編譯或跳過。
有4個指令可以用來指定條件編譯:
#if
#else
#elif
#endif
條件是一個返回true或false的簡單表達式。
- 如下表所總結的,條件可以由單個編譯符號、符號表達式或操作符組成。子條件可以使用圓括號分組
- 文本true或false也可以在條件表達式中使用
如下是一個條件編譯的示例:
表達式 ↓ #if !DemoVersion … #endif 表達式 ↓ #if(LeftHanded && OemVersion)||完整版 … #endif #if true //下面的代碼片段總是會被編譯 … #endif
條件編譯結構
指令在條件編譯結構中需要配對使用。只要有#if
指令,就必須有配對的#endif
。#if
和#if ...#else
結構如下圖所示。
- 如果
#if
結構中的條件運算結果為true,隨后的代碼段就會被編譯,否則就會被跳過 - 在
#if...#else
結構中,如果條件運算結果為true,CodeSection1就會被編譯,否則,CodeSection2會被編譯
例如,如下的代碼演示了簡單的#if...#else
結構。如果符號RightHanded被定義了,那么#if
和#else
之間的代碼會被編譯。否則,#else
和#endif
之間的代碼會被編譯。
… #if RightHanded //實現右邊函數的代碼 … #else //實現左邊函數的代碼 … #endif
下圖演示了#if ...#elif
以及#if ...#elif...#else
的結構。
- 在
#if...#elif
結構中;- 如果Cond1運算結果為true,CodeSectionl就會被編譯,然后就會繼續編譯
#endif
之后的代碼 - 否則,如果Cond2運算結果為true,CodeSection2就會被編譯,然后會繼續編譯
#endif
之后的代碼
- 如果Cond1運算結果為true,CodeSectionl就會被編譯,然后就會繼續編譯
- 直到條件運算結果為true或所有條件都返回false,如果這樣,結構中沒有任何代碼段會被編譯,會繼續編譯
#endif
之后的代碼 #if...#elif...#else
結構也是相同的工作方式,只不過沒有條件是true的情況下,會編譯#else
之后的代碼段,然后會繼續編譯#endif
之后的代碼
如下的代碼演示了#if...#elif...#else
結構。包含程序版本描述的字符串根據定義的編譯符號被設置為各種值。
#define DemoVersionWithoutTimeLimit … const int intExpireLength = 30; string strVersionDesc = null; int intExpireCount = 0; #if DemoVersionWithTimeLimit intExpireCount = intExpireLength; strVersionDesc = "This version of Supergame Plus will expire in 30 days"; #elif DemoVersionWithoutTimeLimit strVersionDesc = "Demo Version of Supergame Plus"; #elif OEMVersion strVersionDesc = "Supergame Plus, distributed under license"; #else strVersionDesc = "The original Supergame Plus!!"; #endif Console.WriteLine( strVersionDesc ); …
診斷指令
診斷指令產生用戶自定義的編譯時警告及錯誤消息。
下面是診斷指令的語法。Message是字符串,但是需要注意,與普通的C#字符串不同,它們不需要被引號包圍。
#warning Message #error Message
當編譯器遇到診斷指令時,它會輸出相關的消息。診斷指令的消息會和任何編譯器產生的警告和錯誤消息列在一起。
例如,如下代碼顯式了一個#error
指令和一個#warning
指令。
#error
指令在#if
結構中,閃此只有符合#if
指令的條件時才會生成消息#warning
指令用於提醒程序員回頭來清理這段代碼
#define RightHanded #define LeftHanded #if RightHanded && LeftHanded #error Can't build for both RightHanded and LeftHanded #endif #warning Remember to come back and clean up this code!
行號指令
行號指令可以做很多事情,諸如:
- 改變由編譯器警告和錯誤消息報告的出現行數
- 改變被編譯源文件的文件名
- 對交互式調試器隱藏一些行
#line
指令的語法如下:
#line integer //設置下一行值為整數的行的行號 #line "filename" //設置文件名 #line default //重新保存實際的行號和文件名 #line hidden //在斷點調試器中隱藏代碼 #line //停止在調試器中隱藏代碼
#line
指令加上一個整數參數會使編譯器認為下面代碼的行是所設置的行,之后的行數會在這個行數的基礎上遞增。
- 要改變外觀文件名,可以在雙引號內使用文件名作為參數。雙引號是必需的
- 要返回真實行號和真實文件名,可以使用default參數
- 要對交互調試器的斷點調試功能隱藏代碼段,可以使用hidden作為參數。要停止隱藏,可以使用不帶任何參數的指令。到目前為止,這個功能大多用於在ASP.NET和WPF中隱藏編譯器生成的代碼。
下面的代碼給出了行號指令的示例:
#line 226 x=y+z; //編譯器將執行第226行 … #line 330 "SourceFile.cs"//改變報告的行號和文件名 var1=var2+var3; … #line default //重新保存行號和文件名
區域指令
區域指令允許我們標注和有選擇性地命名一段代碼。#region
指令的特性如下:
- 被放置在希望標注的代碼段之上
- 用指令后的可選字符串文本作為其名字
- 在之后的代碼中必須由
#endregion
指令終止
盡管區域指令被編譯器忽略,但它們可以被源代碼工具所使用。例如,Visual Studio允許我們很簡單地隱藏或顯式區域。
作為示例,下面的代碼中有一個叫做Constructors的區域,它封閉了MyClass類的兩個構造函數。在Visual Studio中,如果不想看到其中的代碼,我們可以把這個區域折疊成一行,如果又想對它進行操作或增加另外一個構造函數,還可以擴展它。
#region Constructors MyClass() { … } MyClass(string s) { … } #endregion
如下圖所示,區域可以被嵌套。
#pragma warning 指令
#pragma warning
指令允許我們關閉及重新開啟警告消息。
- 要關閉瞀告消息,可以使用disable加上逗號分隔的希望關閉的警告數列表的形式
- 要重新開啟警告消息,可以使用restore加上逗號分隔的希望關閉的警告數列表的形式
例如,下面的代碼關閉了兩個警告消息:618和414。在后面的代碼中又開啟了618警告消息,但還是保持414消息為關閉狀態。
要關閉的警告消息 ↓ #pragma warning disable 6l8, 414 … 列出的警告消息在這段代碼中處於關閉狀態 #pragma warning restore 618
如果我們使用任一種不帶警告數字列表的形式,這個命令會應用於所有警告。例如,下面的代碼關閉,然后恢復所有警告消息。
#pragma warning disable … 所有警告消息在這段代碼中處於關閉狀態 #pragma warning restore … 所有譬告消息在這段代碼中處於開啟狀態