# 原则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:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数