转自: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特性来实现代码调试的更多相关文章

  1. Web开发者的六个代码调试平台

    代码调试平台是Web开发者进行开发.测试.分享.协作和交流的网络应用,它们支持实时的编辑.预览HTML.CSS和JavaScript的客户端代码.这些代码调试平台最值得称道的地方在于,它们中的大多数都 ...

  2. 关于代码调试de那些事

    原文出处:http://www.wklken.me/posts/2014/11/23/how-to-debug.html 关于代码调试de那些事 1.你得明白你在做什么, 保持清醒 2.想清楚了再写代 ...

  3. javascript代码 调试方法

    你的代码可能包含语法错误,逻辑错误,如果没有调试工具,这些错误比较难于发现. 通常,如果 JavaScript 出现错误,是不会有提示信息,这样你就无法找到代码错误的位置. 在程序代码中寻找错误叫做代 ...

  4. GDB代码调试与使用

    GDB代码调试与使用 Linux下GDB调试代码 源代码 编译生成执行文件 gcc -g test.c -o test 使用GDB调试 启动GDB:gdb test 从第一行列出源代码:list 直接 ...

  5. OI中的代码调试

    作为一位OIer,代码调试的能力必不可少. 今天梳理一下自己进行代码调试的方法,下面只是一些个人的总结. 代码的评价有三部分: 正确性 强健性 高效性 检查也应该从这三部分出发. [正确性] 打完代码 ...

  6. 初识 Javascript.02 -- Date日期、Math对象、数据类型转换、字符串、布尔Boolean、逻辑运算符、if else 、三元表达式、代码调试方法、

    Date()对象: Date对象用于处理日期和时间. 1.1 Math对象  ◆Math.ceil()   天花板函数    向上取整  只取整数,不足则进1 ◆Math.floor()  地板函数 ...

  7. .NET Core快速入门教程 5、使用VS Code进行C#代码调试的技巧

    一.前言 为什么要调试代码?通过调试可以让我们了解代码运行过程中的代码执行信息,比如变量的值等等.通常调试代码是为了方便我们发现代码中的bug.ken.io觉得熟练代码调试技巧是成为合格程序员的基本要 ...

  8. Node.js系列文章:如何进行代码调试

    使用任何一门编程语言,都少不了代码调试这一功能.我们在使用JavaScript编写浏览器端代码时,Chrome提供了强大的调试工具Dev Tools,但是在编写Node.js代码时,大多数人最开始都使 ...

  9. #7 Python代码调试

    前言 Python已经学了这么久了,你现在已经长大了,该学会自己调试代码了!相信大家在编写程序过程中会遇到大量的错误信息,我也不例外的啦-遇到这些问题该怎么解决呢?使用最多的方法就是使用print打印 ...

随机推荐

  1. keil uvision看厌了么?试试Sublime Text吧!

    之前用 Sublime Text(以下简称 ST )配置了 C/C++ 开发环境,感觉相当不错,作为编辑器的 ST,编辑代码的功能当然是相当棒的,美中不足的是目前只能编译单个文件,但是用来做些小练习也 ...

  2. MATLAB符号运算 分类: 图像处理 2015-07-31 22:53 3人阅读 评论(0) 收藏

    1.符号运算 使用MATLAB可以进行多项式乘除运算,也可以进行因式分解. 例1. 多项式乘除运算(x+3)3 >> syms x; >> expand((x+3)^3) an ...

  3. phpwind数据同步本地后登陆异常

    在讲数据同步到本地之后,发现输入用户名和密码之后点击登陆,依然会返回到之前的页面,并且显示的还是未登录的状态. 解决办法:在后台中将:站点设置--cookie作用域留空即可.

  4. CentOS 中PHP开启 GD功能

    yum install php-gd 然后重启服务器: service httpd restart

  5. ps互补色

    色彩中的互补色有红色与绿色互补,蓝色与橙色互补,紫色与黄色互补.在光学中指两种色光以适当的比例混合而能产生白光时,则这两种颜色就称为“互为补色”. 互补色是相对的混合的白色 互补色:在色环中某种颜色的 ...

  6. C#中的预处理指令

    C#中的预处理指令 作为预处理中的一对:#region name ,#endregion可能是大家使用得最多的,我也常用它来进行代码分块,在一个比较长的cs文件中,这么做确实是一件可以让你使代码更清晰 ...

  7. HtmlAgilityPack

    http://htmlagilitypack.codeplex.com/wikipage?title=Examples http://nuget.org/packages/HtmlAgilityPac ...

  8. linux ascii艺术与ansi艺术

    Linux终端下的ASCII艺术 http://zh.wikipedia.org/zh-tw/%E9%9B%BB%E5%AD%90%E9%81%8A%E6%88%B2%E5%8F%B2 电子游戏史 h ...

  9. 图片加载js类库

    Picturefill Picturefill.WP插件利用picturefill.js脚本展示Responsive图片,即根据视口宽度选择尺寸合适的图片加载,节省带宽,提高网站载入速度.例如用户用手 ...

  10. Underscore.js 函数节流简单测试

    函数节流在日常的DOM界面数据交互中有比较大的作用,可以减少服务器的请求,同时减少客户端的内存影响 Underscore.js  本省就包含了函数节流的处理函数 _.throttle 和 _.debo ...