C#预处理器指令
控制流语句中的条件表达式是在运行时求值的。而C#预处理器指令是在编译时调用的。预处理器指令(preprocessor directive)告诉C#编译器要编译哪些代码,并指出如何处理特定的错误和警告。C#预处理器指令还可以告诉C#编辑器有关代码组织的信息。
语言对比:C++ —— 预处理
C和C++风格的语言包含一个预处理器(preprocessor),它是独立于编译器的一个实用程序,用于对代码进行整理,根据特殊的记号来采取 特殊的行动。预处理器指令通常告诉编译器如何编译一个文件中的代码,而并不参与实际的编译过程。与此相反,C#编译器会将预处理器指令作为对源代码执行的 常规词法分析的一部分。其结果就是,C#不支持更高级的预处理器宏,它最多只允许定义常量。事实上,“预编译器”在C++中显得很贴切,但在C#中就属于 用词不当。
每个预处理器指令都以一个#开头,而且必须在一行中写完。换行符(而不是分号)标志着预处理器指令的结束。
表3-4总结了所有预处理器指令。
表3-4 预处理器指令
语句或表达式 |
常规语法结构 |
示例 |
#if指令 |
#if preprocessor-expression code #endif |
#if CSHARP2 Console.Clear(); #endif |
#define指令 |
#define conditional-symbol |
#define CSHARP2 |
#undef指令 |
#undef conditional-symbol |
#undef CSHARP2 |
#error指令 |
#error preproc-message |
#error Buggy implementation |
#warning指令 |
#warning preproc-message |
#warning Needs code review |
#pragma指令 |
#pragma warning |
#pragma warning disable 1030 |
#line指令 |
#line org-line new-line |
#line 467 "TicTacToe.cs" ... #line default |
#line default |
||
#region指令 |
#region pre-proc-message code |
#region Methods ... #endregio |
3.9.1 排除和包含代码
或许最常用的预处理器指令就是用于控制什么时候以及如何包含代码的指令。举个例子来说,要使代码能够同时由C# 2.0和之前的C# 1.2版本编译器进行编译,可以使用一个预处理器指令,在遇到1.2编译器的时候,就排除C# 2.0特有的代码。我们的tic-tac-toe例子和代码清单3-52对此进行了演示。
代码清单3-52 遇到C# 1.x编译器的时候排除C# 2.0代码
#if CSHARP2 |
在这个例子中,调用了System.Console.Clear()方法,这是只有2.0 CLI才支持的方法。使用#if和#endif预处理器指令,这一行代码就只有在定义了预处理器符号CSHARP2的前提下才会编译。
预处理器指令的另一个应用是适应不同平台之间的差异,比如用WINDOWS和LINUX #if指令将Windows和Linux特有的API包围起来。开发者经常用这些指令来取代多行注释(/*...*/),因为它们更容易通过定义恰当的符 号或者通过一次搜索/替换来移除。预处理器指令最后一个常见的用途是调试。如果用一个#if DEBUG指令将调试代码包围起来,那么在大多数IDE中,都能在最终的发布版本中移除这些代码。IDE默认将DEBUG符号用于调试编译,将 RELEASE符号用于发布版本。
为了处理else-if条件,可以在#if指令中使用#elif指令,而不是创建两个完全独立的#if块,如代码清单3-53所示。
代码清单3-53 使用#if、#elif和#endif指令
#if LINUX |
3.9.2 定义预处理器符号
可以采取两种方式来定义预处理器符号。第一种方式是使用#define指令,如代码清单3-54所示。
代码清单3-54 一个#define例子
#define CSHARP2 |
第二种方式是在为.NET编译的时候使用define选项,如输出3-27所示。
输出3-27
>csc.exe /define:CSHARP2 TicTacToe.cs |
输出3-28展示了在使用Mono编译器的前提下,如何实现相同的功能。
输出3-28
>mcs.exe -define:CSHARP2 TicTacToe.cs |
要添加多个定义,只需以分号分隔定义。使用编译器选项的优点在于,不需要更改源代码,所以可以使用相同的源代码文件来生成两套不同的
二进制程序。
要取消符号的定义,可以采取和使用#define相同的方式来使用#undef指令。
3.9.3 生成错误和警告
有的时候,或许想要标记出代码中潜在的问题。为此,可以插入#error和#warning指令来分别生成一条错误或警告消息。代码清单3-55使用tic-tac-toe例子来发出警告:代码无法防止玩家多次输入同一步棋。输出3-29展示了结果。
代码清单3-55 用#warning来定义一个警告
#warning "Same move allowed multiple times." |
输出3-29
Performing main compilation... Build complete -- 0 errors, 1 warnings |
包含#warning指令后,编译器会主动报告一条警告,如输出3-29所示。利用这种警告,可以标记出代码中可能存在的增强或者bug。它是提醒开发者任务尚未完结的好帮手。
3.9.4 关闭警告消息
警告可以指出代码中可能存在的问题,所以非常有用。然而,有的警告可以忽略,所以有必要明确地关闭它们。C# 2.0提供了预处理器指令#pragma来做到这一点,如代码清单3-56所示。
代码清单3-56 使用预处理器指令#pragma来禁用一个警告
#pragma warning disable 1030 |
注意,在编译器实际输出的时候,会在警告编号之前附加CS前缀。然而,在用#pragma禁用一个警告的时候,不需要添加这个前缀。
要重新启用警告,仍然可以使用#pragma指令,只是要在warning之后添加一个restore选项,如代码清单3-57所示。
代码清单3-57 使用预处理器指令#pragma来还原一个警告
|
上述两条指令正好可以将一个特定的代码块包围起来——前提是我们已知该警告不适用于这个代码块。
我们最喜欢禁用的一个警告或许就是CS1591。如果使用doc编译器选项来生成XML文档,但是没有对程序中的所有public项进行文档化,就会显示该警告。
3.9.5 nowarn:<warn list>选项
除了#pragma指令,C#编译器通常还支持nowarn:<warn list>选项。它可以获得与#pragma相同的结果,只是不是把它加进源代码,而是把它作为一个编译器选项使用。除此之外,nowarn选项会 影响整个编译过程,而#pragma指令只影响该指令所在的那个文件。例如在输出3-30中,我们在命令行上关闭了CS1591警告。
输出3-30
> csc /doc:generate.xml /nowarn:1591 |
利用#line指令可以改变C#编译器在报告错误或警告时显示的行号。主要是能够自动生成C#代码的实用程序和设计器使用这个指令。在代码清单3-58中,真实行号显示在最左侧。
代码清单3-58 #line预处理器指令
124 #line 113 "TicTacToe.cs" |
在上例中,使用#line指令后,编译器会将实际发生在125行的警告报告成在113行上发生,如输出3-31所示。
输出3-31
Performing main compilation... |
在#line指令后添加一个default,会反转之前的所有#line的效果,并指示编译器报告真实的行号,而不是之前使用#line指定的行号。
3.9.7 可视编辑器提示
C#提供了只有在可视代码编辑器中才有用的两个预处理器指令,也就是#region和#endregion。像Microsoft Visual Studio .NET IDE这样的可视代码编辑器能够搜索源代码,找到这些指令,并相应地进行处理。C#允许使用#region指令声明一个代码区域。#region 和#endregion必须成对使用,两个指令都可以选择在指令后面跟随一个描述性的字符串。除此之外,还可以将一个区域嵌套到另一个区域中。
代码清单3-59展示了来自tic-tac-toe程序的例子。
代码清单3-59 #region和#endregion预处理器指令
... #if CSHARP2 // Display the current board; // Display the top line of dashes. // Increment to the next border; // Reset border to 0 if it is 3. |
Visual Studio.NET会检查上述代码,并提供一个树形控件来展开和折叠由#region和#endregion指令界定的一个代码区域(在代码编辑器窗口的左侧),如图3-5所示。