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

条件编译,顾名思义,就是根据在编译时指定的条件决定最后需要编译的代码。条件编译是我们可以针对某些特性的环境编写相应的代码,比如有写的代码只需要在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:\Others\Conditional
   3: Compilation\Program.cs:line 28
   4:    at Artech.ConditionalCompilation.Program.Main(String[] args) in E:\Others\Con
   5: ditionalCompilation\Program.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: }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏玩转JavaEE

Redis中的五种数据类型简介

上篇文章我们介绍了如何在Linux中安装Redis,本文我们来了解下Redis中的五种数据类型。 本文是Redis系列的第二篇文章,了解前面的文章有助于更好的理...

3777
来自专栏james大数据架构

Android实现TCP断点上传,后台C#服务实现接收

终端实现大文件上传一直都是比较难的技术,其中涉及到后端与前端的交互,稳定性和流量大小,而且实现原理每个人都有自己的想法,后端主流用的比较多的是Http来实现,因...

3059
来自专栏老马说编程

(94) 组合式异步编程 / 计算机程序的思维逻辑

前面两节讨论了Java 8中的函数式数据处理,那是对38节到55节介绍的容器类的增强,它可以将对集合数据的多个操作以流水线的方式组合在一起。本节继续讨论Java...

2017
来自专栏WebApiClient

WebApiClient基础

如果接口IMyWebApi有多个方法且都指向同一服务器,可以将请求的域名抽出来放到HttpHost特性。

5160
来自专栏大内老A

WCF技术剖析之十七:消息(Message)详解(下篇)

《WCF技术剖析(卷1)》自出版近20天以来,得到了园子里的朋友和广大WCF爱好者的一致好评,并被卓越网计算机书店作为首页推荐,在这里对大家的支持表示感谢。同时...

2315
来自专栏angularejs学习篇

.net捕捉全局未处理异常的3种方式

 我们在实际项目开发中,经常会遇到一些不可预见的异常产生,有的异常在程序运行时就对其进行处理(try) 但是,有的程序不需要每一个地方都用try进行处理,那么针...

2043
来自专栏有趣的django

CRM客户关系管理系统(十三) 第十三章、用户自定义认证第十四章、万能通用权限框架设计

4070
来自专栏大内老A

WCF技术剖析之六:为什么在基于ASP.NET应用寄宿(Hosting)下配置的BaseAddress无效

本篇文章来源于几天前一个朋友向我咨询的问题。问题是这样的,他说他采用ASP.NET应用程序的方式对定义的WCF服务进行寄宿(Hosting),并使用配置的方式对...

1917
来自专栏Jackson0714

03.如何实现一个遥控器-命令模式

3527
来自专栏更流畅、简洁的软件开发方式

数据访问函数库 for ado.net2.0

前言 源代码和调用演示下载:http://www.cnblogs.com/jyk/archive/2008/04/25/1170979.html 数据访问函...

1927

扫码关注云+社区

领取腾讯云代金券