C# 中的预处理器指令

C/C++ 开发人员在 C# 中容易不适应与困惑的地方之一

由 Anawaert 于 2024-06-04 发布   

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#中的ifelse ifelse语句,只不过用于判断的东西由布尔值变成了符号。考虑下面的这个示例:

#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,它们就真的和ifelse ifelse语句几乎一样的使用方法。因此,如果我们把调试阶段的代码都放在#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()的未被使用也被抑制。

可空引用类型开关预处理器指令:#nullableC#独有)

  使用#nullable可以启用或禁用当前代码文件的可空引用类型,且优先级最高,即无论是项目文件(.csproj文件)如何设置,只要在代码文件开头指定#nullable enable,则启用可空引用类型。#nullable指令的用法有如下三种:

#nullable enable  // 当前代码文件启用可空引用类型
#nullable disable  // 当前代码文件禁用可空引用类型
#nullable restore  // 将可空引用类型的启/禁用设置改回项目文件中的设置

  多数情况下,我们用这个指令是为了临时禁用/启用可空引用类型来排查某些错误,并非是为了搞特立独行,例如在禁止可空引用类型的项目里整一段允许可空引用类型的代码,对于某些从旧的.NET Framework迁移上来的项目而言,这是灾难性的。

总结

  本文介绍了C#中的预处理器指令,并解释了它们的使用方法,与同系语言(C/C++)中的预处理器指令有什么异同等。若有其他关于C#中预处理器指令的使用方法或小技巧,欢迎各位在本篇文章下方的评论区补充留言。