ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 原则4:使用条件特性(conditional attribute)代替 #if **By D.S.Qiu** 尊重他人的劳动,**支持原创,转载请注明 [出处](/blog/1979093) :[http://dsqiu.iteye.com](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个原则有点偏门,而且两点少了点,说服力不够。 欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德! 有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创! 如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。 转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/1979093](/blog/1979093) 更多精彩请关注D.S.Qiu的 博客 和微博(ID:静水逐风)