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打印 ...
随机推荐
- UVa 1394 约瑟夫问题的变形
https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...
- Spring MVC程序中得到静态资源文件css,js,图片文件的路径问题总结
上一篇 | 下一篇 Spring MVC程序中得到静态资源文件css,js,图片 文件的路径 问题总结 作者:轻舞肥羊 日期:2012-11-26 http://www.blogjava.net/fi ...
- LeetCode() Minimun Size Subarray Sum
别人的代码 class Solution { public: int minSubArrayLen(int s, vector<int>& nums) { int l, r, cu ...
- cron 配置计划任务的书写格式(quartz 时间配置)
一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素. 按顺序依次为 1.秒(0~59) 2.分钟(0~59) 3.小时(0~23) 4.天(月)(0~31,但是你需要考虑你月的天数) 5 ...
- hihoCoder#1080 (线段树)
题目大意:线段树的区间更改与查询,但是涉及到两种区间修改方式,一是给区间中的数全部加上一个数,二是将一个区间全部置为同一个数,然后询问整个区间和. 题目分析:处理好set操作和add操作的先后顺序就O ...
- [luogu P2170] 选学霸(并查集+dp)
题目传送门:https://www.luogu.org/problem/show?pid=2170 题目描述 老师想从N名学生中选M人当学霸,但有K对人实力相当,如果实力相当的人中,一部分被选上,另一 ...
- VC++多线程编程
一.问题的提出 编写一个耗时的单线程程序: 新建一个基于对话框的应用程序SingleThread,在主对话框IDD_SINGLETHREAD_DIALOG添加一个按钮,ID为IDC_SLEEP_SIX ...
- 归档-ios
/****普通对象归档**/ NSString *homePath=NSHomeDirectory(); NSString *fileName=@"test.vse"; NSStr ...
- QQ登入(4)QQ分享-内容转载
///////////////////QQ分享///////////// public void myclick3(View v){ //shareType : SHARE_TO_QQ_TYPE_IM ...
- linq字符串搜索条件,排序条件-linq动态查询语句 Dynamic LINQ
在做搜索和排序的时候,往往是前台传过来的字符串做条件,参数的数量还不定,这就需要用拼sql语句一样拼linq语句.而linq语句又是强类型的,不能用字符串拼出来. 现在好了,有个开源的linq扩展方法 ...