第 28 講:預處理指令
(Preprocessor),是在程序編譯期間,特別告訴編譯器一些特殊行為的指令。這些指令一定是以井號開頭,且以單行作為單位書寫,因此預處理指令也不需要添加分號結(jié)尾。
預處理指令可以被劃分為如下幾類:
條件編譯指令:
#if
、#endif
、#else
、#elif
、#define
、#undef
引發(fā)警告和錯誤指令:
#warning
、#error
行號指示指令:
#line
代碼折疊展開指令:
#region
、#endregion
消除警告信息指令:
#pragma warning
預處理指令的內(nèi)容其實并不多。雖然也分成很多類,但是介紹其實并不是很多,因此打算將它們?nèi)繉懺谶@一篇文章里。下面我們來看看它們的用法。
實際上,還存在一些別的預處理指令,例如
#pragma checksum
。但是這個玩意兒對于初學來說,用處非常小,它一般是提供給“代碼生成代碼”過程所需,檢查文件是否有變動。
Part 1 條件編譯
1-1 條件編譯的基本用法
條件編譯(Conditional Compilation)是針對于編程環(huán)境來制定的一種特殊編譯規(guī)則。它不能通過代碼來實現(xiàn),而是通過某一個特征符號來代表當前環(huán)境是如何的。舉個例子,C 語言里的 int
的大小隨著機器的情況,而產(chǎn)生變化。條件編譯剛好可以通過設置一個符號來表達這樣的情況。當用戶在使用程序代碼的時候,如果這個環(huán)境不是這樣的,我們只需要刪除符號就可以達到切換編譯模式的效果。其中,我們使用的、用來區(qū)分不同環(huán)境的這個符號叫做條件編譯符號(Conditional Compilation Symbol)。
下面我們來詳細解釋一下條件編譯的過程。我們之前使用過指針,來完成對底層的兼容。但是很遺憾的是,C 語言的 int
和 C# 的 int
貌似并不能夠直接劃等號。C 語言的 int
是不定長的,因為它隨著系統(tǒng)的位數(shù)有不同的情況;而 C# 里,int
是固定大小。那么,我們不得不在 C# 里指定大小,避免程序的崩潰。
比如這樣的函數(shù)。在導入的時候,int*
和 int
都必須要指定長度,因此,我們需要借助 MarshalAs
特性指定。
則不必指定,因為它的類型是指針。指針類型的長度則和 C 里是一樣的。這樣還不夠。因為 int
大小是沒有指定的,因此我們這里需要借助一個操作了。
我們在 int length
的周圍添加這樣一段代碼,使得整個函數(shù)的聲明改成這樣:
#if TARGET_64BITS
、#else
和 #endif
包裝了整個參數(shù);而且我們還寫上了兩個截然不同的“方案”,一個是用的 long
,而另外一個則是用的 int
類型。這個作用是什么呢?用戶自己是知道你電腦是什么系統(tǒng)、什么位數(shù)的。如果你是 64 位,那么你可以去配置一個叫做 TARGET_64BITS
的符號到項目配置里,然后,C# 自動就會認為 TARGET_64BITS
這個符號存在,那么 #if TARGET_64BITS
和 #else
之間的這段代碼就會得到編譯,而 #else
和 #endif
之間的部分就在 C# 編譯器編譯代碼的時候忽略掉了。
我們在項目配置菜單里找到“Conditional compilation symbols”(條件編譯符號),然后填入 TARGET_64BITS
,保存后關(guān)閉。

接著你就可以在你的代碼里,條件編譯的部分,代碼會有著色,但是不編譯的部分,就不會有著色(是灰色的)。

