现在的位置: 首页 > 综合 > 正文

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

2013年03月04日 ⁄ 综合 ⁄ 共 4015字 ⁄ 字号 评论关闭

     #if/#endif条件编译常用来由同一份源代码生成不同的结果文件,最常见的有debug版和release版。但是,这些工具在具体应用中并不是非常得心应手,因为它们太容易被滥用,使用他们创建的代码通常都比较难理解,且难以调试。C#语言的设计者们对这种问题的解决方案是创建更好的工具,以达到为不同环境创建不同机器码的目的。C#为此添加了一个Conditional特性,该特性可以标示出某种环境设置下某个方法是否应该被调用。使用这种方式来描述条件编译要比#if/#endif更加清晰。由于编译器理解Conditional特性,所以它可以在Conditional特性被应用时对代码做更好的验证。Conditional特性应用在方法这一层次上,因此它要求我们将条件代码以方法为单位来表达。当需要创建条件代码块时,我们应该使用Conditional特性来代替传统的#if/#endif。
    大多数程序老手都使用过条件编译来检查对象的前置条件和后置条件。例如,编写一个私有方法来检查所有类与对象的不变式(invariant:指为了确保类或者对象的本质不会被破坏,而采用一套对其自身状态进行校验的条件。),然后将这样的方法进行条件编译,从而让其只出现在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特性的工作原来,以及适用场合。
    

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");
}

    我们也可以将其作为前置条件和后置条件,在所有的公有方法和受保护方法中调用它。
    

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()方法生成到程序集中产生的影响是非常微不足道的。这种策略耗费很小的性能,换来的却是灵活性。
    我们创建的方法也可以依赖于多个环境变量,当我们应该多个Conditional特性时,它们之间的组合关系将为“或”的关系。
    Conditional特性只可以应用在整个方法上,另外需要注意的是,任何一个使用Conditional特性的方法只能返回void类型。
    我们不能在一个方法内的代码块上应用Conditional特性,也不可以在有返回值的方法上应用Conditional特性。为了应用Conditional特性,我们需要将具有条件性的行为单独放到一个方法中。虽然我们仍然注意那些Conditional方法可能给对象状态带来负面效应,但是Conditional特性的隔离策略总归比#if/#endif好的多。
    综上所述,使用Conditional特性比使用#if/#endif产生的IL代码更有效率。同时,将其限制在函数层次上可以清晰的将条件性的代码分离出来,从而使我们的代码具有更好的结构。

抱歉!评论已关闭.