从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: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: }
- 3301: [USACO2011 Feb] Cow Line
- SQL Server 索引和表体系结构(包含列索引)
- TiDB 源码阅读系列文章(七)基于规则的优化
- 博弈论入门之nim游戏
- 1477: 青蛙的约会
- 2045: 双亲数
- 树莓派常用文本编辑器
- BZOJ1299: [LLH邀请赛]巧克力棒(Nim游戏)
- 2301: [HAOI2011]Problem b
- mysql相关命令
- mac python3 安装 mysqlclient包失败如何解救
- SQL Server 深入解析索引存储(上)
- 1475: 方格取数
- python3 将字典,列表等转换成字符串形式存入mysql数据库并复原成字典,列表(处理稍复杂的格式)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- redis实战第四篇 手动容灾故障转移记录
- 10条很棒的Python一行代码
- 如何在一个Docker中同时运行多个程序进程?
- 如何在CentOS / RHEL 7上启用IPv6
- Golang中的RegExp正则表达式用法指南
- Golang glog使用详解
- kubernetes使用securityContext和sysctl
- 浅谈分词算法基于字的分词方法(HMM)
- 优雅的重启服务
- Go defer 会有性能损耗,尽量不要用?
- 带入gRPC:分布式链路追踪 gRPC + Opentracing + Zipkin
- 聊聊Golang逃逸分析
- 结构型设计模式:适配器模式和门面模式
- 结构型设计模式:代理模式
- kubernete中的原子调度单位:pod