原则4:使用条件特性(conditional attribute)代替 #if

By D.S.Qiu

尊重他人的劳动,支持原创,转载请注明 出处http://dsqiu.iteye.com

#if/#endif 块在相同的代码会编译成不同的版本,大多数会有调试(debug)和发布(release)两个版本。但我们会很不乐意去使用这个工具。 #if/#endif 块很容易被滥用,写的代码很难理解而且不容易调试。语言设计者应该负责设计能在不同环境产生不同的机器码的工具。 C# 提供了条件特性(conditional attribute),可以根据设置的环境决定函数的调用。使用条件特性会使用 #if/endif 更加清晰。编译器能解析条件特性,所以能很好的确定那段代码被调用。条件特性是应用在函数级别的,所以你应该分开不同的条件代码封装成不同的方法。当你要使用条件代码块的时候,使用条件特性代替 #if/endif 。

很多资深的程序员喜欢使用条件编译来检查对象的先决和后续条件。你会写一个 private 方法去检查所有类型和对象变量。这个方法使用了条件编译以至于只能在调试版本出现。

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");
    Trace.WriteLine("Exiting CheckState for Person"); 
#endif 
}

上面方法使用 #if 和 #endif 编译选项,你会发现在发布版本你实际创建了一个空的方法。 CheckState() 会被所有版本中调用,比如发布版本和调试版本。在发布版本没有做任何事情,但你为此付出了函数调用的代价。同时还要有很小花费在加载和 JIT 空程序。

这个实践是正确的,但在发行版本会导致一个微妙的错误。下面这个就是使用条件编译选项的常见错误:

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

在调试版本会正确工作,但是你的发行版本只会让你哭笑不得地输出空字符串。当然这不是你想要的。你出了错,编译器就帮不了你了。在条件编译器块中的代码就是你的逻辑。在 #if/endif 块中的代码很难让你诊断出不同版本的不同行为。

C# 有一个更好的选择:条件特性。使用条件特性,能分离出不同函数,只有在特定的环境变量的定义或某些值的设置才会属于你的类。这个功能最常见的好处就是在调试的时候能有可用的声明。 .NET 框架已经提供了基本的通用功能。这个例子告诉我们 .NET 框架的调试功能,已经条件特性是怎么工作的和什么时候添加到你的代码中。

当你创建了 Person 对象,你要写一个方法去检查对象的变量:

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 。如果被 inline 调用会有一个小的风险,另一种方法就是在调用 CheckState 函数的方法使用 MethodBase.GetCurrentMethod() 传入方法名。你很快会明白为什么不使用这个策略。

后面的方法是 System.Diagnositics.Debug 类或 System.Diagnostics.Trace 类的函数。 Debug.Assert 测试条件,且当条件为 false 是程序停止。后面的是条件为 false 是打印出来的信息。 Trace.WriteLine 将诊断信息输出到调试控制台上。所以这个方法实际当 person 对象不正确是输出信息并终止程序。你可以在所有 public 方法或属性中调用这个方法作为先决条件和后续条件:

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

如果将一个空字符串或 null 赋给 lastName , CheckState 触发一个断言(assert)。然后检验 lastName 的值。这就是你想要做的。

但这额外的检查会在每个例行任务中花费时间。你只是在调试版本中需要额外的检查。那就是为什么会有条件特性:

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

条件特性告诉 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 之间的区别。不管 DEBUG 环境环境变量是否定义, CheckState() 方法都会被编译嵌入在程序集中。这可能不是很搞笑,但这只是花费了硬盘的容量。 CheckState() 不会被载入内存和 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

的确,当你要创建一个依赖多于一个环境变量的条件行为,你不得不退回到之前 #if 的做法。所有 #if 创建一个符号。但要避免在编译选项中加入任何可运行代码。

所以,你可以按下面方式重写 CheckState 方法:

private void CheckStateBad() 
{
// 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.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 
}

条件特性只能运用于整个的方法。除此之外,任何条件特性方法必须是 void 返回值。你在代码块中使用条件特性,也不能创建有返回值的条件特性方法。而是,要分离出具体条件行为的代码单独写成条件特性方法。你应该回顾下那些条件方法对对象状态的副作用,条件特性会比 #if/endif 好很多。使用 #if/endif 块,你会错误的移除了很多重要的方法调用或赋值语句。

前面的例子使用预定义的 DEBUG 或 TRACE 符号。你也可以扩展任何你定义的符号。条件特性可以被多种方式定义的符号控制。你可以定义符号从编译命令行,或从操作系统 shell 的环境变量,或从代码的编译选项中定义。

你应该注意到前面的每个条件特性的方法都是 void 返回值而且没有参数。实际实践应该遵从这个原则。编译器是前置条件特性方法必须是 void 返回值。然后,你可以创建一个方法含有引用类型参数。这种做法会导致副作用,应该尽量避免。考虑下面一段代码:

Queue<string> names = new Queue<string>(); 
names.Enqueue("one"); 
names.Enqueue("two"); 
names.Enqueue("three");
string item = string.Empty; 
SomeMethod(item = names.Dequeue()); 
Console.WriteLine(item);

SomeMethod 添加了条件特性:

[Conditional("DEBUG")] 
private static void SomeMethod(string param) 
{
}

这里会有一个很微妙的错误。 SomeMethod() 只有在 DEBUG 符号被定义了才会被调用。而且 names.Dequeue() 也是一样的。因为结果不是必须的,所以方法没有调用。任何条件特性的方法不应该有任何参数。使用调用方法来产生参数会有副作用。如果条件不为 ture 这些方法不会被调用。

条件特性比 #if/#endif 产生了更高效的 IL 代码。还有一个好处就是只能使用在函数级别上,这迫使你要更好的组织你的条件代码。编译器使用条件特性帮助我们避免了使用 #if/#endif 的常见错误。条件特性比预处理更能让你你的条件代码分离的更清晰。

小结:

更新晚了,昨天晚上写了一半,现在弄完。昨天家里发生了点事情,心里一直不安,感觉挺无奈的。只有对自己说,我要努力,我要顶住。原则4,相对于前面3个原则有点偏门,而且两点少了点,说服力不够。

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!

有关本书的其他章节翻译请点击查看,转载请注明出处,尊重原创!

如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

转载请在文首注明出处:http://dsqiu.iteye.com/blog/1979093

更多精彩请关注D.S.Qiu的 博客 和微博(ID:静水逐风)

powered by Gitbook 该教程制作时间: 2016-01-31 09:38:29