第 26 节 · 第四部分 进阶特性

预处理器指令

预处理器指令在编译之前决定哪些代码参与编译、编译时出什么提示,不是运行时执行的语句。

时长 10:58授课计划第 9 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1预处理器指令是什么

预处理器指令是写给编译器看的指示:在真正编译之前,告诉它这份文件编译哪些部分。它们都以 # 开头,比如定义符号用的 #define。

视频里把它比成分拣台:每张卡片是一行代码,闸门看开关决定卡片落进编译器(被编译),还是落进弃卡盒(当它没写过)。C# 编译器里其实没有一台单独的预处理器,它是边读代码边处理指令的,只是效果像先过了一道分拣。

2两条写法规矩

第一,一条指令独占一行,前面只能有空格。接在代码后面就报错:

C# · 第 8 行
8Console.WriteLine("A"); #if DEBUG// 把 #if 接在一句代码后面
查看完整输出(4 行)
编译报错
Program.cs(8,31): error CS1040: 预处理器指令必须作为一行的第一个非空白字符出现
Program.cs(10,7): error CS1028: 意外的预处理器指令
Program.cs(12,7): error CS1028: 意外的预处理器指令
Program.cs(14,7): error CS1028: 意外的预处理器指令

第二,指令不是语句,末尾不加分号:

C# · 第 1 行
1#define DEBUG;// 多写了一个分号
编译报错
Program.cs(1,14): error CS1025: 应输入单行注释或行尾

3指令分四组

教程列出了 13 条指令:

  1. 条件编译:#define(定义符号)、#undef(取消定义)、#if、#elif、#else、#endif(如果、否则如果、否则、结束如果)。
  2. 编译时出提示:#warning 出警告,#error 出错误。
  3. 只管编辑器显示:#region、#endregion,标出可以折叠的区域。
  4. 各管一件事:#line 改报告里的行号和文件名,#pragma 给编译器发特殊指令,#nullable 控制检不检查“能不能为空”。

教程里那段把它们写在一起的示例带着 #error,不能直接运行,下面一组一组拆开真跑。

4#define 和 #undef

#define 后面跟一个符号名(符号就是一个只有名字的标记),定义过的符号放进 #if 里判断,结果就是真。

C# · 第 9-13 行
9#if (PI)// 判断 PI 有没有定义:第 1 行 #define PI 定义了,为真
10 Console.WriteLine("PI is defined");// 这一行被编译
11#else// 否则
12 Console.WriteLine("PI is not defined");// 这一行不参与编译
13#endif// 结束 #if
运行结果
PI is defined

#define 要写在文件最前面,在 using 之前:

C# · 第 1-2 行
1using System;// 先写了 using
2#define DEBUG// 再写 #define,位置太晚
编译报错
Program.cs(2,2): error CS1032: 不能在文件的第一个标记之后定义或取消定义预处理器符号

C# 的 #define 只能定义一个名字,不能带值,也不能像 C 语言那样创建宏:

C# · 第 1 行
1#define PI 3.14159// 照 C 语言习惯给符号带上值
编译报错
Program.cs(1,12): error CS1025: 应输入单行注释或行尾

#undef 正好相反,取消定义一个符号,效果放到下一节一起看。

5条件编译:#if、#elif、#else、#endif

#if 后面跟符号,可以用 ==、!=、&&(并且)、||(或者)连上更多符号,! 表示取反,也能直接写 true、false,用括号分组。

条件为真的那段被编译,不是被执行;为假的那段压根不参与编译,里面写中文也不报错:

C# · 第 6-9 行
6#if false// 条件直接写 false,永远为假
7这一行根本不是代码// 编译器根本看不到这一行
8#endif// 结束 #if
9Console.WriteLine("编译通过");// 正常编译、正常运行
运行结果
编译通过

所以 #if 和普通的 if 不一样:普通 if 在程序运行时判断,#if 在编译之前就定好了。用 #if 开头,就必须用 #endif 结尾:

C# · 第 12-14 行
12 #else// 否则
13 Console.WriteLine("Other mode");// 最后一个分支
14}// 漏写了 #endif,这个 } 也被当成跳过的部分
编译报错
Program.cs(9,43): error CS1513: 应输入 }
Program.cs(17,1): error CS1027: 应输入 #endif 指令

三个分支

C# · 第 8-14 行
8#if DEBUG// DEBUG 定义了吗?第 1 行定义了,成立
9 Console.WriteLine("Debug mode");// 第一个成立的分支被编译
10#elif RELEASE// 否则如果 RELEASE 定义了
11 Console.WriteLine("Release mode");// 跳过
12#else// 都不成立时
13 Console.WriteLine("Other mode");// 跳过
14#endif// 结束 #if
运行结果
Debug mode

从上往下判断,第一个成立的分支被编译,其余全部跳过。

加上 #undef

后面还是同一串判断,只在开头多了一行 #undef:

C# · 第 1-3 行
1#define DEBUG// 先定义 DEBUG:开关扳上
2
3#undef DEBUG// 紧接着取消:开关扳下
运行结果
Other mode

DEBUG 被取消了,RELEASE 也没定义,所以编译的是 #else 那一句。

