C# 预编译指令环境参数

C# 预编译指令环境参数

C#中预处理指令的基本概念以及具体谈#IF指令参数的用法

上周在处理一份老项目的代码时发现代码中出现一个 #if INTERNAL 的代码块。 项目的编译环境有 Debug, InternalRelease 三个。 程序原意应该是只有在 Internal 环境下才执行代码块中的代码。 结果调试下来发现,这个指令并没有生效: 无论什么环境, 都能够调试到程序里面去, 由此引出来几个问题:

  1. 为什么这条指令没有生效
  2. 怎么样解决这个问题
  3. 怎样让这个指令生效
  4. 这是不是最好的解决办法?

为什么指令没有生效

首先先简单介绍一下这种格式的预编译指令的概念。 在 C# 编程时, 可以通过如 #if DEBUG 等这样的指令来指定某些代码的执行条件。 这其中 #if 是预编译指令,DEBUG 是指令参数。 一般预处理指令都是成对出现,一对指令中间的代码就是其作用的代码,功能类似与大括号。 也有单条出现的, 作用在该行后面的代码。 常见的预处理指令有

  • #if / #else/ #elif / # endif 顾名思义。 但是是预处理, 编译的时候便知道该执行的分支,不需要再运行的时候才判断, 由 #endif 来表示代码块结束
  • # region / #endregion 用来包住一块代码并命名(注释),没有实际的执行含义, 但是在编程时默认会收起, 只显示名字(注释),以达到简洁代码的目的
  • #define / endef 定义预处理变量和取消定义,常与 #if 组并用
  • #warning 编译时在此处会产生一条警告消息
  • #error 编译时在此处会产一条错误消息,并且编译会失败

下面简单介绍下 C# 里面 #if 的参数。

首先, #if 后面紧跟的参数,再 C# 里面称之为 符号,这个参数可以是一个 bool 值的类型, 也可以是一个返回 bool值的表达式。例如, 下面这两个指令效果时一样的

// 1
#if DEBUG
// some code
#endif

//2
#if (DEBUG == true)
// some code
#endif

其次,是这个参数的来源。 这些参数里面的条数是怎么来的呢? 首先需要明确的是,既然这个是预处理指令,说明这个指令应该是再代码执行之前生效的,那么这个参数就不能用代码中的变量

// 下面这段代码不会编译错误, 但是没有输出。 因为 #if 后面的 a 实际上没有定义
var a = true;
#if a == true
Console.WriteLine("从变量中获取");
#endif

这个参数主要有两种办法获取:

  1. 通过 #define 预处理指令
  2. 通过 -define 编译指令

第一种, #define 预处理指令。 也是我们最常见到的方式,具体说就是在文件最开头指定对应的变量。 这里需要注意#define指令必须全部写在文件最开头(即所有 using 指令之前), 并且只能定义,不能赋值,定义之后该符号值为true, 例如:

#define a
using System;
namespace ConsoleTest
{
	class Program
	{
		static void Main(string[] args)
		{
			var a = false;
			#if a
			Console.WriteLine("'a' was defined");
			#endif
		}
	}
}

// 输出:'a' was defined

这种办法的特点是, 所定义的符号只在当前文件中生效,不会影响其他代码。

第二种办法,-define 编译指令, 即在编译的时候定义一些预定义符号来使代码能够根据不同的符号做不同的操作。跟第一种办法最明显区别就是通过这个办法进行定义的符号, 会在整个项目中生效。 直到第一次遇到对应的 #undef 指令后失效。 所以如果项目中有多处用到一个预处理符号, 适合用这种办法实现。

#undef 指令也是必须写在文件的开头,比如需要取消 DEBUG符号:#undef DEBUG

很显然,我们开头将的 DEBUG 就是通过这种办法定义的。这种办法的具体操作如下:

在 VisualStudio 的项目上右键 -> 属性 -> 生成(对应英文:Property -> Build) 选项卡中, 可以设置 DEBUG, TRACE 两种预设符号,以及在输入框中可以设置自定义符号: 这样, 只要在不同平台中,按照需求进行预设,便可以在代码中使用对应的变量了。

怎样解决这个问题 & 怎样让指令生效

回到上面一开头我们的问题,如果这个时候定义了一种新的编译环境Internal, 那么怎样让代码中的 INTERNAL 指令生效嗯?

最开始写出这样的代码,是自然以为#if 指令的参数直接等于编译环境, 现在看来显然不是, 通过了解上面的内容之后, 那么这两个问题就很好解决了。

首先回答第二个问题: 怎么样让 INTERNAL 符号生效? 如上面图片所示,我们只要在**“条件和编译符号”**输入框中输入 “INTERNAL” 便可以解决了。 另外,如果需要定义多个变量, 变量之间用逗号(,)分隔。 这样的话第一个问题自然也就解决了。

其次是第一个问题:怎样解决问题。这跟第一个问题并不完全相等。 自然,上面这样做看起来已经是最好的解决办法。 但是,如果结合具体场景的话,我们不妨想一想:这段代码是不是只有在Internal环境下执行,因为从字面意义来看, Internal 可能是在某些内部环境测试用的执行环境。 可是在本地进行调试的时候, 是不是也需要执行相应的代码呢? 如果是的话那么, 除了定义 INTERNAL 符号之外, 预处理指令应该定义成 #if DEBUG || INTERNAL 应该更合适些。

又或者既然 Internal 环境其实跟 Debug 在此处表现是一致的, 那么, 只需要 #if DEBUG 即可?

当然这个需要具体问题具体分析。 只是说有时候需要意识到: 解决了关键问题 != 解决了问题

这是不是最好的解决办法?

这一段不是为了给出答案, 因为存在即合理,C# 既然存在这么一条功能,字必有其发挥用处的地方, 比如说在在 DEBUG 环境下做一些模拟数据, 或者一些性能检测,逻辑验证性的代码, 这些代码本身并不影响功能的运行,所以可以通过预处理指令来选择其运行的环境。

但是如果不是上面这些情况却也用到了预处理指令的时候, 我们不妨多想一下, 这是不是最好的解决办法, 还有没有更好的解决办法? 最典型的一个例子是在我刚学习 ASP.NET Core 做 WebApi 开发的时候, 不同环境下的配置便用这个指令来区分, 大概的代码如下:

#if DEBUG
    var connectString = "database=dev....";
    // some service or repository in dev environment
#else
    var connectString = "database=product....";
    // some service or repository in procuct environment
#endif

看起来还不错? 但是实际上这并不是很好的办法, 因为首席按代码变得很冗余, 另外也很容易让人产生迷惑, 仿佛一个值被定义两次一般, 再者, 想前面说到的当我加第三种环境时,这个地方又不知要修改多少代码。

更重要的时,对于这种情况, 其实 .NetCore 提供了更加优雅的解决方式,那就是用不同环境下的配置文件来选择配置, 这么一来我们只要指定本次运行的时什么环境便可。 详情参考下一篇文章 -- 【.NetCore 中的配置文件】 (还没写


参考链接: #if preprocessor directive - C# Reference | Microsoft Docs