原则四:需要条件编译时,使用conditional特性代替#if
#if和#endif代码块用来从一份相同的源代码产生成不同的编译版本,最常见的就是debug版和release版。但是,这种工具并不是我们所喜欢用的(因为用起来并不太友好),而且它容易被滥用。这样写出来的代码不易于理解,而且也难以调试。语言的设计者有责任提供一种更好的能够针对不同环境生成不同机器代码的工具。C#加入了conditional特性,用来根据环境设置决定一个方法是否应该被调用。比起#if它是一个更清晰的条件编译的方法。编译器理解conditional特性,所以当conditional特性被应用的时候,它能够更好的区分代码。Conditional特性是在方法层级上被使用的,所以,它强迫你将需要用conditional的代码独立出来做一个方法。当你需要使用条件编译的时候,使用conditional特性替代#if和#endif代码吧。
大部分有经验的程序员都使用过条件编译来检查对象的置和后置条件。你应该写一个私有的方法来检查所有的类和对象的变量。这个方法会被条件编译,所以它只会在你的debug版本中出现。
private void CheckStateBad()
        {
            //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");
            
            #endif
        }
使用#if和#endif编程,会在你的release版本中将遗留下一个空的方法。这个CheckState()方法在所有的版本中都会被调用,不管是debug版还是release版。它在release版中什么事情都不做,但是仍然要为函数调用增加额外地开销。你在加载和JIT编译这个方法的时候也要付出一点点代价。
通常情况下这没什么问题,但有时候会产生一些只在release版本中出现的问题。下面这段代码显示了你在使用条件编译时可能产生的问题。
private void Func()
        {
            string msg = null;
            #if DEBUG
            msg = GetDiagnostics();            
            #endif
            Console.WriteLine(msg);
        }