我們可以通過自行的配置,將代碼傳給所有人使用的時候,按自己的需求去更替和修改條件編譯符號,然后達到編譯不同的代碼的效果。
當然了,你可能會問我,這些符號是隨便寫的嗎?如果是的話,我怎么知道代碼里用到了什么符號呢?這個問題其實解釋起來很簡單:這個是寫代碼的這個人必須給出來的。你如果要用,那么就可以參考符號的用途,來配置或取消配置這個符號。
1-2 臨時配置編譯符號
條件編譯符號的靈活之處在于它并不一定非得在項目配置菜單里,我們完全可以將符號寫在代碼里,然后提供用戶到底是否是用,還是不用。
我們使用 #define
指令來完成。我們在代碼文件的最開頭添加 #define TARGET_64BITS
來啟用這個符號。
這樣也可以。你甚至可以不用去配置菜單里添加符號,就可以做到。但是前提是,這個符號在 #define
當然了,你也可以用 #undef
來手動取消對某個符號的啟用。假設你配置了這個符號,你可以使用 #undef 符號名
的格式來取消符號的定義。
1-3 多條件編譯符號的判斷
因為 C 語言的 int
類型隨系統(tǒng)位數(shù)變化而變更大小,而系統(tǒng)位數(shù)還可能是 16 位,所以我們可以這么去操作那段代碼:
我們使用 #elif
指令,可以對前文的條件編譯符號不存在的時候,繼續(xù)判斷另外的符號,這相當于 else
-if
組合的條件判斷。
elif 是 else 和 if 兩個單詞的拼接。
Part 2 引發(fā)自定義警告和錯誤
有些時候我們可能需要一點必要的手段來提供給用戶,不要隨意和濫用編譯符號。比如前面的例子里,如果有些用戶故意三個符號都不配置的話,那么這個參數(shù)就留空了。那么前面的參數(shù)最后跟了一個逗號,這必然會產(chǎn)生很嚴重的錯誤。
我們可以這樣。如果三個符號都不配置,我們就在最后一個代碼段落里添加一個 #error
指令,來提供給用戶。
#error You should specify one symbol in 'TARGET_16BITS', 'TARGET_32BITS' and 'TARGET_64BITS'.
看起來很長,實際上后面的文字是我們自定義的,這表示錯誤信息。當如果不配置上面的三個符號的話,第 8 行自動會產(chǎn)生一個編譯器錯誤,告知用戶“你不能這么用”。
那前面逗號的編譯錯誤呢?沒事,用戶可能會因為別的原因,刪掉那個逗號來避免編譯錯誤,但這是很危險的行為;所以單獨給它設置一條錯誤信息,可以提供給用戶,告訴用戶你不可以這么用。
在實際代碼編譯的時候,因為你正常的配置符號,因此前面三條代碼會挨個判斷,#error
指令完全碰不著。因此,你也不用擔心,這段代碼是否會永久遇到這個編譯錯誤指令。
Part 3 人為變動行號和文件信息
我們簡單說一下就可以了。這個符號對入門來說,也沒啥大用。#line
指令表示,我們從添加這個指令開始,這一行的編號就是指定的數(shù)值了。
using System;
的話,顯然 Console
就不知道在哪里了,于是編譯器就會產(chǎn)生錯誤信息:找不到 Console

不過,你查看詳細錯誤信息的時候,在最后你會發(fā)現(xiàn),行號改成 300 了。

這個就是這個符號的用法。我們可以指定錯誤編號在哪里,來故意告知用戶所在位置是一些特殊位置。你甚至可修改文件名稱:
然后,你在查看詳情的時候:

這個 #line
指令就是這個用法。
Part 4 給代碼增加額外的折疊塊
如果代碼非常復雜,我們可能會用到 #region
和 #endregion
指令。#region
和 #endregion
指令是成對出現(xiàn)的,和 #if
以及 #endif
是一樣的,都得成對出現(xiàn)。
舉個例子,我們用上之前的求質(zhì)數(shù)的代碼。我們在三大段代碼之間分別加上 #region
和 #endregion
指令。
#region


這個指令就是這么用的。我們可以人為指定代碼塊,然后給它寫上文字;到時候,代碼就可以通過 VS 提供的功能進行代碼的折疊;另外,折疊之后,那個我們寫在 #region
指令后面的文字是可以在折疊的時候呈現(xiàn)出來的。當然了,這個文字是可以不寫的。
Part 5 總結(jié)
總的來說,預處理指令并不是很重要,但是對一些代碼起著非常重要的作用。如果沒有它們,我們可能無法做到一些行為;這些行為可能就是為了補足和完善代碼書寫過程。