第 28 讲:预处理指令
(Preprocessor),是在程序编译期间,特别告诉编译器一些特殊行为的指令。这些指令一定是以井号开头,且以单行作为单位书写,因此预处理指令也不需要添加分号结尾。
预处理指令可以被划分为如下几类:
条件编译指令:
#if
、#endif
、#else
、#elif
、#define
、#undef
引发警告和错误指令:
#warning
、#error
行号指示指令:
#line
代码折叠展开指令:
#region
、#endregion
消除警告信息指令:
#pragma warning
预处理指令的内容其实并不多。虽然也分成很多类,但是介绍其实并不是很多,因此打算将它们全部写在这一篇文章里。下面我们来看看它们的用法。
实际上,还存在一些别的预处理指令,例如
#pragma checksum
。但是这个玩意儿对于初学来说,用处非常小,它一般是提供给“代码生成代码”过程所需,检查文件是否有变动。
Part 1 条件编译
1-1 条件编译的基本用法
条件编译(Conditional Compilation)是针对于编程环境来制定的一种特殊编译规则。它不能通过代码来实现,而是通过某一个特征符号来代表当前环境是如何的。举个例子,C 语言里的 int
的大小随着机器的情况,而产生变化。条件编译刚好可以通过设置一个符号来表达这样的情况。当用户在使用程序代码的时候,如果这个环境不是这样的,我们只需要删除符号就可以达到切换编译模式的效果。其中,我们使用的、用来区分不同环境的这个符号叫做条件编译符号(Conditional Compilation Symbol)。
下面我们来详细解释一下条件编译的过程。我们之前使用过指针,来完成对底层的兼容。但是很遗憾的是,C 语言的 int
和 C# 的 int
貌似并不能够直接划等号。C 语言的 int
是不定长的,因为它随着系统的位数有不同的情况;而 C# 里,int
是固定大小。那么,我们不得不在 C# 里指定大小,避免程序的崩溃。
比如这样的函数。在导入的时候,int*
和 int
都必须要指定长度,因此,我们需要借助 MarshalAs
特性指定。
则不必指定,因为它的类型是指针。指针类型的长度则和 C 里是一样的。这样还不够。因为 int
大小是没有指定的,因此我们这里需要借助一个操作了。
我们在 int length
的周围添加这样一段代码,使得整个函数的声明改成这样:
#if TARGET_64BITS
、#else
和 #endif
包装了整个参数;而且我们还写上了两个截然不同的“方案”,一个是用的 long
,而另外一个则是用的 int
类型。这个作用是什么呢?用户自己是知道你电脑是什么系统、什么位数的。如果你是 64 位,那么你可以去配置一个叫做 TARGET_64BITS
的符号到项目配置里,然后,C# 自动就会认为 TARGET_64BITS
这个符号存在,那么 #if TARGET_64BITS
和 #else
之间的这段代码就会得到编译,而 #else
和 #endif
之间的部分就在 C# 编译器编译代码的时候忽略掉了。
我们在项目配置菜单里找到“Conditional compilation symbols”(条件编译符号),然后填入 TARGET_64BITS
,保存后关闭。

接着你就可以在你的代码里,条件编译的部分,代码会有着色,但是不编译的部分,就不会有着色(是灰色的)。

我们可以通过自行的配置,将代码传给所有人使用的时候,按自己的需求去更替和修改条件编译符号,然后达到编译不同的代码的效果。
当然了,你可能会问我,这些符号是随便写的吗?如果是的话,我怎么知道代码里用到了什么符号呢?这个问题其实解释起来很简单:这个是写代码的这个人必须给出来的。你如果要用,那么就可以参考符号的用途,来配置或取消配置这个符号。
1-2 临时配置编译符号
条件编译符号的灵活之处在于它并不一定非得在项目配置菜单里,我们完全可以将符号写在代码里,然后提供用户到底是否是用,还是不用。
我们使用 #define
指令来完成。我们在代码文件的最开头添加 #define TARGET_64BITS
来启用这个符号。
这样也可以。你甚至可以不用去配置菜单里添加符号,就可以做到。但是前提是,这个符号在 #define
当然了,你也可以用 #undef
来手动取消对某个符号的启用。假设你配置了这个符号,你可以使用 #undef 符号名
的格式来取消符号的定义。
1-3 多条件编译符号的判断
因为 C 语言的 int
类型随系统位数变化而变更大小,而系统位数还可能是 16 位,所以我们可以这么去操作那段代码:
我们使用 #elif
指令,可以对前文的条件编译符号不存在的时候,继续判断另外的符号,这相当于 else
-if
组合的条件判断。
elif 是 else 和 if 两个单词的拼接。
Part 2 引发自定义警告和错误
有些时候我们可能需要一点必要的手段来提供给用户,不要随意和滥用编译符号。比如前面的例子里,如果有些用户故意三个符号都不配置的话,那么这个参数就留空了。那么前面的参数最后跟了一个逗号,这必然会产生很严重的错误。
我们可以这样。如果三个符号都不配置,我们就在最后一个代码段落里添加一个 #error
指令,来提供给用户。
#error You should specify one symbol in 'TARGET_16BITS', 'TARGET_32BITS' and 'TARGET_64BITS'.
看起来很长,实际上后面的文字是我们自定义的,这表示错误信息。当如果不配置上面的三个符号的话,第 8 行自动会产生一个编译器错误,告知用户“你不能这么用”。
那前面逗号的编译错误呢?没事,用户可能会因为别的原因,删掉那个逗号来避免编译错误,但这是很危险的行为;所以单独给它设置一条错误信息,可以提供给用户,告诉用户你不可以这么用。
在实际代码编译的时候,因为你正常的配置符号,因此前面三条代码会挨个判断,#error
指令完全碰不着。因此,你也不用担心,这段代码是否会永久遇到这个编译错误指令。
Part 3 人为变动行号和文件信息
我们简单说一下就可以了。这个符号对入门来说,也没啥大用。#line
指令表示,我们从添加这个指令开始,这一行的编号就是指定的数值了。
using System;
的话,显然 Console
就不知道在哪里了,于是编译器就会产生错误信息:找不到 Console

不过,你查看详细错误信息的时候,在最后你会发现,行号改成 300 了。

这个就是这个符号的用法。我们可以指定错误编号在哪里,来故意告知用户所在位置是一些特殊位置。你甚至可修改文件名称:
然后,你在查看详情的时候:

这个 #line
指令就是这个用法。
Part 4 给代码增加额外的折叠块
如果代码非常复杂,我们可能会用到 #region
和 #endregion
指令。#region
和 #endregion
指令是成对出现的,和 #if
以及 #endif
是一样的,都得成对出现。
举个例子,我们用上之前的求质数的代码。我们在三大段代码之间分别加上 #region
和 #endregion
指令。
#region


这个指令就是这么用的。我们可以人为指定代码块,然后给它写上文字;到时候,代码就可以通过 VS 提供的功能进行代码的折叠;另外,折叠之后,那个我们写在 #region
指令后面的文字是可以在折叠的时候呈现出来的。当然了,这个文字是可以不写的。
Part 5 总结
总的来说,预处理指令并不是很重要,但是对一些代码起着非常重要的作用。如果没有它们,我们可能无法做到一些行为;这些行为可能就是为了补足和完善代码书写过程。