Item 4: Use Conditional Attributes Instead of #if

      我们可以使用#if/#endif从同一源码中生成不同的程序版本,比如debug和release版本,不过它并没有那么好用。#if/#endif很容易造成混乱,代码也难以阅读和调试。开发语言的设计者们意识到这个问题,并且为不同的开发环境创建了更好的生成工具。在C#中就添加了Conditional属性,它可以更好的完成工作。conditional属性是在方法级别(method level)上被提供的,因此你必须将不同情况的代码分布到特定的方法中去。我们应当在创建不同版本程序块时使用conditional属性代替#if/#endif。

      有经验的程序员使用条件编译来检验对象的事前和事后状态。我们应当使用私有方法来检验这些类和对象。由于是条件编译,它们只会出现在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()方法被调用但是什么都不做,这也会降低一点点装载和执行的效率。

      上例中的程序可以正常运行,但是一些潜在的bug会发生在release版本中。下例中就是一个在使用条件编译时经常会犯的错误。

public void Func()
{
    
string msg = null;
#if DEBUG
    msg 
= GetDiagnostics();
#endif
    Console.WriteLine(msg);
}

      上例在debug时一切正常,但是在release时会显式空白行,而且那并不是你想要的,编译器也不能帮你,因为你将完成逻辑的部分编写在条件编译块内部了。将代码用#if/#endif任意分割会造成代码在不同的版本中有可能出现难以检测原因的不同行为。

      C#有更好的解决方法:使用Conditional属性。这样我们可以将那些要在特定的环境变量被定义或设定的时候才发挥作用的函数孤立出来。下例中展示了如何使用.net类库中的debug工具类、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类中,我们通过反射机制来获得调用方法的名称。这些方法的作用非常大,例如收集程序中所需的信息。这里通过它检验调用StackTrace的方法的名称。上例中其他的那些方法包含在System.Diagnostics.Debug类或System.Diagnostics.Trace类中。当不满足测试条件时,断言方法将中断程序,并返回失败信息。Trace.WriteLine将调试的信息显示到控制台上。因此,如果用来进行判断的对象无效,这个方法将中断程序并显示信息。我们可以在任何公共方法和属性中调用这个方法来进行事前和事后检测。

public string LastName
{
    
get
    
{
        CheckState();
        
return _lastName;
    }

    
set
    
{
        CheckState();
        _lastName 
= value;
        CheckState();
    }

}

      当lastName为null时,CheckState会中断程序。当我们为LastName重新设定值之后,程序就可以正常运行。

      但是这种额外的检验会降低执行的效率。我们只想在debug版本中包含这些额外的检验。这时我们就要用到Conditional属性了。

[Conditional("DEBUG")]
private void CheckState()
{
}

      Conditional属性告诉C#编译器这个方法只有在检测到DEBUG环境变量时才会被调用。它并不会影响到CheckState()函数中的代码,它修改了对函数的调用。如果DEBUG被定义了,我们就可以得到这样的代码:

public string LastName
{
    
get
    
{
        CheckState();
        
return _lastName;
    }

    
set
    
{
        CheckState();
        _lastName 
= value;
        CheckState();
    }

}

      否则会得到这样的代码:

public string LastName
{
    
get
    
{
        
return _lastName;
    }

    
set
    
{
        _lastName 
= value;
    }

}

      不论环境变量如何改变,CheckState()函数本身并没有变化。从这个例子中我们可以看到.Net中编译和JIT之间的区别。不管环境是否被定义,CheckState()都会被编译并传送到程序集中,这仅仅会消耗一点磁盘空间。CheckState()函数不会被装载到内存或JITed中除非它被调用,就好像它不存在于程序集中一样。这样我们通过一点小小的消耗获得了更大的灵活性。通过查看.Net Framework中的Debug类库,我们可以获得更深层次的理解。在所有安装了.Net Framework的机器上,System.dll中包含了所有Debug类中的方法代码。函数是否被调用由环境变量来控制。

      我们也可以创建基于多余环境变量的方法。当我们提供多个conditional属性时,它们之间是用or来进行连接的。例如在下例中,CheckState将在DEBUG或TRACE定义时被调用。

[Conditional("DEBUG"),Conditional("TRACE")]
private void CheckState()
{
}

      当我们想要使用and进行条件判断时,就必须在代码中提前添加判断

#if (VAR1 && VAR2)
#define BOTH
#endif

      如果想要创建基于多个环境变量之间and关系的方法,我们就要回头使用#if,但是我们并没有创建任何可执行程序在代码块中。

      Conditional属性只能应用在整个方法上。另外,应用Conditional属性的方法必须是void型。我们不能将Conditional添加到一个程序块或者有返回值的方法上。因此,我们需要谨慎的构建方法并将条件判断行为隔离出来。使用这种方法时我们必须要仔细检查是否会对程序产生负作用,但是同#if/#endif相比,Conditional属性要强很多,至少不会因为使用#if/#endif而错误删除重要的程序块。

      上面的所有例子中我们都使用的DEBUG和TRACE符号。我们可以自定义任何符号,Conditional属性支持多种符号定义方式,我们可以通过编译器命令行,操作系统环境变量或者代码中定义它们。

      Conditional属性会生成比#if/#endif更高效的中间语言。它只能应用在函数级别,这使得我们必须构建更好的代码结构。通过使用Conditional可以帮助我们避免在使用#if/#endif时会发生的错误。Conditional属性为我们隔离条件代码提供了更加清晰的方式。

      译自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著



P.S. 
      关于多个Conditional之间AND的关系 ,在MSDN中是这样写的:
[Conditional("A")] public static void IfAandB( ) 
{
   AandBPrivate( ); 
}

[Conditional(
"B")] static void AandBPrivate( ) 
{
   
/* Code to execute when both A and B are defined */
}


      条件方法不能为接口声明方法和接口实现方法。
      不能使用override来修饰条件方法,但是可以将其声明为virtual型。被override的virtual型条件方法的方法隐含有条件方法属性,不能对其使用Conditional属性。重写条件方法很容易造成混乱。如果条件方法用在委托创建表达式中时,也会发生错误。
class Class1 
{
   [Conditional(
"DEBUG")]
   
public virtual void M() {
      Console.WriteLine(
"Class1.M executed");
   }

}


class Class2: Class1
{
   
public override void M() {
      Console.WriteLine(
"Class2.M executed");
      
base.M();                  //由于DEBUG未定义,base.M()不被调用
   }

}


      回到目录
posted on 2006-09-07 10:59  aiya  阅读(1379)  评论(8编辑  收藏  举报