以上代码,在debug版本中没什么问题。但是在release版本中,它将输出空信息。这不是我们想要得到的结果。你自己分内的事情没有做好,编译器也帮不上什么忙。你将属于程序主逻辑的代码和条件编译的代码混在一起了。到处使用#if和#endif代码块,会让你难以意料程序在不同版本将产生出怎样不同的行为。
对此,C#里面有个很好的替代品:conditional特性。使用conditional特性,你可以将一个函数独立出来,并且只有在定义了特定的环境变量或此环境变量设定为指定的值得时候这个函数才会出现在你的类里面。特性的最常见的用法是将一段代码变成调试状态。.NET FRAMEWORK库已经为此提供了一些基础支持。下面这段代码示例了怎样使用.NET FRAMEWOR库的dubugging功能,展示了怎样使用特性以及你应在何时将他们加入到你的代码中去。当你添加了一个person对象的时候,你加入了一个方法来验证对象的状态是否发生率改变。
private void CheckState()
        {
            //Grab the name of calling runtime
            string methodName = new StackTrace().GetFrame(1).GetMethod().Name;
            
            Trace.WriteLine("Enter CheckState for person:");
            Trace.Write("\Call 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,"Firs Name cannot be blank");
            
            Trace.WriteLine("Exiting CheckState for person");
        }

或许你对上述代码中的一些库函数还不够熟悉,这里先来简单地介绍一下。stackTrace类将使用反射来获取当前正在调用的方法的名称。其代价相当高,但却可以极大地简化我们的工作,例如生成有关程序流程的信息。在上面的代码中,使用stackTrace即可得到正在被调用的方法名称(CheckState)。不过有一点风险,即当调用方法已经被内联时。对于这种情况,我们可以使每个调用ChekState()方法都传入其方法名称(使用MethodBase.GetCurrentMethod())稍后将会介绍为何这里没有采用这种方法。
其余的方法均定义于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将抛出一个断言错误。这样调用者即可找到问题,并快速修复——这正是我们想要的功能。
但在每个公用的例程中都做这样额外地检查显得比较浪费时间,我们可能只希望其出现在调试脚本中。这是即可使用Conditional特性:
[Conditional(“DEBUG”)]
private  void  CheckState()
{
//same  code  as  above
}
应用了Conditional特性之后,C#编译器只有在检测到定义了DEBUG环境变量是才会对CheckState方法进行调用。Conditional特性不会影响对CheckState()方法的编译,它只会影响对该方法的调用。如果定义了DEBUG符号,你将会得到如下代码:
Public  string  LastName
{
get
{
CheckState();
return  lastName();
}
Set
{
CheckState();
lastName = value;
CheckState();
}
}如果没有定义,那么将得到这样的代码:
Public  string  LastName
{
get
{
return  lastName();
}
Set
{
lastName = value;
}
}
无论是否定义DEBUG符号,CheckState()方法的方法体都不变。这个例子其实也演示了C#编译器的编译过程和JIT编译过程之间的区别。无论是否定义DEBUG环境变量,CheckState()方法都将被编译至程序集中。这种做法看起来也是否不那么高效了,但是其占用的仅仅是一点磁盘空间而已。如果没有被调用,CheckState()方法并不会加载到内存。这种策略用很小的性能降低换来了更高的灵活性。如果感兴趣的话,可以参考.NET  Framework类库中Debug类来深入理解。在每个安装有.NET  Framework的机器上,System.dll程序集都将包含有Debug类中所有方法的代码。使用conditional特性支持你创建内嵌有调试功能的库,这些调试功能也可以在运行时启用或禁止。
这种方式创建的方法甚至可以依赖与多个环境变量。在应用多个Conditional特性时,他们之间的组合关系将称为“或”(OR)。例如,下面的CheckState方法被调用的条件为,要么定义了DEBUG,要么定义了TRACE环境变量:
[Contitional(“DEBUG”),Conditional(“TRACE”)]
private  void  CheckState() 而若想创建一个使用“与”(AND)关系的构造,则需要自己在源代码中定义预处理符号:
#if(VAR1  &&  VAR2)
#define BOTH
#endif
可以看到,若想创建一个依赖于多个环境变量的条件例程,我们不得不回到使用#if的老式方法中去。不过,所有的#if都只不过是创建新的符号而已,我们应该避免将可执行代码放在其中。
随后即可按照老式的做法编写CheckState方法:
private  void  CheckState()
{
//the  old  way
#if  BOTH
Trace.WriteLine(“Entering  CheckState  for  person”);
//Grab  the  Name  of  the  calling  routine:
string  methodName = new  StackTrace().GetFrame(1).GetMethod().Name;

Debug.Asser(lastName != null,methodName,”Last  name  cannot  be  null”);
Debug.Asser(lastName.Length>0,methodName,”Last  name  cannot  be  blank”);
Debug.Asser(firstName != null,methodName,”Last  name  cannot  be  null”);
Debug.Asser(firstName.Length>0,methodName,”Last  name  cannot  be  blank”);Trace.WriteLine(“Exiting  CheckState  for  person”);
#endif
}
Conditional特性只可以应用在整个方法上。另外需要注意的是,任何一个使用Conditional特性的方法都只能返回void类型。你不能在方法内的代码上应用Conditional特性,也不可以在有返回值的方法上使用Conditional特性。为了应用Conditional特性,我们需要将所有的条件性的行为独立放到一个方法中。虽然你仍然需要注意那些条件性可能给对象状态带来的副作用,但是Conditional特性的隔离策略总归是要比#if#endif好得多。使用#if和#endif代码,你会不小心错误地删除一些重要的方法调用或者赋值语句。
上面的例子使用了DEBUG和TRACE这样的预定义符号,但你也可以将其技术扩展wie使用自己定义的符号。Conditional特新可以被任何方式定义的符号控制。你可以在编译器命令行、操作系统的环境变量,或者源代码中给出这些符号。
或许你已经注意到,上面每个使用了Conditional特性的方法都返回void类型,且不接受任何参数。这个规则我们必须遵守,编译器将强制Conditional方法必须返回void类型,不过你仍可以让该方法接受任意数目的引用类型参数。但这是一个不好的做法,有可能会导致一些负面效果。比如,考虑下一段代码:
Queue<string>  names = new  Queue<string>();
names.Enqueue(“one”);
names.Enqueue(“two”);
names.Enqueue(“three”);
stirng  item = string.Empty;
SomeMethod(item = names.Dequeue());
Console.WriteLine(item);
这里的SomeMethod是一个Conditional方法:
[Contitional(“DEBUG”)]
pricate  static  void  SomeMethod(string  param)
{}
这样也就会出现一些难以觉察的bug。仅当定义了DEBUG符号是,才会调用SomeMethod()方法。若没用定义的话,那么就不会调用。自然,names.Dequeue()也不会被调用。因为程序不再需要方法的返回值,所以就不会调用该方法。考虑到这些,所有应用了Conditional特性的方法都不应该接受任何参数。因为用户可以调用某些会产生副作用的方法来得到Conditional方法的参数,若Conditional方法没有被调用的话,那么这些产生副作用的方法也不会被调用。
综上所述,使用Conditional特性生成的IL要比使用#if#endif更有效率。同时,将其限制在函数层面上可以更加清晰地将条件性的代码分离出来,以便更进一步包装代码的良好结构。此外,C#编译器也为此提供了良好的支持,从而避免了使用以前#if#endif时常犯的错误。与预处理指令相比,Conditional特性可以让我们更好地将调解性代码分离开来。小结:最近1个多月,刚入职新公司,接手了新项目。原来的项目负责人离职了,恰巧又赶上要发布新版本,项目又不太熟悉。于是乎一个多月来没日没夜的加班,都没有时间过来翻译,拖了好久。这两天终于得闲将第四个原则翻译出来了。已经看到中文版书出来了。不过我还是会继续坚持。纯粹属于锻炼英语了。其它原则欢迎访问我的博客获取