一、什么是预处理指令
源代码指定了程序的定义,预处理指令(preprocessor directive)指示编译器如何处理源代码。例如,在某些情况下,我们希望编译器能够忽略一部分代码,而在其他情况下,我们希望代码被编译,这时我们就可以使用预处理指令了。
C++开发人员应知道,在C 和C++中预处理器指令非常重要,但是,在C#中,并没有那么多的预处理器指令,它们的使用也不太频繁。C#提供了其他机制来实现许多C++指令的功能,如定制特性。还要注意,C#并没有一个像C++那样的独立预处理器,所谓的预处理器指令实际上是由编译器处理的。尽管如此,C#仍保留了一些预处理器指令名称,因为这些命令会让人觉得就是预处理器。
二、基本规则
知道了什么是预处理指令,那么如何使用它呢?下面是它的一些重要语法规则
♥ 预处理指令必须和C#代码在不同的行
♥ 与C#语句不同,预处理指令不需要以分号结尾
♥ 包含预处理指令的每一行必须与 ‘’#‘’ 字符开始(在#字符前可以有空格,在#字符和指令之间也可以有空格)
♥ 允许行尾注释
♥ 在预处理指令所在的行不允许有分隔符注释
讲了这么多,举个栗子
#define Premium //结尾没有分号,可以有行尾注释,不允许分隔符注释,即不能有如下类型的注释/*...*/ #define Budget //前面可以有空格 # define Medium // 中间可以有空格
三、内容详解
下表列出了预处理指令及其含义概要
指令 | 含义概要 |
#define identifier | 定义编译符 |
#undef identifier | 取消编译符 |
#if expression | 如果表达式为true,编译下面的片段 |
#elif expression | 如果表达式为true,编译下面的片段 |
#else | 如果之前的#if或#elif表达式是false,编译下面的片段 |
#endif | 标记为一个#if结构的结束 |
#region name | 标记一段代码的开始,没有编译效果 |
#endregion name | 标记一段代码的结束,没有编译效果 |
#warning message | 显式编译时的警告消息 |
#error message | 显式编译时的错误消息 |
#line indicator | 修改在编译器消息中显式的行数 |
#pragma text | 指定有关程序上下文的信息 |
详细解释上面指令的用法
3.1 #define和#undef指令
编译符号是只有两种可能状态的标识符,要么被定义,要么未被定义,它可以是除了true或false以外的任何标识符,包括C#关键字
#define 的用法如下所示: #define DEBUG
它告诉编译器存在给定名称的符号,在本例中是DEBUG。这有点类似于声明一个变量,但这个变量并没有真正的值,不表示字符串,只是存在而已。
这个符号不是实际代码的一部分,而只在编译器编译代码时存在。在C#代码中它没有任何意义。
#undef 正好相反—— 它删除符号的定义: #undef DEBUG
如果符号不存在,#undef 就没有任何作用。同样,如果符号已经存在,则#define 也不起作用,允许重复定义已存在的编译符号。必须把#define 和#undef 命令放在C#源文件的开头位置,在声明要编译的任何对象的代码之前。
#define 本身并没有什么用,但与其他预处理器指令(特别是#if)结合使用时,它的功能就非常强大了。
这里应注意一般C#语法的一些变化。预处理器指令不用分号结束,一般一行上只有一条命令。这是因为对于预处理器指令,C#不再要求命令使用分号进行分隔。如果它遇到一条预处理器令, 就会假定下一条命令在下一行上。
3.2 条件编译(#if、#elif、#else 和#endif)
条件编译允许我们根据某个编译符号是否被定义标注一段代码编译或跳过,条件是一个返回true或false的简单表达式
参数类型 | 意义 | 运算结果 |
编译符号 | 使用#define指令(未)定义的标识符 |
true:标识符已使用#define定义 false:其他 |
表达式 | 使用符号和操作符!、==、!=、&&、|| 构建的 |
true:如果表达式运算结果为true false:其他 |
如上图所示,在#if和#elif指令中使用的条件可以由单个编译符号、符号表达式或操作符组成。子条件可以使用圆括号分组。 文本true或false也可以在条件表达式中使用
#define Premium #define Budget using System; namespace 预处理指令 { class Program { static void Main(string[] args) { #if Premium&&Budget Console.WriteLine("defined!"); //输出defined! #endif #if Hello Console.WriteLine("defined!"); //Hello未被定义,不输出 #else Console.WriteLine("undefined!"); //输出undefined! #endif Console.ReadKey(); } } }
当编译器遇到#if 语句后,将先检查相关的符号是否存在,如果符号存在,就编译#if 子句中的代码。否则,编译器会忽略所有的代码,直到遇到匹配的#endif 指令为止。
#elif (=else if),其用法及含义十分直观,就不举例子了
值得注意的是,#if和#endif指令在条件编译结构中必须配对使用,只要有#if指令,就必须有#endif配对
3.3 诊断指令(#warning和#error)
诊断指令产生用户自定义的编译时的警告或错误消息,它的用法为
#warning Message
Message是字符串,它与普通的C#字符串不同,不需要被引号包围
#define Premium #define Budget using System; namespace 预处理指令 { class Program { static void Main(string[] args) { #if Premium && Budget #error Can't build this code #endif #warning Remember to come back to check this code Console.ReadKey(); } } }
可以看到,编译器中出现了对应的错误和警告提醒
3.4 行号指令(#line)
行号指令可以做许多事,例如
♥ 改变由编译器警告和错误消息报告的出现行数
♥ 改变被编译源文件的文件名
♥ 对交互式调试器隐藏一些行
class MainClass { static void Main() { #line 200 "Special" //设置下一行的行号值为200,并将编译源文件的名字改成“Special” int i; int j; #line default //重新保存实际的行号和文件名 char c; float f; #line hidden // 在断点调试器中隐藏代码 string s; double d;
#line //停止在调试器中隐藏代码 } }
编译产生以下输出
Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used
仔细观察输出结果你将可以发现其中的不同,使用时要注意的是:
要改变外观文件名,可以在双引号内使用文件名作为参数,双引号是必须的
要返回真是的文件名和行号,可以使用default参数
要对交互调试器的断点调试功能隐藏代码段,可以使用hidden参数。要停止隐藏,可以使用不带任何参数的指令
#line hidden
指令能对调试程序隐藏连续行,当开发者逐行执行代码时,介于 #line hidden
和下一 #line
指令(假设它不是其他 #line hidden
指令)间的任何行都将被跳过。它不影响错误报告中的文件名或行号。 也就是说,如果在隐藏块中遇到错误,编译器将报告错误的当前文件名和行号。
3.5 区域指令(#region和#endregion)
利用 #region
,可以指定在使用 Visual Studio Code 编辑器的大纲功能时可展开或折叠的代码块。 在较长的代码文件中,能够折叠或隐藏一个或多个区域会十分便利,这样,可将精力集中于当前处理的文件部分。
#define Premium #define Budget using System; namespace 预处理指令 { #region program //名字为program class Program { static void Main(string[] args) { #line 89 "special" int c; #line default #if Budgets Console.WriteLine("defined!"); #else Console.WriteLine("undefined!"); #endif Console.ReadKey(); } } #endregion //结束标记 }
在使用#region的地方出现了可以折叠的标志
备注:
#region
块必须通过 #endregion 指令终止。
#region
块不能与 #if块重叠。 但是,可以将 #region
块嵌套在 #if
块内,或将 #if
块嵌套在 #region
块内。
#region指令后的可选字符串文本作为其名字
3.6 #pragma warning指令
#pragma warning指令允许我们关闭或重新开启warning指令
要关闭警告消息,可以使用disable加上逗号分隔希望关闭的警告数列表
要开启警告消息,可以使用restore加上逗号分隔希望开启的警告数列表
using System; #pragma warning disable 414 //关闭414行的警告 public class C { int i = 1; static void Main() { } } #pragma warning restore //所有警告消息在下面代码中处于开启状态 public class D { int i = 1; public static void F() { } }
如果还有梦就追,至少不会遗憾后悔!