项目自带的 DEBUG

不写 #define DEBUG,直接跑同一串判断,打印的仍是 Debug mode:Visual Studio 工具栏上选的是 Debug 配置,项目已经替你定义好了 DEBUG。切到 Release 配置再运行,打印的是 Release mode。

用运算符连起来

开头定义了 DEBUG 和 VC_V10 两个符号,四个分支分别是:只有第一个、只有第二个、两个都有、两个都没有。

C# · 第 13-14 行
13#elif (DEBUG && VC_V10)// 第三个分支:两个都有,成立(&& 是并且)
14 Console.WriteLine("DEBUG and VC_V10 are defined");// 只有这一句被编译,其余三句都跳过
运行结果
DEBUG and VC_V10 are defined

6#warning 和 #error

#warning 让编译器出一条警告,内容就是你写的那句话。警告不挡路,程序照样生成、运行:

C# · 第 6-7 行
6#warning This is a warning message// 编译时出警告 CS1030,内容就是这句话
7Console.WriteLine("Hello");// 照常运行,打印 Hello

再加上 #error,就报一条错误,生成失败,程序跑不起来:

C# · 第 6-7 行
6#warning This is a warning message// 出一条警告
7#error This is an error message// 出一条错误,生成到这里停下
编译报错
Program.cs(7,14): error CS1029: #错误:“This is an error message”

7#region 和 #endregion

标出一段代码区域,在 Visual Studio 里可以折叠成一行名字。它对编译没有影响,加不加程序都打印 Hello。

C# · 第 4-6 行
4#region MyRegion// 区域开始,名字叫 MyRegion
5 // Your code here
6#endregion// 区域结束

8#line

#line 改编译器报告里的行号和文件名,程序照常运行。故意把两处 WriteLine 少写一个字母,看错误报在哪:

C# · 第 6-11 行
6#line 100 "MyFile.cs"// 把下一行的行号定成 100,文件名改成 MyFile.cs
7 // The next line will be reported as line 100 in MyFile.cs
8 Console.WriteLin("This is line 100");// 故意写错;它是 #line 下面第 2 行,所以是第 101 行
9#line default// 恢复正常的行号和文件名
10 // Line numbering returns to normal
11 Console.WriteLin("back");// 故意写错,报回 Program.cs 第 11 行
编译报错
预处理器指令/line/MyFile.cs(101,19): error CS0117: “Console”未包含“WriteLin”的定义
Program.cs(11,19): error CS0117: “Console”未包含“WriteLin”的定义

#line 100 管的是紧挨着的下一行(那一行是注释),所以打印那句报在第 101 行。这条指令多用在自动生成代码的工具里,平时很少手写。

9#pragma

最常见的用法是关掉某个警告。一个没赋值、也没用到的字段,报的是 169 号警告 warning CS0169: 从不使用字段“Program.unusedVariable”,要关的就是 169。教程写的 414 号管的是“赋了值却从没读过”的字段,换成 414 实测仍会报 CS0169。

C# · 第 4-6 行
4#pragma warning disable 169// 关掉 169 号警告“从不使用字段”
5 private int unusedVariable;// 同样的字段,这次不再报警告
6#pragma warning restore 169// 重新打开 169 号警告

10#nullable

#nullable 控制编译器检不检查引用能不能为空。新建的项目默认就打开了,项目文件(.csproj)里写着 <Nullable>enable</Nullable>。

C# · 第 6-12 行
6#nullable enable// 打开“能否为空”检查
7 string? nullableString = null;// string? 表示可以为空,赋 null 没问题
8 string name = null;// 不带 ? 却赋 null:出警告 CS8600
9#nullable disable// 关掉检查
10 string title = null;// 同样赋 null,没有警告
11 Console.WriteLine(nullableString == null);// null == null,输出 True
12 Console.WriteLine(name == title);// 两个都是 null,输出 True
运行结果
True
True

警告教程:Program.cs(8,25): warning CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。 警告不影响运行。

11使用建议

  • 用 #region 给代码分块,读起来更有条理。
  • 用 #if 这类条件编译,让开发时和发布时编译不同的代码。
  • 用 #warning、#error 在编译时提醒自己注意问题。

12小结

  • 预处理器指令以 # 开头,独占一行,不加分号。
  • 它们在编译之前就定下哪些代码参与编译,不是在运行时判断。
  • #define 只定义名字,要写在 using 之前;Debug、Release 配置会自带 DEBUG、RELEASE。

下一节:怎么从一大段文字里,一下子找出所有的手机号和邮箱?

检查题

5 道,选完马上告诉你对不对。

  1. 1下面哪一行能通过编译?

  2. 2文件开头 #define DEBUG 再 #undef DEBUG,后面 #if DEBUG … #elif RELEASE … #else … 各打印一句,用 Debug 配置运行打印哪句?

  3. 3#if false 和 #endif 之间写了一行中文(不是代码),会怎样?

  4. 4#line 100 "MyFile.cs" 下一行是注释,再下一行写错了 Console.WriteLin(...),错误报在哪?

  5. 5类里写 #pragma warning disable 414,下面 private int unusedVariable;(没赋值、没使用),编译时?