预处理指令
什么是预处理指令
源代码指定了程序的定义,预处理指令(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 … 所有譬告消息在这段代码中处于开启状态