C#使用ConditionalAttribute特性来实现代码调试
转自:http://www.csharpwin.com/csharpspace/10729r8541.shtml
#if/#endif条件编译常用来由同一份源代码生成不同的结果文件,最常见的有debug版和release版。但是,这些工具在具体应用中并不是非常得心应手,因为它们太容易被滥用了,使用它们创建的代码通常都比较难理解,且难以调试。C#语言的设计者们对这种问题的解决方案是创建更好的工具,以达到为不同环境创建不同机器码的目的。C#为此添加了一个Conditional特性,该特性可以标示出某种环境设置下某个方法是否应该被调用。使用这种方式来描述条件编译要比#if/#endif更加清晰。由于编译器理解Conditional特性,所以它可以在Conditional特性被应用时对代码做更好的验证。Conditional特性应用在方法这一层次上,因此它要求我们将条件代码以方法为单位来表达。当需要创建条件代码块时,我们应该使用Conditional特性来代替传统的#if/#endif。
大多数程序老手都使用过条件编译来检查对象的前置条件和后置条件。例如,编写一个私有方法来检查所有类与对象的不变式(invariant,然后将这样的方法进行条件编译,从而让其只出现在debug版本的程序中。
private void CheckState()
{
string _lastName = "";
string _firstName = "";
//... // 老式的做法:
#if DEBUG
Trace.WriteLine("Entering CheckState for Person");
// 获取正在被调用函数的名称:
string methodName = new StackTrace().GetFrame().GetMethod().Name;
Debug.Assert(_lastName != null, methodName, "Last Name cannot be null");
Debug.Assert(_lastName.Length > , methodName, "Last Name cannot be blank");
Debug.Assert(_firstName != null, methodName, "First Name cannot be null");
Debug.Assert(_firstName.Length > , methodName, "First Name cannot be blank");
Trace.WriteLine("Exiting CheckState for Person");
#endif
}
条件编译#if和#endif使得最终release版本中的CheckState()成为一个空方法,但它在release版和debug版中都将得到调用。虽然在release版中,CheckState()什么也不做,但是我们必须为方法的加载、JIT编译和调用付出成本。
就正确性而言,这种做法一般没什么问题,但有时候还是可能会在release版本中导致一些诡异的bug。下面的代码展示了使用#if和#endif条件编译时可能常犯的错误:
public void Func()
{
string msg = null;
#if DEBUG
msg = GetDiagnostics();
#endif
Console.WriteLine(msg);
}
上面的代码在debug版本中运行得很好,但是放到release版本中就会输出一个空行。输出一个空行本身没有什么,但这毕竟不是我们本来的意图。我们自己搞糟的事情,编译器也帮不上什么忙,因为我们把属于程序主逻辑的代码和条件编译代码混在一起了。在源代码中随意地使用#if和#endif将使我们很难诊断不同版本间的行为差别。
C#为此提出了一种更好的选择:Conditional特性。使用Conditional特性,我们可以将一些函数隔离出来,使得它们只有在定义了某些环境变量或者设置了某个值之后才能发挥作用。Conditional特性最常用的地方就是将代码改编为调试语句。.NET框架已经为此提供了相关的功能支持。下面的代码展示了Conditional特性的工作原理,以及适用场合。
构建Person对象时,我们一般会添加如下的方法来验证对象的不变式:
private void CheckState()
{
string _lastName = "";
string _firstName = "";
//......
// 获取正在被调用函数的名称:
string methodName = new StackTrace().GetFrame().GetMethod().Name;
Trace.WriteLine("Entering CheckState for Person:");
Trace.Write("\tcalled by ");
Trace.WriteLine(methodName);
Debug.Assert(_lastName != null, methodName, "Last Name cannot be null");
Debug.Assert(_lastName.Length > , methodName, "Last Name cannot be blank");
Debug.Assert(_firstName != null, methodName, "First Name cannot be null");
Debug.Assert(_firstName.Length > , methodName, "First Name cannot be blank");
Trace.WriteLine("Exiting CheckState for Person");
}
有些读者可能对上面代码中的一些库函数还不够熟悉,我们来简单介绍一下。StackTrace类使用反射(reflection,参见条款43)来获取当前正被调用的方法名。其代价相当高,但它可以极大地简化我们的工作,例如帮助我们获取有关程序流程的信息。在上面的代码中,使用它,我们便可以得到正被调用的方法名称为CheckState。其余的方法在另外两个类中,分别为System.Diagnostics.Debug和System.Diagnostics.Trace。Debug.Assert方法用于测试某个条件,如果该条件错误,程序将被终止,其他参数定义的消息也将被打印出来。Trace.WriteLine方法则会把诊断信息打印到调试控制台上。因此,如果有Person对象状态无效,CheckState方法将会显示信息,并终止程序。我们可以将其作为前置条件和后置条件,在所有的公有方法和受保护方法中调用它。
public string LastName
{
get
{
CheckState();
return _lastName;
}
set
{
CheckState();
_lastName = value;
CheckState();
}
}
当首次试图将LastName属性设置为空字符串或者null时,CheckState将引发一个断言错误。这样我们就会修正set访问器以检查传递给LastName的参数。这正是我们想要的功能。
但在每个公有函数中都做这样的额外检查显然比较浪费时间,我们可能只希望其出现在调试版本中。这就需要Conditional特性了:
[Conditional("DEBUG")]
private void CheckState()
{
// 代码保持不变。
}
应用了Conditional特性之后,C#编译器只有在检测到DEBUG环境变量时,才会产生对CheckState方法的调用。Conditional特性不会影响CheckState()方法的编译,它只会影响对该方法的调用。如果定义有DEBUG符号,上面的LastName属性将变为如下的代码:
public string LastName
{
get
{
CheckState();
return _lastName;
} set
{
CheckState();
_lastName = value;
CheckState();
}
}
否则,将得到如下代码:
public string LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
}
}
无论是否定义有DEBUG符号,CheckState()方法的方法体都维持不变,它都会被C#编译器处理,并生成到结果程序集中。这个例子其实也向大家展示了C#编译器的编译过程与JIT编译过程之间的区别。这种做法看起来也会带来一点效率损失,但是其中耗费的成本仅仅是磁盘空间。如果没有被调用,CheckState()方法并不会加载到内存中并进行JIT编译。将CheckState()方法生成到程序集中产生的影响是非常微不足道的。这种策略耗费很小的性能,换来的却是灵活性。如果感兴趣的话,大家可以查看.NET框架类库中的Debug类来对此获得更深的理解。在每个安装有.NET框架的机器上,System.dll程序集中都包含有Debug类中所有方法的代码。当调用这些方法的代码被编译时,系统环境变量将决定这些方法是否被调用。
我们创建的方法也可以依赖于多个环境变量。当我们应用多个Conditional特性时,它们之间的组合关系将为“或(OR)”。例如,下面的CheckState方法被调用的条件为定义有DEBUG或者TRACE环境变量:
[Conditional( "DEBUG" ),Conditional( "TRACE" )]
private void CheckState( )
{}
要创建一个使用“与(AND)”关系的构造,我们需要自己在源代码中定义预处理符号:
#if ( VAR1 && VAR2 ) #define BOTH #endif
是的,要创建一个依赖于多个环境变量的条件程序,我们不得不回到使用#if的老式做法中去。不过所有#if都只不过是创建新的符号而已,我们应该避免将可执行代码放在其中。
Conditional特性只可以应用在整个方法上。另外需要注意的是,任何一个使用Conditional特性的方法只能返回void类型。
我们不能在一个方法内的代码块上应用Conditional特性,也不可以在有返回值的方法上应用Conditional特性。为了应用Conditional特性,我们需要将具有条件性的行为单独放到一个方法中。虽然我们仍然需要注意那些Conditional方法可能给对象状态带来的负面效应,但Conditional特性的隔离策略总归要比#if/#endif好得多。使用#if和#endif代码块,我们很有可能会错误地删除一些重要的方法调用或者赋值语句。
C#使用ConditionalAttribute特性来实现代码调试的更多相关文章
- Web开发者的六个代码调试平台
代码调试平台是Web开发者进行开发.测试.分享.协作和交流的网络应用,它们支持实时的编辑.预览HTML.CSS和JavaScript的客户端代码.这些代码调试平台最值得称道的地方在于,它们中的大多数都 ...
- 关于代码调试de那些事
原文出处:http://www.wklken.me/posts/2014/11/23/how-to-debug.html 关于代码调试de那些事 1.你得明白你在做什么, 保持清醒 2.想清楚了再写代 ...
- javascript代码 调试方法
你的代码可能包含语法错误,逻辑错误,如果没有调试工具,这些错误比较难于发现. 通常,如果 JavaScript 出现错误,是不会有提示信息,这样你就无法找到代码错误的位置. 在程序代码中寻找错误叫做代 ...
- GDB代码调试与使用
GDB代码调试与使用 Linux下GDB调试代码 源代码 编译生成执行文件 gcc -g test.c -o test 使用GDB调试 启动GDB:gdb test 从第一行列出源代码:list 直接 ...
- OI中的代码调试
作为一位OIer,代码调试的能力必不可少. 今天梳理一下自己进行代码调试的方法,下面只是一些个人的总结. 代码的评价有三部分: 正确性 强健性 高效性 检查也应该从这三部分出发. [正确性] 打完代码 ...
- 初识 Javascript.02 -- Date日期、Math对象、数据类型转换、字符串、布尔Boolean、逻辑运算符、if else 、三元表达式、代码调试方法、
Date()对象: Date对象用于处理日期和时间. 1.1 Math对象 ◆Math.ceil() 天花板函数 向上取整 只取整数,不足则进1 ◆Math.floor() 地板函数 ...
- .NET Core快速入门教程 5、使用VS Code进行C#代码调试的技巧
一.前言 为什么要调试代码?通过调试可以让我们了解代码运行过程中的代码执行信息,比如变量的值等等.通常调试代码是为了方便我们发现代码中的bug.ken.io觉得熟练代码调试技巧是成为合格程序员的基本要 ...
- Node.js系列文章:如何进行代码调试
使用任何一门编程语言,都少不了代码调试这一功能.我们在使用JavaScript编写浏览器端代码时,Chrome提供了强大的调试工具Dev Tools,但是在编写Node.js代码时,大多数人最开始都使 ...
- #7 Python代码调试
前言 Python已经学了这么久了,你现在已经长大了,该学会自己调试代码了!相信大家在编写程序过程中会遇到大量的错误信息,我也不例外的啦-遇到这些问题该怎么解决呢?使用最多的方法就是使用print打印 ...
随机推荐
- C陷阱与缺陷 2
1,数组 对数组只能进行两种操作,1确定数组的大小,2获得数组第一个元素的指针,其他的操作均是通过指针来实现的. 1 2 3 4 5 6 7 8 9 #include <stdio.h> ...
- codevs3872 邮递员送信(SPFA)
邮递员送信 时间限制: 1 Sec 内存限制: 64 MB提交: 10 解决: 5[提交][状态][讨论版] 题目描述 有一个邮递员要送东西,邮局在节点1.他总共要送N-1样东西,其目的地分别是2 ...
- hdu1078 记忆化搜索(DP+DFS)
题意:一张n*n的格子表格,每个格子里有个数,每次能够水平或竖直走k个格子,允许上下左右走,每次走的格子上的数必须比上一个走的格子的数大,问最大的路径和. 我一开始的思路是,或许是普通的最大路径和,只 ...
- 自定义颜色显示的CheckBox
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- Linux驱动设计—— 中断与时钟@request_irq参数详解
request_irq函数定义 /*include <linux/interrupt.h>*/ int request_irq(unsigned int irq, irq_handler_ ...
- 浅谈开源项目Android-Universal-Image-Loader(Part 3.1)
本文转载于:http://www.cnblogs.com/osmondy/p/3266023.html 浅谈开源项目Android-Universal-Image-Loader(Part 3.1) 最 ...
- python文件操作--字符串替换
如把test.txt文件的 所有 AAA 字符串 替换成 aaaaa with open('test.txt','+r') as f: t = f.read() t = d.replace('AAA' ...
- Java的位运算 待整理
位运算:二进制运算 Java的异或运算^ 真^假=真 假^真=真 假^假=假 真^真= 假,这四个是在网上copy的例子,真是1,假是0 但它却是说明了Java异或运算的基本法则,那就是:只要两个条件 ...
- 单例模式-ios
#import <Foundation/Foundation.h> @interface UserContext : NSObject <NSCopying> @propert ...
- objective-c 一个链式加法计算器实现
一个链式加法计算器实现思路 1.使用时的效果 Calculate * manger=[Calculate new]; int result=manger.add(123).add(123).sub(1 ...