C# 预处理器指令

尽管编译器没有单独的预处理器,但本节中所述指令的处理方式与有预处理器时一样。 可使用这些指令来帮助条件编译。 不同于 C 和 C++ 指令,不能使用这些指令来创建宏。 预处理器指令必须是一行中唯一的说明。

可为空上下文

#nullable 预处理器指令将设置可为空注释上下文和可为空警告上下文 。 此指令控制是否可为空注释是否有效,以及是否给出为 Null 性警告。 每个上下文要么处于已禁用状态,要么处于已启用状态 。

通过将 Nullable 元素添加到 PropertyGroup 元素中,可在项目级别(C# 源代码之外)指定这两个上下文。 #nullable 指令控制注释和警告上下文,并优先于项目级设置。 指令会设置其控制的上下文,直到另一个指令替代它,或直到源文件结束为止。

指令的效果如下所示:

  • #nullable disable:将可为空注释和警告上下文设置为“已禁用”。
  • #nullable enable:将可为空注释和警告上下文设置为“已启用”。
  • #nullable restore:将可为空注释和警告上下文还原为项目设置。
  • #nullable disable annotations:将可为空注释上下文设置为“已禁用”。
  • #nullable enable annotations:将可为空注释上下文设置为“已启用”。
  • #nullable restore annotations:将可为空注释上下文还原为项目设置。
  • #nullable disable warnings:将可为空警告上下文设置为“已禁用”。
  • #nullable enable warnings:将可为空警告上下文设置为“已启用”。
  • #nullable restore warnings:将可为空警告上下文还原为项目设置。

条件编译

使用四个预处理器指令来控制条件编译:

  • #if:打开条件编译,其中仅在定义了指定的符号时才会编译代码。
  • #elif:关闭前面的条件编译,并基于是否定义了指定的符号打开一个新的条件编译。
  • #else:关闭前面的条件编译,如果没有定义前面指定的符号,打开一个新的条件编译。
  • #endif:关闭前面的条件编译。

仅在定义指定的符号时或者在使用 ! not 运算符时未定义指定的符号时,C# 编译器才编译 #if 指令和 #endif 指令之间的代码。 与 C 和 C++ 不同,不能将数字值分配给符号。 C# 中的 #if 语句是布尔值,且仅测试是否已定义该符号。 例如,定义 DEBUG 时将编译以下代码:

#if DEBUG
    Console.WriteLine("Debug version");
#endif

未定义 MYTEST 时,将编译以下代码:

#if !MYTEST
    Console.WriteLine("MYTEST is not defined");
#endif

可以使用运算符 ==(相等)!=(不相等)来测试 bool 值是 true 还是 falsetrue 表示定义该符号。 语句 #if DEBUG 具有与 #if (DEBUG == true) 相同的含义。 可以使用 && (and)|| (or)!(not) 运算符来计算是否已定义多个符号。 还可以用括号对符号和运算符进行分组。

下面是一个复杂指令,允许代码利用较新的 .NET 功能,同时保持向后兼容。 例如,假设你在代码中使用 NuGet 包,但该包仅支持 .NET 6(及更高版本)和 .NET Standard 2.0(及更高版本):

#if (NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER)
    Console.WriteLine("Using .NET 6+ or .NET Standard 2+ code.");
#else
    Console.WriteLine("Using older code that doesn't support the above .NET versions.");
#endif

#if 以及 #else#elif#endif#define#undef 指令,允许基于是否存在一个或多个符号包括或排除代码。 条件编译在编译调试版本的代码或编译特定配置的代码时会很有用。

#if 指令开头的条件指令必须以 #endif 指令显式终止。 #define 允许你定义一个符号。 通过将该符号用作传递给 #if 指令的表达式,该表达式的计算结果为 true。 还可以通过 DefineConstants 编译器选项来定义符号。 可以通过 #undef 取消定义符号。 使用 #define 创建的符号的作用域是在其中定义它的文件。 使用 DefineConstants 或 #define 定义的符号与具有相同名称的变量不冲突。 也就是说,变量名称不应传递给预处理器指令,且符号仅能由预处理器指令评估。

#elif 可以创建复合条件指令。 如果之前的 #if 和任何之前的可选 #elif 指令表达式的值都不为 true,则计算 #elif 表达式。 如果 #elif 表达式计算结果为 true,编译器将计算 #elif 和下一条件指令间的所有代码。 例如:

#define VC7
//...
#if DEBUG
    Console.WriteLine("Debug build");
#elif VC7
    Console.WriteLine("Visual Studio 7");
#endif

#else 允许创建复合条件指令,因此,如果先前 #if 或(可选)#elif 指令中的任何表达式的计算结果都不是 true,则编译器将对介于 #else 和下一个 #endif 之间的所有代码进行求值。 #endif(#endif) 必须是 #else 之后的下一个预处理器指令。

#endif 指定条件指令的末尾,以 #if 指令开头。

此外,生成系统还会感知表示 SDK 样式项目中不同目标框架的预定义预处理器符号。 在创建可以面向多个 .NET 版本的应用程序时,这些符号会很有用。

目标框架 符号 其他符号
(在 .NET 5+ SDK 中可用)
平台符号(仅
在指定特定于 OS 的 TFM 时可用)
.NET Framework
.NET Standard
.NET 5 及更高版本(和 .NET Core)
[OS][version](例如,IOS15_1),
[OS][version]_OR_GREATER(例如,IOS15_1_OR_GREATER

注意

  • 无论目标版本是什么,都将定义无版本符号。
  • 仅针对目标版本定义特定于版本的符号。
  • 为目标版本和所有早期版本定义 <framework>_OR_GREATER 符号。 例如,如果针对 .NET Framework 2.0,则会定义以下符号:NET20NET20_OR_GREATERNET11_OR_GREATERNET10_OR_GREATER
  • NETSTANDARD<x>_<y>_OR_GREATER 符号仅针对 .NET Standard 目标定义,而不适用于实现 .NET Standard 的目标,例如 .NET Core 和 .NET Framework。
  • 它们与 MSBuild TargetFramework 属性NuGet 使用的目标框架名字对象 (TFM) 不同。

备注

对于传统的非 SDK 样式的项目,必须通过项目的属性页面在 Visual Studio 中为不同目标框架手动配置条件编译符号。

其他预定义符号包括 DEBUGTRACE 常数。 你可以使用 #define 替代项目的值集。 例如,会根据生成配置属性(“调试”或者“发布”模式)自动设置 DEBUG 符号。

下例显示如何在文件上定义 MYTEST 符号,然后测试 MYTESTDEBUG 符号的值。 此示例的输出取决于是在“调试”还是“发布”配置模式下生成项目 。

#define MYTEST
using System;
public class MyClass
{
    static void Main()
    {
#if (DEBUG && !MYTEST)
        Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
        Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
        Console.WriteLine("DEBUG and MYTEST are defined");
#else
        Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
    }
}

下例显示如何针对不同的目标框架进行测试,以便在可能时使用较新的 API:

public class MyClass
{
    static void Main()
    {
#if NET40
        WebClient _client = new WebClient();
#else
        HttpClient _client = new HttpClient();
#endif
    }
    //...
}

定义符号

使用以下两个预处理器指令来定义或取消定义条件编译的符号:

  • #define:定义符号。
  • #undef:取消定义符号。

使用 #define 来定义符号。 将符号用作传递给 #if 指令的表达式时,该表达式的计算结果为 true,如以下示例所示:

#define VERBOSE

#if VERBOSE
   Console.WriteLine("Verbose output version");
#endif

注意

在 C# 中,应使用 const 关键字定义基元常量。 const 声明会创建一个在运行时无法修改的 static 成员。 #define 指令不能用于声明常量值,而在 C 和 C++ 中通常可以这样做。 如果具有多个此类常量,请考虑创建一个单独的“常量”类来容纳它们。

符号可用于指定编译的条件。 可通过 #if#elif 测试符号。 还可以使用 ConditionalAttribute 来执行条件编译。 可以定义符号,但不能为符号分配值。 文件中必须先出现 #define 指令,才能使用并非同时也是预处理器指令的任何指示。 还可以通过 DefineConstants 编译器选项来定义符号。 可以通过 #undef 取消定义符号。

定义区域

可以使用以下两个预处理器指令来定义可在大纲中折叠的代码区域:

  • #region:启动区域。
  • #endregion:结束区域。

利用 #region,可以指定在使用代码编辑器的大纲功能时可展开或折叠的代码块。 在较长的代码文件中,折叠或隐藏一个或多个区域十分便利,这样,可将精力集中于当前处理的文件部分。 下面的示例演示如何定义区域:

#region MyClass definition
public class MyClass
{
    static void Main()
    {
    }
}
#endregion

#region 块必须通过 #endregion 指令终止。 #region 块不能与 #if 块重叠。 但是,可以将 #region 块嵌套在 #if 块内,或将 #if 块嵌套在 #region 块内。

错误和警告信息

使用以下指令指示编译器生成用户定义的编译器错误和警告,并控制行信息:

  • #error:使用指定的消息生成编译器错误。
  • #warning:使用指定的消息生成编译器警告。
  • #line:更改用编译器消息输出的行号。

#error 可从代码中的特定位置生成 CS1029 用户定义的错误。 例如:

#error Deprecated code in this method.

注意

编译器以特殊的方式处理 #error version 并报告编译器错误 CS8304,消息中包含使用的编译器和语言版本。

#warning 允许你从代码中的特定位置生成 CS1030 第一级编译器警告。 例如:

#warning Deprecated code in this method.

借助 #line,可修改编译器的行号及(可选)用于错误和警告的文件名输出。

以下示例演示如何报告与行号相关联的两个警告。 #line 200 指令将下一行的行号强制设为 200(尽管默认值为 #6);在执行下一个 #line 指令前,文件名都会报告为“特殊”。 #line default 指令将行号恢复至默认行号,这会对上一指令重新编号的行进行计数。

class MainClass
{
    static void Main()
    {
#line 200 "Special"
        int i;
        int j;
#line default
        char c;
        float f;
#line hidden // numbering not affected
        string s;
        double d;
    }
}

编译产生以下输出:

Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used

可在生成过程的自动、中间步骤中使用 #line 指令。 例如,如果已从原始源代码文件中删除行,但仍希望编译器基于文件中的原始行号生成输出,可在删除行后,使用 #line 来模拟原始行号。

#line hidden 指令能对调试程序隐藏连续行,当开发者逐行执行代码时,介于 #line hidden 和下一 #line 指令(假设它不是其他 #line hidden 指令)间的任何行都将被跳过。 还可通过此选项允许 ASP.NET 区分用户定义和计算机生成的代码。 虽然此功能主要用于 ASP.NET,但可能更多的源生成器会利用此功能。

#line hidden 指令不影响错误报告中的文件名或行号。 也就是说,如果编译器在隐藏块中发现错误,编译器将报告错误的当前文件名和行号。

#line filename 指令可指定要在编译器输出中显示的文件名。 默认情况下,将使用源代码文件的实际名称。 该文件名必须以双引号 ("") 引起来,且必须位于行号之后。

从 C# 10 开始,可以使用一种新形式的 #line 指令:

#line (1, 1) - (5, 60) 10 "partial-class.cs"
/*34567*/int b = 0;

这种形式的组件包括:

  • (1, 1):指令后面行上的第一个字符的起始行和列。 在此示例中,下一行将报告为第 1 行第 1 列。
  • (5, 60):标记区域的结束行和列。
  • 10:将使 #line 指令生效的列偏移量。 在此示例中,第 10 列将报告为第 1 列。 这就是声明 int b = 0; 开始的位置。 此字段可选。 如果省略了该字段,则指令将对第 1 列生效。
  • "partial-class.cs":输出文件的名称。

前面的示例将生成以下警告:

partial-class.cs(1,5,1,6): warning CS0219: The variable 'b' is assigned but its value is never used

重新映射后,变量 b 位于 partial-class.cs 文件第 1 行的第 6 个字符处。

特定于域的语言 (DSL) 通常使用此格式来提供从源文件到生成的 C# 输出的更好的映射。 此扩展 #line 指令的最常见用途是将生成的文件中出现的警告或错误重新映射到原始源。 例如,请考虑以下 razor 页:

@page "/"
Time: @DateTime.NowAndThen

属性 DateTime.Now 被错误地键入为 DateTime.NowAndThen。 在 page.g.cs 中,为此 razor 代码片段生成的 C# 如下所示:

  _builder.Add("Time: ");
#line (2, 6) - (2, 27) 15 "page.razor"
  _builder.Add(DateTime.NowAndThen);

前面的代码片段的编译器输出如下:

page.razor(2, 2, 2, 27)error CS0117: 'DateTime' does not contain a definition for 'NowAndThen'

page.razor 中的第 2 行的第 6 列是文本 @DateTime.NowAndThen 的开始位置。 指令中的 (2, 6) 已指出这一点。 @DateTime.NowAndThen 的结束位置在第 2 行的第 27 列。 指令中的 (2, 27) 已指出这一点。 DateTime.NowAndThen 的文本从 page.g.cs 的第 15 列开始。 指令中的 15 已指出这一点。 将所有参数放在一起,编译器将在其在 page.razor 中的位置报告错误。 开发人员可直接导航到该错误在源代码(而不是生成的源)中的位置。

若要查看此格式的更多示例,请参阅示例部分中的功能规范

杂注

#pragma 为编译器给出特殊指令以编译它所在的文件。 这些指令必须受编译器支持。 换句话说,不能使用 #pragma 创建自定义的预处理指令。

#pragma pragma-name pragma-arguments

其中 pragma-name 是可识别 pragma 的名称,pragma-arguments 是特定于 pragma 的参数。

#pragma warning

#pragma warning 可以启用或禁用特定警告。

#pragma warning disable warning-list
#pragma warning restore warning-list

其中 warning-list 是以逗号分隔的警告编号的列表。 “CS”前缀是可选的。 未指定警告编号时,disable 会禁用所有警告,restore 会启用所有警告。

注意

若要在 Visual Studio 中查找警告编号,请生成项目,然后在“输出”窗口中查找警告编号。

disable 从源文件的下一行开始生效。 警告会在后面的 restore 行上还原。 如果文件中没有 restore,则在同一编译中任何之后文件的第一行,警告将还原为其默认状态。

// pragma_warning.cs
using System;

#pragma warning disable 414, CS3021
[CLSCompliant(false)]
public class C
{
    int i = 1;
    static void Main()
    {
    }
}
#pragma warning restore CS3021
[CLSCompliant(false)]  // CS3021
public class D
{
    int i = 1;
    public static void F()
    {
    }
}

#pragma checksum

生成源文件的校验和以帮助调试 ASP.NET 页面。

#pragma checksum "filename" "{guid}" "checksum bytes"

其中,"filename" 是需要监视更改或更新的文件的名称,"{guid}" 是哈希算法的全局唯一标识符 (GUID),"checksum_bytes" 是表示校验和字节的十六进制数字的字符串。 必须是偶数个十六进制数字。 奇数个数字会导致编译时警告出现,且指令遭忽略。

Visual Studio 调试器使用校验和确保它可始终找到正确的源。 编译器为源文件计算校验和,然后将输出发出到程序数据库 (PDB) 文件。 调试器随后使用 PDB 针对它为源文件计算的校验和进行比较。

此解决方案不适用于 ASP.NET 项目,因为计算出的校验和是针对生成的源文件,而不是 .aspx 文件。 为解决此问题,#pragma checksum 为 ASP.NET 页面提供校验和支持。

在 Visual C# 中创建 ASP.NET 项目时,生成的源文件包含 .aspx 文件(从该文件生成源)的校验和。 编译器随后将此信息写入 PDB 文件中。

如果编译器在文件中没有找到 #pragma checksum 指令,它将计算校验和,并将该值写入 PDB 文件。

class TestClass
{
    static int Main()
    {
        #pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
    }
}