2.1.5 C#8.0教程

如果你熟悉 C语言 或它的直系衍生语言,你一定了解预处理器的基本概念。那么 C# 是否提供了预处理器?结论是:它没有单独的预处理阶段,也不提供宏。然而,它确实有一些类似于 C语言 预处理器提供的指令,尽管它的选择非常有限。尽管 C# 不像 C 一样有完整的预处理阶段,但是这些被称为预处理指令。本课将会提到一些常用的指令。

  1. 编译符号
  2. #error 与 #warning
  3. #line
  4. #pragma
  5. #nullable
  6. #region and #endregion

编译符号

C# 提供了一个 #define 指令,可以让你定义编译符号。这些符号通常与 #if 指令一起使用,以不同的方式针对不同的情况编译代码。例如,你可能希望某些代码只出现在调试中,或者你可能需要在不同的平台上使用不同的代码来实现特定的效果。通常,你不会使用 #define 指令,但更常见的是通过编译器构建设置来定义编译符号。要对此进行控制,可以直接打开 .csproj 项目文件,在 PropertyGroup 中的 DefineConstants 元素中定义需要的值。

.NET SDK 定义了某些默认符号。它支持两种配置:调试Debug 和 发布Release。它在调试配置中定义 DEBUG 编译符号,而在发布中定义了 RELEASE。在两种配置中定义了一个两者共有的称为 TRACE 的符号。某些项目类型使用额外的符号。例如,针对 .NET Standard 2.0 将同时定义NETSTANDARD 和 NETSTANDARD2_0。

编译符号通常与 #if、#else、#elif 和 #endif 指令一起使用(#elif是else if的缩写)。例5.1 使用其中一些指令来确保某些代码行只在调试版本中编译。(你还可以编写#if false来完全防止代码段被编译。这通常只是作为一种临时措施,是注释的替代方法,避免了试图嵌套注释时的词汇陷阱。)

<code>// 示例 5.1
static void Main(string[] args)
{
    #if RELEASE
        Console.WriteLine("RELEASE ...");
    #endif
    #if DEBUG
        Console.WriteLine("DEBUG ...");
    #endif
}/<code>
2.1.5 C#8.0教程 - 预处理指令

图1. 此时会执行的预编译指令

c#提供了一种更微妙的机制来支持这类事情,称为条件方法。编译器识别.NET类库定义的称为ConditionalAttribute的属性,并为其提供特殊的 编译时compile-time 行为。你可以给任何方法用此属性注释。例5.2 使用它表示只有在定义了调试编译符号时才应该使用带注释的方法。

<code>// 示例 5.2
static void Main(string[] args)
{
    ShowDebugInfo();
    ShowReleaseInfo();
}

[System.Diagnostics.Conditional("DEBUG")]
static void ShowDebugInfo()
{
    Console.WriteLine("Conditional DEBUG ...");
}

[System.Diagnostics.Conditional("RELEASE")]
static void ShowReleaseInfo()
{
    Console.WriteLine("Conditional RELEASE ...");
}/<code>
2.1.5 C#8.0教程 - 预处理指令

图2. Debug阶段输出结果

2.1.5 C#8.0教程 - 预处理指令

图3. Release阶段输出结果

注意对比图2 、图3的两个条件方法,一个是在 Debug 阶段运行的,一个是在 Release 阶段运行的。

特别要注意输出面板,分别输出了 “Conditional DEBUG ...” 和 “Conditional RELEASE ...”。说明条件方法在起作用。

另外要特别理解的是,如果您编写的代码调用了这样注释的方法,c#编译器将在没有定义相关符号的编译中忽略该调用。因此,如果你编写代码来调用这两个 ShowDebugInfo

/ ShowReleaseInfo 方法,编译器会在编译中删除另外一个调用方法,但不会使用指令使代码混乱。

.NET类库中的 Debug类 和 Trace类 在System.Diagnostics命名空间使用此特性。Debug类提供了各种以调试编译符号为条件的方法,而Trace类有以跟踪为条件的方法。如果保留新C#项目的默认设置,那么通过Trace类生成的任何诊断输出都将在调试和发布版本中可用,但是调用Debug类上的方法的任何代码都不会编译到发布版本中。

#error 与 #warning

C# 允许您使用 #error 和 #warning 指令来选择生成编译器错误或警告。这些通常在条件区域内使用,如例2-30所示,当然一个无条件执行的 #warning 可以用来提醒自己还没有编写一些特别重要的代码。

<code>// 示例 5.3
static void Main(string[] args)
{
    #warning 当您运行此代码后,您有权保持沉默,您所说的一切将会作为呈堂证供!
    #error 停止交易,有内鬼!
}/<code>
2.1.5 C#8.0教程 - 预处理指令

