从Trace和Debug来看条件编译(Conditional Compilation)

时间:2022-04-27
本文章向大家介绍从Trace和Debug来看条件编译(Conditional Compilation),主要内容包括一、Trace.WriteLine() V.S. Debug.WriteLine()、三、一个条件编译的例子、四、看看编译后的代码、五、ConditionalAttribute与#if/#endif、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

条件编译,顾名思义,就是根据在编译时指定的条件决定最后需要编译的代码。条件编译是我们可以针对某些特性的环境编写相应的代码,比如有写的代码只需要在Debug模式下才需要执行,有些代码仅仅是为了在SIT或者UAT环境下有效地进行Troubleshooting,而在Production环境下则不应该执行。通过条件编译机制,我们可以针对某中特定的“条件编译符(Conditional Compilation Symbol)”编写相应的代码。在进行最终编译的时候,通过指定的条件编译符,编译器判断这些特殊的代码是否应该被编译。

目录: 一、Trace.WriteLine() V.S. Debug.WriteLine() 二、一个重要的特性ConditionalAttribute 三、一个条件编译的例子 四、看看编译后的代码 五、ConditionalAttribute与#if/#endif

一、Trace.WriteLine() V.S. Debug.WriteLine()

为了让大家对条件编译有一个相对直观的认识,我们举一个大家很熟悉的例子。我们都知道,在Trace和Debug是定义在System.Diagnostics命名空间下两个重要的用于应用程序“诊断”的类,我们可以通过它们的静态方法Write或者WriteLine方法写入一些追踪和调试消息。如果你对Trace和Debug具有一定的了解,你应该知道定义在它们之中的Write或者WriteLine方法具有相同的实现,最终都是将消息传递给配置的TraceListener,并被写入相应的目标存储中。

为了演示Trace和Debug消息写入机制,我写了一个非常简单的程序。首先我通过继承TraceListener,写了一个自定义的TraceListener:ConsoleTraceListener。ConsoleTraceListener实现了抽象方法Write和WriteLine,直接将消息通过控制台打印出来。这个ConsoleTraceListener定义如下:

   1: public class ConsoleTraceListener : TraceListener
   2: {
   3:     public override void Write(string message)
   4:     {
   5:         Console.Write(message);
   6:     }
   7:  
   8:     public override void WriteLine(string message)
   9:     {
  10:         Console.WriteLine(message);
  11:     }
  12: }

然后,我们通过下面的配置,将这个自定的ConsoleTraceListener应用到.NET的Tracing体系中:

   1: <?xml version="1.0"?>
   2: <configuration>
   3:   <system.diagnostics>
   4:     <trace>
   5:       <listeners>
   6:         <add name="ConsoleTraceListener" type="Artech.ConditionalCompilation.ConsoleTraceListener, Artech.ConditionalCompilation"/>
   7:       </listeners>
   8:     </trace>
   9:   </system.diagnostics>
  10: </configuration>

最后我们编写如下的代码,分别调用Debug和Trace的WriteLine方法写入一段指定的消息:

   1: static void Main(string[] args)
   2: {
   3:     Trace.WriteLine("This is message written by invoking Trace.WriteLine method.");
   4:     Debug.WriteLine("This is message written by invoking Debug.WriteLine method.");
   5: }

我们指定的消息将会通过ConsoleTraceListener直接写入到控制台上:

   1: This is message written by invoking Trace.WriteLine method.
   2: This is message written by invoking Debug.WriteLine method.

二、一个重要的特性ConditionalAttribute

从上面的例子,我们基本上可以看出定义在Trace和Debug中的WriteLine方法在实现上并没有什么不同之处,最终的诊断消息的写入操作都是通过配置好的TraceListener列表来完成的。如果你通过Reflector来看看WriteLine方法在两者中的实现,你更会发现方法的实现逻辑是一样的。

   1: public static class Debug
   2: {
   3:     //...
   4:     [Conditional("DEBUG")]
   5:     public static void WriteLine(string message)
   6:     {
   7:         TraceInternal.WriteLine(message);
   8:     }
   9: }
  10:  
  11: public sealed class Trace
  12: {
  13:     //...
  14:     [Conditional("TRACE")]
  15:     public static void WriteLine(string message)
  16:     {
  17:         TraceInternal.WriteLine(message);
  18:     }
  19: }

不但WriteLine方法在Trace和Debug中的实现一样,而且这两个方法均具有一个相同的特性ConditionalAttrite,所不同的ConditionalAttrite中具有不同的参数,分别是DEBUG和TRACE。这个特殊的ConditionalAttribute特性就涉及到我们今天讨论的主题:条件编译,这个特性中指定的参数(DEBUG和TRACE)就是我们之前说的条件编译符。

当我们调用一个应用了ConditionalAttribute特性的方法,在编译的时候,方法调用代码只有在指定了相应的条件编译符的情况下才会参与编译。比如说,我们调用Trace.WriteLine方法,但是在编译的时候我们没有指定TRACE这个条件编译符,在最终编译的程序集中,是没有这句代码的。

C#和VB.NET编译器(csc.exe, vbc.exe)定义相应的命令行参数使你利用指定条件编译符。如果你完全采用VS进行编译,在默认的情况下,TRACE这个条件编译符会自动会包含进行,在Debug模式下条件编译符DEBUG会被包含进来,而Release模式则不会。你可以通过项目属性对话框的Build页选择是否需要包含DEBUG和TRACE这两个条件编译符,你也可以定义你自己的条件编译符。比如下面的设置中,我选择包含DEBUG和TRACE这两个条件编译符,同时自定义了一个新的条件编译符:UAT,表明本次编译环境为用户接收测试。

三、一个条件编译的例子

为了更好地说明条件编译的意义,我写了另一个小小的例子。场景时这样的:有些逻辑需要在被授权的条件下才能被指定,但是为了测试方便(测试人员可以采用匿名用户进行测试),我们希望授权的检查只有在Production环境下才生效,开发、SIT和UAT阶段则不需要。我们就可以通过条件编译机制来解决这个问题。

首先我们简化授权的逻辑,假设只有具有Admin角色的用户才是授权的用户。这样的授权逻辑被定义在如下的Authorize方法中,在该方法上应用了ContitionalAttribute特性,并将作为参数的条件编译符定义成PRODUCTION,表明这个方法只有在Production环境中有效。

   1: [Conditional("PRODUCTION")]
   2: public static void Authorize()
   3: {
   4:     if (!Thread.CurrentPrincipal.IsInRole("Admin"))
   5:     {
   6:         throw new SecurityException("Access denied!");
   7:     }
   8: }

这个Authorize方法会在如下的情况下被调用:当前线程被赋予了一个角色列表为空的GenericPrincipal对象。

   1: static void Main(string[] args)
   2: {
   3:     var identity            = new GenericIdentity("Foo");
   4:     var principal           = new GenericPrincipal(identity, new string[0]);
   5:     Thread.CurrentPrincipal = principal;
   6:     Authorize();
   7:     Console.WriteLine("Continue...");
   8: }

在默认的情况下(没有显式指定条件编译符),我们定义的授权检查不会发生,运行我们的程序,不会得到任何的异常。但是,如果我们通过VS的项目属性对话框,自定义一个PRODUCTION条件编译符,再次运行程序,定义在Authorize方法中的授权检验将会生效。下面是输出结果:

   1: Unhandled Exception: System.Security.SecurityException: Access denied!
   2:    at Artech.ConditionalCompilation.Program.Authorize() in E:OthersConditional
   3: CompilationProgram.cs:line 28
   4:    at Artech.ConditionalCompilation.Program.Main(String[] args) in E:OthersCon
   5: ditionalCompilationProgram.cs:line 19

四、看看编译后的代码

我们之前已经说了,条件编译就是在编译的时候将指定的条件编译符动态去过滤不需要参与编译的源代码。对于调用了ConditionalAttribute特性的方法,只有里面的参数和指定的条件编译符一致,相应的代码才会参与编译。以上面的代码为例,在我们没有指定PRODUCTION条件编译符的情况下,编译出来包含在程序集中的代码等同于下面:

   1: private static void Main(string[] args)
   2: {
   3:     GenericIdentity identity = new GenericIdentity("Foo");
   4:     GenericPrincipal principal = new GenericPrincipal(identity, new string[0]);
   5:     Thread.CurrentPrincipal = principal;
   6:     Console.WriteLine("Continue...");
   7: }

五、ConditionalAttribute与#if/#endif

我个人推荐尽量将条件编译的代码封装到一个方法中,并在上面应用ConditionalAttribute特性。如果不能,才使用#if/#endif这样的条件编译指令。如果我们采用内联的方式来实现基于上面的授权检验,我们可以直接使用#if/#endif块来封装授权逻辑。相应的代码如下:

   1: static void Main(string[] args)
   2: {
   3:     var identity = new GenericIdentity("Foo");
   4:     var principal = new GenericPrincipal(identity, new string[0]);
   5:     Thread.CurrentPrincipal = principal;
   6:     #if PRODUCTION
   7:         if (!Thread.CurrentPrincipal.IsInRole("Admin"))
   8:         {
   9:             throw new SecurityException("Access denied!");
  10:         }   
  11:     #endif
  12:     Console.WriteLine("Continue...");
  13: }