1预处理器指令是什么
预处理器指令是写给编译器看的指示:在真正编译之前,告诉它这份文件编译哪些部分。它们都以 # 开头,比如定义符号用的 #define。
视频里把它比成分拣台:每张卡片是一行代码,闸门看开关决定卡片落进编译器(被编译),还是落进弃卡盒(当它没写过)。C# 编译器里其实没有一台单独的预处理器,它是边读代码边处理指令的,只是效果像先过了一道分拣。
2两条写法规矩
第一,一条指令独占一行,前面只能有空格。接在代码后面就报错:
| 8 | Console.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: 意外的预处理器指令
第二,指令不是语句,末尾不加分号:
| 1 | #define DEBUG; | // 多写了一个分号 |
Program.cs(1,14): error CS1025: 应输入单行注释或行尾
3指令分四组
教程列出了 13 条指令:
- 条件编译:
#define(定义符号)、#undef(取消定义)、#if、#elif、#else、#endif(如果、否则如果、否则、结束如果)。 - 编译时出提示:
#warning出警告,#error出错误。 - 只管编辑器显示:
#region、#endregion,标出可以折叠的区域。 - 各管一件事:
#line改报告里的行号和文件名,#pragma给编译器发特殊指令,#nullable控制检不检查“能不能为空”。
教程里那段把它们写在一起的示例带着 #error,不能直接运行,下面一组一组拆开真跑。
4#define 和 #undef
#define 后面跟一个符号名(符号就是一个只有名字的标记),定义过的符号放进 #if 里判断,结果就是真。
| 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 之前:
| 1 | using System; | // 先写了 using |
| 2 | #define DEBUG | // 再写 #define,位置太晚 |
Program.cs(2,2): error CS1032: 不能在文件的第一个标记之后定义或取消定义预处理器符号
C# 的 #define 只能定义一个名字,不能带值,也不能像 C 语言那样创建宏:
| 1 | #define PI 3.14159 | // 照 C 语言习惯给符号带上值 |
Program.cs(1,12): error CS1025: 应输入单行注释或行尾
#undef 正好相反,取消定义一个符号,效果放到下一节一起看。
5条件编译:#if、#elif、#else、#endif
#if 后面跟符号,可以用 ==、!=、&&(并且)、||(或者)连上更多符号,! 表示取反,也能直接写 true、false,用括号分组。
条件为真的那段被编译,不是被执行;为假的那段压根不参与编译,里面写中文也不报错:
| 6 | #if false | // 条件直接写 false,永远为假 |
| 7 | 这一行根本不是代码 | // 编译器根本看不到这一行 |
| 8 | #endif | // 结束 #if |
| 9 | Console.WriteLine("编译通过"); | // 正常编译、正常运行 |
编译通过
所以 #if 和普通的 if 不一样:普通 if 在程序运行时判断,#if 在编译之前就定好了。用 #if 开头,就必须用 #endif 结尾:
| 12 | #else | // 否则 |
| 13 | Console.WriteLine("Other mode"); | // 最后一个分支 |
| 14 | } | // 漏写了 #endif,这个 } 也被当成跳过的部分 |
Program.cs(9,43): error CS1513: 应输入 } Program.cs(17,1): error CS1027: 应输入 #endif 指令
三个分支
| 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:
| 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 两个符号,四个分支分别是:只有第一个、只有第二个、两个都有、两个都没有。
| 13 | #elif (DEBUG && VC_V10) | // 第三个分支:两个都有,成立(&& 是并且) |
| 14 | Console.WriteLine("DEBUG and VC_V10 are defined"); | // 只有这一句被编译,其余三句都跳过 |
DEBUG and VC_V10 are defined
6#warning 和 #error
#warning 让编译器出一条警告,内容就是你写的那句话。警告不挡路,程序照样生成、运行:
| 6 | #warning This is a warning message | // 编译时出警告 CS1030,内容就是这句话 |
| 7 | Console.WriteLine("Hello"); | // 照常运行,打印 Hello |
再加上 #error,就报一条错误,生成失败,程序跑不起来:
| 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。
| 4 | #region MyRegion | // 区域开始,名字叫 MyRegion |
| 5 | // Your code here | |
| 6 | #endregion | // 区域结束 |
8#line
#line 改编译器报告里的行号和文件名,程序照常运行。故意把两处 WriteLine 少写一个字母,看错误报在哪:
| 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.,要关的就是 169。教程写的 414 号管的是“赋了值却从没读过”的字段,换成 414 实测仍会报 CS0169。
| 4 | #pragma warning disable 169 | // 关掉 169 号警告“从不使用字段” |
| 5 | private int unusedVariable; | // 同样的字段,这次不再报警告 |
| 6 | #pragma warning restore 169 | // 重新打开 169 号警告 |
10#nullable
#nullable 控制编译器检不检查引用能不能为空。新建的项目默认就打开了,项目文件(.csproj)里写着 <Nullable>enable</。
| 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. 警告不影响运行。
11使用建议
- 用
#region给代码分块,读起来更有条理。 - 用
#if这类条件编译,让开发时和发布时编译不同的代码。 - 用
#warning、#error在编译时提醒自己注意问题。
12小结
- 预处理器指令以
#开头,独占一行,不加分号。 - 它们在编译之前就定下哪些代码参与编译,不是在运行时判断。
#define只定义名字,要写在using之前;Debug、Release 配置会自带 DEBUG、RELEASE。
下一节:怎么从一大段文字里,一下子找出所有的手机号和邮箱?
