C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

2.10 C#预处理器指令

除了前面介绍的常用关键字外,C#还有许多名为“预处理器指令”的命令。这些命令从来不会转化为可执行代码中的命令,但会影响编译过程的各个方面。例如,使用预处理器指令可以禁止编译器编译代码的某一部分。如果计划发布两个版本的代码,即基本版本和拥有更多功能的企业版本,就可以使用这些预处理器指令。在编译软件的基本版本时,使用预处理器指令可以禁止编译器编译与附加功能相关的代码。另外,在编写提供调试信息的代码时,也可以使用预处理器指令。实际上,在销售软件时,一般不希望编译这部分代码。

预处理器指令的开头都有符号#。

注意:C++开发人员应该知道,在C和C++中预处理器指令非常重要。但是,在C#中,并没有那么多的预处理器指令,它们的使用也不太频繁。C#提供了其他机制来实现许多C++指令的功能,如定制特性。还要注意,C#并没有一个像C++那样的独立预处理器,所谓的预处理器指令实际上是由编译器处理的。尽管如此,C#仍保留了一些预处理器指令名称,因为这些命令会让人觉得就是预处理器。

下面简要介绍预处理器指令的功能。

2.10.1 #define和#undef

#define的用法如下所示:

        #define DEBUG

它告诉编译器存在给定名称的符号,在本例中是DEBUG。这有点类似于声明一个变量,但这个变量并没有真正的值,只是存在而已。这个符号不是实际代码的一部分,而只在编译器编译代码时存在。在C#代码中它没有任何意义。

#undef正好相反——它删除符号的定义:

        #undef DEBUG

如果符号不存在,#undef就没有任何作用。同样,如果符号已经存在,则#define也不起作用。

必须把#define和#undef命令放在C#源文件的开头位置,在声明要编译的任何对象的代码之前。

#define本身并没有什么用,但与其他预处理器指令(特别是#if)结合使用时,它的功能就非常强大了。

注意:这里应注意一般C#语法的一些变化。预处理器指令不用分号结束,一般一行上只有一条命令。这是因为对于预处理器指令,C#不再要求命令使用分号进行分隔。如果编译器遇到一条预处理器指令,就会假定下一条命令在下一行。

2.10.2 #if、#elif、#else和#endif

这些指令告诉编译器是否要编译代码块。考虑下面的方法:

        int DoSomeWork(double x)
        {
          // do something
          #if DEBUG
            WriteLine($"x is {x}");
          #endif
        }

这段代码会像往常那样编译,但Console.WriteLine方法调用包含在#if子句内。这行代码只有在前面的#define指令定义了符号DEBUG后才执行。当编译器遇到#if指令后,将先检查相关的符号是否存在,如果符号存在,就编译#if子句中的代码。否则,编译器会忽略所有的代码,直到遇到匹配的#endif指令为止。一般是在调试时定义符号DEBUG,把与调试相关的代码放在#if子句中。在完成了调试后,就把#define指令注释掉,所有的调试代码会奇迹般地消失,可执行文件也会变小,最终用户不会被这些调试信息弄糊涂(显然,要做更多的测试,确保代码在没有定义DEBUG的情况下也能工作)。这项技术在C和C++编程中十分常见,称为条件编译(conditional compilation)。

#elif (=else if)和#else指令可以用在#if块中,其含义非常直观。也可以嵌套#if块:

        #define ENTERPRISE
        #define W10
        // further on in the file
        #if ENTERPRISE
          // do something
          #if W10
              // some code that is only relevant to enterprise
              // edition running on W10
          #endif
        #elif PROFESSIONAL
          // do something else
        #else
          // code for the leaner version
        #endif

#if和#elif还支持一组逻辑运算符“! ”、“==”、“! =”和“||”。如果符号存在,就被认为是true,否则为false,例如:

        #if W10 && (ENTERPRISE==false) // if W10 is defined but ENTERPRISE isn't

2.10.3 #warning和 # error

另两个非常有用的预处理器指令是#warning和#error。当编译器遇到它们时,会分别产生警告或错误。如果编译器遇到#warning指令,会向用户显示#warning指令后面的文本,之后编译继续进行。如果编译器遇到#error指令,就会向用户显示后面的文本,作为一条编译错误消息,然后会立即退出编译,不会生成IL代码。

使用这两条指令可以检查#define语句是不是做错了什么事,使用#warning语句可以提醒自己执行某个操作:

        #if DEBUG && RELEASE
          #error "You've defined DEBUG and RELEASE simultaneously! "
        #endif
        #warning "Don't forget to remove this line before the boss tests the code! "
          WriteLine("*I hate this job.*");

2.10.4 #region和#endregion

#region和#endregion指令用于把一段代码视为有给定名称的一个块,如下所示:

        #region Member Field Declarations
          int x;
          double d;
          Currency balance;
        #endregion

这看起来似乎没有什么用,它根本不影响编译过程。这些指令真正的优点是它们可以被某些编辑器识别,包括Visual Studio编辑器。这些编辑器可以使用这些指令使代码在屏幕上更好地布局。第17章会详细介绍。

2.10.5 #line

#line指令可以用于改变编译器在警告和错误信息中显示的文件名和行号信息。这条指令用得并不多。如果编写代码时,在把代码发送给编译器前,要使用某些软件包改变输入的代码,该指令最有用,因为这意味着编译器报告的行号或文件名与文件中的行号或编辑的文件名不匹配。#line指令可以用于还原这种匹配。也可以使用语法#line default把行号还原为默认的行号:

        #line 164 "Core.cs"  // We happen to know this is line 164 in the file
                             // Core.cs, before the intermediate
                             // package mangles it.
        // later on
        #line default     // restores default line numbering

2.10.6 #pragma

#pragma指令可以抑制或还原指定的编译警告。与命令行选项不同,#pragma指令可以在类或方法级别实现,对抑制警告的内容和抑制的时间进行更精细的控制。下面的例子禁止“字段未使用”警告,然后在编译MyClass类后还原该警告。

        #pragma warning disable 169
        public class MyClass
        {
          int neverUsedField;
        }
        #pragma warning restore 169