C# 中的预处理器指令
概述
在 C# 中,有一些以 #... 开头的命令语句,称为“预处理器指令”。这些命令从来不会转换为可执行代码中的命令,但会影响编译过程的各个方面。例如,使用预处理器指令可以禁止编译器编译代码的某一部分。再如,使用预处理器指令可以在编译时发出/抑制警告和错误,合理利用它们可以使得程序的开发更加灵活、便捷。
但是,C# 中的预处理器指令与 C/C++ 中的预处理器指令有相当的区别,尤其是在宏定义上(当然,C# 中不存在头文件包含,自然不存在 #include 命令),两者的区别非常直观地体现了 C# 与 C/C++ 在语言设计理念上的不同。下面将列举说明 C# 中的七类常用预处理器指令。
宏定义预处理器指令:#define 与 #undef
如同 C/C++ 一般,C# 中可以使用 #define 在代码开头定义一个符号,如:
#define ANAWAERT
此时我们告诉编译器,我们定义了一个符号,叫 “ANAWAERT”。当然,如果这个符号已经存在了,那么这条 #define ANAWAERT 不会起任何作用。但是如果我们想定义一个符号,名叫 “PI”,它的值为 3.1415926,那就不行了。下面这个代码示例在 C# 中是不合法的:
#define PI 3.1415926 // 不合法的宏定义
原因就在于,C# 从诞生之初就被设计为一种语法简练、优雅的语言,为了避免像 C/C++ 那样,可以写出让人困惑的套娃式宏定义、宏函数的问题,C# 不允许使用 #define 来定义常量或者宏函数。的确,这在某种程度上解决了代码杂乱、定义递归的问题。
而 #undef 则与 #define 正好相反——它用于删除符号的定义:
#undef ANAWAERT
当然,如果符号本身就不存在,那么 #undef 也不会起任何作用。有一个需要注意的点是,#undef 也必须放在代码开头。
单看 #define 与 #undef 这两个预处理器指令可能觉得它们没什么作用,不过如果有熟悉 C/C++ 或者经常做 .NET CLR 调试的朋友就知道,接下来的条件预处理器指令才是让 #define 与 #undef 发挥重要作用的好搭档。
条件预处理器指令:#if、#elif、#else 和 #endif
#if、#elif、#else 这三个类比 C# 中的 if、else if、else 语句,只不过用于判断的东西由布尔值变成了符号。考虑下面的这个示例:
#define DEBUG
using System;
class Program
{
static int Main()
{
#if RELEASE
Console.WriteLine("运行在 Release 的定义下");
#elif DEBUG
Console.WriteLine("运行在 Debug 的定义下");
#endif
return 0;
}
}
运行代码,会在控制台看见输出:运行在 Debug 的定义下。因为我们使用 #define 定义了"DEBUG"这个符号,当编译器遇到 #if 指令后,会检查 #if 后的符号是否已被定义。若已被定义,则会编译 #if 与 #endif 之间所包围的代码。同理 #elif,它们就真的和 if、else if、else 语句几乎一样的使用方法。因此,如果我们把调试阶段的代码都放在 #if 子句中,在发布时,我们只需要将相关的 #define 指令注释掉,就可以“无损”地获得可用于发行的代码,是不是很方便呢?在 C/C++ 中,这项技术非常常见,也就是我们常说的条件编译。
#if 和 #elif 还支持使用一组逻辑运算符,也与布尔逻辑运算符类似,具体的有 !、==、!=、&& 和 ||。如果符号存在,则被认为是是 true,反之为 false,例如:
#define ADAM
#define ANAWAERT
#define VAR
#undef ADAM
#if !ADAM && ANAWAERT == VAR // 条件通过,因为ADAM被删除了,所以!ADAM为true,true && true == true的结果依然是true
区域标记预处理器指令:#region 与 #endregion
这两个预处理器指令实际上不会被编译器理会,但是像 Visual Studio、Visual Studio Code 与 Jetbrains Rider 等主流 IDE 或代码编辑器会运行在视图上折叠 #region 与 #endregion 所包围的代码块,只显示与 #region 关联的名称,例如:
#region 我的代码块
static void Foo(int x) => Console.WriteLine(x);
#endregion
在 Visual Studio Code 中的效果:
文件名&行号信息修改预处理器指令:#line
这个预处理器指令的使用频率相对较低,一般使用场景在当你使用了他人的包,并且该包的某些代码行为会改变你的代码,并在编译器报错时候将错误引到他的代码文件/包内容上了。那么此时你可以使用 #line 来将编译器错误与警告中的文件名与行号信息修正为你的报错文件名与行号,例如:
#line 128 "Shared.cs"
当然,要恢复默认,可以使用下面的指令:
#line default
编译警告抑制/还原预处理器指令:#pragma
#pragma 指令可以抑制或者还原指定的编译警告,并可以对类、函数的编译警告的抑制与还原实现精细的控制。例如,下面这个示例中的两个函数各有一个已定义但是没有初始化的变量,可以通过 #pragma 指令来实现警告的抑制与恢复:
using System;
static void Func1()
{
#pragma warning disable 168
int num1;
#pragma warning restore 168
}
#pragma warning disable 8321
static void Func2()
{
int num2;
}
#pragma warning restore 8321
若我们对这段代码进行编译,则会得到2个编译警告,而不是4个。它们分别是:声明了本地函数 “Func1",但从未使用过(CS8321);声明了变量 “num2",但从未使用过(CS0168)。由于我们在 Func1() 中对 CS0168 警告进行抑制,因此 num1 未被赋值的警告被抑制;同理在 Func2() 中,本地函数 Func2() 的未被使用也被抑制。
可空引用类型开关预处理器指令:#nullable(现代 C# 特有)
使用 #nullable 可以启用或禁用当前代码文件的可空引用类型,且优先级最高,即无论是项目文件(.csproj 文件)如何设置,只要在代码文件开头指定 #nullable enable,则启用可空引用类型。#nullable 指令的用法有如下三种:
#nullable enable // 当前代码文件启用可空引用类型
#nullable disable // 当前代码文件禁用可空引用类型
#nullable restore // 将可空引用类型的启/禁用设置改回项目文件中的设置
多数情况下,我们用这个指令是为了临时禁用/启用可空引用类型来排查某些错误,并非是为了搞特立独行,例如在禁止可空引用类型的项目里整一段允许可空引用类型的代码,对于某些从旧的 .NET Framework 迁移上来的项目而言,这是灾难性的。
总结
本文介绍了 C# 中的预处理器指令,并解释了它们的使用方法,与同系语言 ( C/C++) 中的预处理器指令有什么异同等。若有其他关于 C# 中预处理器指令的使用方法或小技巧,欢迎各位在本篇文章下方的评论区补充留言。