改善C#程序的50种方法 条款4:使用Conditional特性代替#if条件编译

转载 2007年09月21日 10:58:00

#if/#endif 条件编译常用来由同一份源代码生成不同的结果文件,最常见的有debug版和release版。但是,这些工具在具体应用中并不是非常得心应手,因为它们 太容易被滥用了,使用它们创建的代码通常都比较难理解,且难以调试。C#语言的设计者们对这种问题的解决方案是创建更好的工具,以达到为不同环境创建不同 机器码的目的。C#为此添加了一个Conditional特性,该特性可以标示出某种环境设置下某个方法是否应该被调用。使用这种方式来描述条件编译要比 #if/#endif更加清晰。由于编译器理解Conditional特性,所以它可以在Conditional特性被应用时对代码做更好的验证。 Conditional特性应用在方法这一层次上,因此它要求我们将条件代码以方法为单位来表达。当需要创建条件代码块时,我们应该使用 Conditional特性来代替传统的#if/#endif。

大多数程序老手都使用过条件编译来检查对象的前置条件和后置条件。例如,编写一个私有方法来检查所有类与对象的不变式(invariant)[8],然后将这样的方法进行条件编译,从而让其只出现在debug版本的程序中。

private void CheckState( )

{

// 老式的做法:

#if DEBUG

  Trace.WriteLine( "Entering CheckState for Person" );

  // 获取正在被调用函数的名称:

  string methodName =

    new StackTrace( ).GetFrame( 1 ).GetMethod( ).Name;

 

  Debug.Assert( _lastName != null,

    methodName,

    "Last Name cannot be null" );

  Debug.Assert( _lastName.Length > 0,

    methodName,

    "Last Name cannot be blank" );

  Debug.Assert( _firstName != null,

    methodName,

    "First Name cannot be null" );

  Debug.Assert( _firstName.Length > 0,

    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 methodName =

    new StackTrace( ).GetFrame( 1 ).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 > 0,

    methodName,

    "Last Name cannot be blank" );

  Debug.Assert( _firstName != null,

    methodName,

    "First Name cannot be null" );

  Debug.Assert( _firstName.Length > 0,

    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代码块,我们很有可能会错误地删除一些重要的方法调用或者赋值语句。

条款5:总是提供ToString()方法  34

 
上面的例子使用了DEBUG或者TRACE这样的预定义符号,但我们也可以将其扩展到我们自己定义的符号上。Conditional特性可以被任何方式定义的符号所控制,例如编译器命令行,操作系统shell的环境变量,或者源代码pragma。

综上所述,使用Conditional特性比使用 #if/#endif产生的IL代码更有效率。同时,将其限制在函数层次上可以清晰地将条件性的代码分离出来,从而使我们的代码具有更好的结构。另外, C#编译器也为此提供了很好的支持,从而帮助我们避免以前使用#if或#endif时常犯的错误。

 

使用Conditional特性代替#if条件编译

1原因 #if/#else条件编译常用来由同一份源代码生成不同的结果文件,最常见的有 debug版和release版。但是这些工具在具体的应用中并不是非常得心应手,因为它们它容易被滥用了,使用它们创...
  • zaiguo
  • zaiguo
  • 2011年11月14日 05:06
  • 472

条款4:使用Conditional特性代替#if条件编译

条款4:使用Conditional特性代替#if条件编译 #if/#endif条件编译常用来由同一份源代码生成不同的结果文件,最常见的有debug版和release版。但是,这些工具在具体应用中并不是...
  • sunbird69
  • sunbird69
  • 2007年04月01日 12:22
  • 608

条款4:使用Conditional特性代替#if条件编译

使用通常的条件编译,经常把属于程序主逻辑的代码和条件编译代码混在一起。容易引起意想不到的问题。使用Conditional特性把条件编译应用在法方法这一层上,要求我们将条件代码以方法为单位来表达,这样可...
  • cswch
  • cswch
  • 2007年05月28日 16:55
  • 285

c# Conditional用法详解

百度了一下关于conditional的文章,大都说的都是非常浅显,只是老生常谈了下尽量多使用conditional而少用#if,收获不大。 现在我自己测试了一下,也查阅了MSDN,有点心得,说出来与大...
  • fdyshlk
  • fdyshlk
  • 2017年08月26日 16:43
  • 674

c# 条件编译 Conditional ("DEBUG")

c# 条件编译 Conditional ("DEBUG") 简而言之:可以通过Conditional 指定函数和属性是否编译到最终产品中去。同时还应该看看 AttributeUsage Obso...
  • is2120
  • is2120
  • 2012年02月24日 17:45
  • 4477

C#特性(Attribute)之预定义特性(Conditional)

特性(Attribute)是用于在运行时传递程序中各种元数据(类、方法、结构、枚举、组件)的行为性声明标签。 特性用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。 .NET 框架提供了两...
  • ilipan
  • ilipan
  • 2015年05月07日 09:48
  • 1607

【C#】利用Conditional属性完成编译忽略

Conditional是.NET提供关于编译的属性描述,其作用是添加到方法或属上,通过定义编译符的方式告指示编译器应忽略方法调用或属性在我们的代码中经常会出现Console.WriteLint(“XX...
  • sinat_20559947
  • sinat_20559947
  • 2017年08月02日 00:43
  • 3755

C#条件编译选项:Conditional(代替#if...#endif)

#if (Debug && Trace)     #define DebugAndTrace #endif using System; using System.Collections.G...
  • yanchezuo
  • yanchezuo
  • 2014年06月21日 14:17
  • 1418

改善C#程序的50种方法卵

http://www.ichacha.net/%e5%a4%a7%e5%ba%86%e5%93%aa%e9%87%8c%e6%9c%89%e5%8d%96%e5%86%b0%e6%af%92%e7%9...
  • gong0810
  • gong0810
  • 2014年07月09日 13:47
  • 107

改善C#程序的50种方法 条款1:使用属性代替可访问的数据成员

为什么程序已经可以正常工作了,我们还要改变它们呢? 答案就是我们可以让它们变得更好。我们常常会改变所使用的工具或者语言,因为新的工具或者语言更富生产力。如果固守旧有的习惯,我们将得不到期望的结果。 对...
  • xuchengzhong
  • xuchengzhong
  • 2007年09月21日 10:36
  • 460
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:改善C#程序的50种方法 条款4:使用Conditional特性代替#if条件编译
举报原因:
原因补充:

(最多只允许输入30个字)