图4. 请注意输出结果

如图4.所示,#warning 会导致出现警告提示,但仍会继续编译过程。但 #error 除了会出现错误提示外,还会导致编译失败,或者称 build 错误,修复后才能继续。

#line

在生成的代码中,#line指令非常有用。当编译器生成错误或警告时,它通常会声明问题发生的位置,提供文件名、行号和该行内的偏移量。但是,如果问题代码是使用其他文件作为输入自动生成的,并且其他文件包含问题的根本原因,那么在输入文件中报告错误可能比在生成的文件中报告错误更有用。一个#line指令可以指示c#编译器就像错误发生在指定的行号上一样,也可以选择像错误发生在一个完全不同的文件中一样。例5.4 展示了如何使用它。指令后的错误将被报告,好像它来自一个名为footbar.cs的文件的第999行。

<code>static void Main(string[] args)
{
    Inttt x;
    #line 999 "footbar.cs"
    Inttt x;
}/<code>
2.1.5 C#8.0教程 - 预处理指令

图5. 两行错误,但行号所指不同

如图5.所示,虽然 Program.cs 的第9行、第11行出现两处明显的语法错误。但提示出现的文件及所在位置却明显不同。请特别关注橙色箭头与白色箭头所示的区别。

另外,文件名部分是可选的,行号也可以伪造,一切都只是注释。您可以通过编写#line default来告诉编译器恢复报告正常的警告和错误状态,否则后面出现的一切错误都会以这个基础继续。

#pragma

#pragma 指令提供了两个特性:它可以用来禁用选定的编译器警告,还可以用来覆盖编译器放入它生成的包含调试信息的 .pdb 文件中的校验和值。这两种方法主要是为代码生成场景设计的,不过它们有时也可以用于禁用普通代码中的警告。例5.5展示了如何使用 #pragma 来防止您声明但没有继续使用的变量而弹出的警告信息。

<code>static void Main(string[] args)
{
    int x;
    #pragma warning disable CS0168
    int y;
}/<code>
2.1.5 C#8.0教程 - 预处理指令

图6. 关闭警告后,变量y 不会被提示未使用

如图6.所示,在启用 #pragma 指令前,会提示警告变量x未使用。但在启用后,变量y的警告提示未显示。

通常应该避免禁用警告。此特性在生成的代码中非常有用,因为代码生成通常会创建不经常使用的项,而pragmas可能提供了获得干净编译的唯一方法。但是当您手工编写代码时,通常应该能够首先避免警告。

总的来说,如果不是强迫症患者,请勿使用该功能。当然,#pragmas 还有一些其他的能力,比如说为 C# 8.0 中添加的新特性 nullable 等。但对于初学者来说,好像暂时无所谓……但毕竟课题是 C# 8.0 教程,所以我们不得不说一些关于 8.0 的新知识。如下:

#nullable

C# 8.0添加了一个新的指令 #nullable,它允许对可空的注释上下文进行精细控制。这是之后章节中描述的可空引用特性的一部分。(这与之前 #pragmas 中描述的 nullable 警告控件没有重叠,因为我们控制是否启用了nullability注释,与是否启用了与这些注释关联的警告无关)

#region and #endregion

最后,我们有两个不做任何事情的预处理指令。如果你写了#region指令,编译器做的唯一一件事就是确保它们有或者响应#endregion指令。不匹配会导致编译器错误,但是编译器会忽略正确配对的#region和#endregion指令。区域可以嵌套。

这些指令的存在完全是为了便于选择识别它们的文本编辑器。Visual Studio 或 Visual Studio Code 等 C#编辑器使用这组指令可将其包含的代码部分折叠为屏幕上的一行。c#编辑器自动允许扩展和折叠某些特性,比如类定义、方法和代码块(称为outline的特性)。如果您使用这两个指令定义区域,它还将允许扩展和折叠这些区域。

如果将鼠标悬停在折叠区域上,Visual Studio Code 等多数C#编辑器将显示工具提示,显示该区域的内容。

2.1.5 C#8.0教程 - 预处理指令

图7. 红色箭头所示的 “下拉折叠按钮”

如图7.所示,因为增加了一组 #region 指令,则会在行号后面自动增加出 折叠按钮 (红色箭头所示),会将从 #region 开始,到 #endregion 结束的代码块折叠起来。

虽然代码块 { } 符号也可实现折叠效果,但区别在于,编译器会完全忽略 #region,但 { } 对于编译器来说是有意义的,所以不是完全等同。

本课小结

本课所提到预编译指令,不能给我们的程序带来更多的功能,也无法提高代码执行效率。更多是为代码编辑者使用。大家了解即可,未来读程序时肯定会遇到,可以再来细读。

下节开始数据类型……


分享到:


相關文章: