Wu.Country@侠缘

勤学似春起之苗,不见其增,日有所长; 辍学如磨刀之石,不见其损,日所有亏!

导航

Effective C# 原则4:用条件属性而不是#if预编译块(译)

Item 4: Use Conditional Attributes Instead of #if
原则4:用条件属性而不是#if

使用#if/#endif 块可以在同样源码上生成不同的编译(结果),大多数debug和release两个版本。但它们决不是我们喜欢用的工具。由于#if/#endif很容易被滥用,使得编写的代码难于理解且更难于调试。程序语言设计者有责任提供更好的工具,用于生成在不同运行环境下的机器代码。C#就提供了条件属性(Conditional attribute)来识别哪些方法可以根据环境设置来判断是否应该被调用。

(译注:属性在C#里有两个单词,一个是property另一个是attribute,它们有不是的意思,但译为中文时一般都是译为了属性。property是指一个对象的性质,也就是Item1里说的属性。而这里的attribute指的是.net为特殊的类,方法或者property附加的属性。可以在MSDN里查找attribute取得更多的帮助,总之要注意:attribute与property的意思是完全不一样的。)

这个方法比条件编译#if/#endif更加清晰明白。编译器可以识别Conditional属性,所以当条件属性被应用时,编译器可以很出色的完成工作。条件属性是在方法上使用的,所以这就使用你必须把不同条件下使用的代码要写到不同的方法里去。当你要为不同的条件生成不同的代码时,请使用条件属性而不是#if/#endif块。

很多编程老手都在他们的项目里用条件编译来检测先决条件(per-conditions)和后续条件(post-conditions)。

(译注:per-conditions,先决条件,是指必须满足的条件,才能完成某项工作,而post-conditions,后续条件,是指完成某项工作后一定会达到的条件。例如某个函数,把某个对象进行转化,它要求该对象不能为空,转化后,该对象一定为整形,那么:per-conditions就是该对象不能为空,而post-conditions就是该对象为整形。例子不好,但可以理解这两个概念。)

你可能会写一个私有方法来检测所有的类及持久对象。这个方法可能会是一个条件编译块,这样可以使它只在debug时有效。

private void CheckState( )
{
// The Old way:
#if DEBUG
  Trace.WriteLine( "Entering CheckState for Person" );

  // Grab the name of the calling routine:
  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编译选项(pragmas),你已经为你的发布版(release)编译出了一个空方法。这个CheckState()方法会在所有的版本(debug和release)中调用。而在release中它什么也不做,但它要被调用。因此你还是得为例行公事的调用它而付出小部份代价。

不管怎样,上面的实践是可以正确工作的,但会导致一个只会出现在release中的细小BUG。下面的就是一个常见的错误,它会告诉你用条件编译时会发生什么:
public void Func( )
{
  string msg = null;

#if DEBUG
  msg = GetDiagnostics( );
#endif
  Console.WriteLine( msg );
}

这一切在Debug模式下工作的很正常,但在release下却输出的为空行。release模式很乐意给你输出一个空行,然而这并不是你所期望的。傻眼了吧,但编译器帮不了你什么。你的条件编译块里的基础代码确实是这样逻辑。一些零散的#if/#endif块使你的代码在不同的编译条件下很难得诊断(diagnose)。

C#有更好的选择:这就是条件属性。用条件属性,你可以在指定的编译环境下废弃一个类的部份函数, 而这个环境可是某个变量是否被定义,或者是某个变量具有明确的值。这一功能最常见的用法就是使你的代码具有调试时可用的声明。.Net框架库已经为你提供了了基本泛型功能。这个例子告诉你如何使用.net框架库里的兼容性的调试功能,也告诉你条件属性是如何工作的以及你在何时应该添加它:
当你建立了一个Person的对象时,你添加了一个方法来验证对象的不变数据(invariants):

private void CheckState( )
{
  // Grab the name of the calling routine:
  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 类通过反射取得了调用方法的的名字。这样的代价是昂贵的,但它确实很好的简化了工作,例如生成程序流程的信息。这里,断定了CheckState所调用的方法的名字。被判定(determining)的方法是System.Diagnostics.Debug类的一部份,或者是System.Diagnostics.Trace类的一部份。Degbug.Assert方法用来测试条件是否满足,并在条件为false时会终止应用程序。剩下的参数定义了在断言失败后要打印的消息。Trace.WriteLine输出诊断消息到调试控制台。因此,这个方法会在Person对象不合法时输出消息到调试控制台,并终止应用程序。你可以把它做为一个先决条件或者后继条件,在所有的公共方法或者属性上调用这个方法。
public string LastName
{
  get
  {
    CheckState( );
    return _lastName;
  }
  set
  {
    CheckState( );
    _lastName = value;
    CheckState( );
  }
}

在某人试图给LastName赋空值或者null时,CheckState会在第一时间引发一个断言。然后你就可以修正你的属性设置器,来为LastName的参数做验证。这就是你想要的。

但这样的额外检测存在于每次的例行任务里。你希望只在调试版中才做额外的验证。这时候条件属性就应运而生了:
[ Conditional( "DEBUG" ) ]
private void CheckState( )
{
  // same code as above
}

Conditional属性会告诉C#编译器,这个方法只在编译环境变量DEBUG有定义时才被调用。同时,Conditional属性不会影响CheckState()函数生成的代码,只是修改对函数的调用。如果DEBGU标记被定义,你可以得到这:
public string LastName
{
  get
  {
    CheckState( );
    return _lastName;
  }
  set
  {
    CheckState( );
    _lastName = value;
    CheckState( );
  }
}
如果不是,你得到的就是这:
public string LastName
{
  get
  {
    return _lastName;
  }
  set
  {
    _lastName = value;
  }
}

不管环境变量的状态如何,CheckState()的函数体是一样的。这只是一个例子,它告诉你为什么要弄明白.Net里编译和JIT之间的区别。不管DEBUG环境变量是否被定义,CheckState()方法总会被编译且存在于程序集中。这或许看上去是低效的,但这只是占用一点硬盘空间,CheckState()函数不会被载入到内存,更不会被JITed(译注:这里的JITed是指真正的编译为机器代码),除非它被调用。它存在于程序集文件里并不是本质问题。这样的策略是增强(程序的)可伸缩性的,并且这样只是一点微不足道的性能开销。你可以通过查看.Net框架库中Debug类而得到更深入的理解。在任何一台安装了.Net框架库的机器上,System.dll程序集包含了Debug类的所有方法的代码。由环境变量在编译时来决定是否让由调用者来调用它们。

你同样可以写一个方法,让它依懒于不只一个环境变量。当你应用多个环境变量来控制条件属性时,他们时以or的形式并列的。例如,下面这个版本的CheckState会在DEBUG或者TRACE为真时被调用:
[ Conditional( "DEBUG" ),
  Conditional( "TRACE" ) ]
private void CheckState( )

如果要产生一个and的并列条件属性,你就要自己事先直接在代码里使用预处理命令定义一个标记:
#if ( VAR1 && VAR2 )
#define BOTH
#endif

是的,为了创建一个依懒于前面多个环境变量的条件例程(conditional routine),你不得不退到开始时使用的#if实践中了。#if为我们产生一个新的标记,但避免在编译选项内添加任何可运行的代码。

Conditional属性只能用在方法的实体上,另外,必须是一个返回类型为void的方法。你不能在方法内的某个代码块上使用Conditional,也不能在一个有返回值的方法上使用Conditional属性。取而代之的是,你要细心构建一个条件方法,并在那些方法上废弃条件属性行为。你仍然要回顾一下那些具有条件属性的方法,看它是否对对象的状态具有副作用。但Conditional属性在安置这些问题上比#if/#endif要好得多。在使用#if/#endif块时,你很可能错误的移除了一个重要的方法调用或者一些配置。

前面的例子合用预先定义的DEBUG或者TRACE标记,但你可以用这个技巧,扩展到任何你想要的符号上。Conditional属性可以由定义标记来灵活的控制。你可以在编译命令行上定义,也可以在系统环境变量里定义,或者从源代码的编译选择里定义。

使用Conditional属性可以比使用#if/#endif生成更高效的IL代码。在专门针对函数时,它更有优势,它会强制你在条件代码上使用更好的结构。编译器使用Conditional属性来帮助你避免因使用#if/#endif而产生的常见的错误。条件属性比起预处理,它为你区分条件代码提供了更好的支持。

=====================================
小结:翻译了几篇了,感觉书写的有点冗余,有些问题可以很简单的说明的。可能是我的理解不到位,总之,感觉就是一个问题说来说去。另外,这里例举的几个例子感觉也不是很好,特别是前一个Item里的强制转化,感觉很牵强。不管怎样,还是认真的把书读好,译好吧。
还是那句话,或者我翻译的不好,或者网上已经有更好的翻译了,或者中文版也出来了,但我还是会坚持翻译下去。但以后的翻译不会再放在博客园的首页了。担心自己的翻译不好,以后的翻译都带上原文。总之,希望对读者有帮助!

分开了一个子类,专门POST Effective C#的翻译。

http://www.cnblogs.com/WuCountry/category/85054.html


===============================

Item 4: Use Conditional Attributes Instead of #if
#if/#endif blocks have been used to produce different builds from the same source, most often debug and release variants. But these have never been a tool we were happy to use. #if/#endif blocks are too easily abused, creating code that is hard to understand and harder to debug. Language designers have responded by creating better tools to produce different machine code for different environments. C# has added the Conditional attribute to indicate whether a method should be called based on an environment setting. It's a cleaner way to describe conditional compilation than #if/#endif. The compiler understands the Conditional attribute, so it can do a better job of verifying code when conditional attributes are applied. The conditional attribute is applied at the method level, so it forces you to separate conditional code into distinct methods. Use the Conditional attribute instead of #if/#endif blocks when you create conditional code blocks.

Most veteran programmers have used conditional compilation to check pre- and post-conditions in an object. You would write a private method to check all the class and object invariants. That method would be conditionally compiled so that it appeared only in your debug builds.

private void CheckState( )
{
// The Old way:
#if DEBUG
  Trace.WriteLine( "Entering CheckState for Person" );

  // Grab the name of the calling routine:
  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
}

 

Using the #if and #endif pragmas, you've created an empty method in your release builds. The CheckState() method gets called in all builds, release and debug. It doesn't do anything in the release builds, but you pay for the method call. You also pay a small cost to load and JIT the empty routine.

This practice works fine, but can lead to subtle bugs that appear only in release builds. The following common mistake shows what can happen when you use pragmas for conditional compilation:

public void Func( )
{
  string msg = null;

#if DEBUG
  msg = GetDiagnostics( );
#endif
  Console.WriteLine( msg );
}

 

Everything works fine in your debug build, but the release builds print a blank line. Your release builds happily print a blank message. That's not your intent. You goofed, but the compiler couldn't help you. You have code that is fundamental to your logic inside a conditional block. Sprinkling your source code with #if/#endif blocks makes it hard to diagnose the differences in behavior with the different builds.

C# has a better alternative: the Conditional attribute. Using the Conditional attribute, you can isolate functions that should be part of your classes only when a particular environment variable is defined or set to a certain value. The most common use of this feature is to instrument your code with debugging statements. The .NET Framework library already has the basic functionality you need for this use. This example shows how to use the debugging capabilities in the .NET Framework Library, to show you how Conditional attributes work and when to add them to your code.

When you build the Person object, you add a method to verify the object invariants:

private void CheckState( )
{
  // Grab the name of the calling routine:
  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" );
}

 

You might not have encountered many library functions in this method, so let's go over them briefly. The StackTrace class gets the name of the calling method using Reflection (see Item 43). It's rather expensive, but it greatly simplifies tasks, such as generating information about program flow. Here, it determines the name of the method that called CheckState. The remaining methods are part of the System.Diagnostics.Debug class or the System.Diagnostics.Trace class. The Debug.Assert method tests a condition and stops the program if that condition is false. The remaining parameters define messages that will be printed if the condition is false. trace.WriteLine writes diagnostic messages to the debug console. So, this method writes messages and stops the program if a person object is invalid. You would call this method in all your public methods and properties as a precondition and a post-condition:

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

 

CheckState fires an assert the first time someone tries to set the last name to the empty string, or null. Then you fix your set accessor to check the parameter used for LastName. It's doing just what you want.

But this extra checking in each public routine takes time. You'll want to include this extra checking only when creating debug builds. That's where the Conditional attribute comes in:

[ Conditional( "DEBUG" ) ]
private void CheckState( )
{
  // same code as above
}

 

The Conditional attribute tells the C# compiler that this method should be called only when the compiler detects the DEBUG environment variable. The Conditional attribute does not affect the code generated for the CheckState() function; it modifies the calls to the function. If the DEBUG symbol is defined, you get this:

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

 

If not, you get this:

public string LastName
{
  get
  {
    return _lastName;
  }
  set
  {
    _lastName = value;
  }
}

 

The body of the CheckState() function is the same, regardless of the state of the environment variable. This is one example of why you need to understand the distinction made between the compilation and JIT steps in .NET. Whether the DEBUG environment variable is defined or not, the CheckState() method is compiled and delivered with the assembly. That might seem inefficient, but the only cost is disk space. The CheckState() function does not get loaded into memory and JITed unless it is called. Its presence in the assembly file is immaterial. This strategy increases flexibility and does so with minimal performance costs. You can get a deeper understanding by looking at the Debug class in the .NET Framework. On any machine with the .NET Framework installed, the System.dll assembly does have all the code for all the methods in the Debug class. Environment variables control whether they get called when callers are compiled.

You can also create methods that depend on more than one environment variable. When you apply multiple conditional attributes, they are combined with OR. For example, this version of CheckState would be called when either DEBUG or trACE is TRue:

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

 

To create a construct using AND, you need to define the preprocessor symbol yourself using preprocessor directives in your source code:

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

 

Yes, to create a conditional routine that relies on the presence of more than one environment variable, you must fall back on your old practice of #if. All #if does is create a new symbol for you. But avoid putting any executable code inside that pragma.

The Conditional attribute can be applied only to entire methods. In addition, any method with a Conditional attribute must have a return type of void. You cannot use the Conditional attribute for blocks of code inside methods or with methods that return values. Instead, create carefully constructed conditional methods and isolate the conditional behavior to those functions. You still need to review those conditional methods for side effects to the object state, but the Conditional attribute localizes those points much better than #if/#endif. With #if and #endif blocks, you can mistakenly remove important method calls or assignments.

The previous examples use the predefined DEBUG or trACE symbols. But you can extend this technique for any symbols you define. The Conditional attribute can be controlled by symbols defined in a variety of ways. You can define symbols from the compiler command line, from environment variables in the operating system shell, or from pragmas in the source code.

The Conditional attribute generates more efficient IL than #if/#endif does. It also has the advantage of being applicable only at the function level, which forces you to better structure your conditional code. The compiler uses the Conditional attribute to help you avoid the common errors we've all made by placing the #if or #endif in the wrong spot. The Conditional attribute provides better support for you to cleanly separate conditional code than the preprocessor did.
 

posted on 2007-02-13 16:29  Wu.Country@侠缘  阅读(5941)  评论(6编辑  收藏  举报