Effective C#: Item 4: Use Conditional Attributes Instead of #if

 

Item 4: Use Conditional Attributes Instead of #if

我们经常用#if/#endif来对同一源码生成的不同版本的build(debugrelease版本), 但这并不是很好的方式. 因为 #if/#endif很容易被滥用,使得源码非常的难读和难调试.有人已经注意到了这个问题,并且开发了一些工具来针对不同的环境生成不同的machine code. C#中就加入了这样的机制: Conditional Attribute.

Conditional Attribute根据环境设置来判断是否调用一个方法,这是一种更加清晰的格式(相比于#if/#endif).使用Conditional Attribute的编译器更加的高效.需要指出的是,Conditional Attribute应用于method level, 所以你必须把需要用到条件编译的代码放到不同的method中去.

一些恐龙级的程序员使用条件编译来检查一个objectpre-, post- condition. 你自己也想写一个private的方法来检查classobject的状态. 当然,这个方法应该只有当你生成debug版本的build时才出现. 下面是使用#if/#endif时的代码:

            private void CheckState( )<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

            {

                  // 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

            }

 

 

 

这就带来了一个问题,分析代码我们可以看到,如果你生成一个release版本的build,那么你的CheckState()方法是一个空方法. 如果CheckState()被调用的话,在你的release版本中,你就多了一个method调用(虽然这个method什么也不做),当然这也包括多装载一次和JIT一次这个空方法的代价.

这种方式(#if/#endif)理论上是可行的,但会带来一些很微妙的bug,这种bug只会出现在release版本的build.下面的这段代码显示了当你在条件编译中使用参数时会产生的bug.

            public void Func( )

            {

                  string msg = null;

 

#if DEBUG

                  msg = GetDiagnostics( );

#endif

                  Console.WriteLine( msg );

            }

debug版本的build,一切正常.但是在release版本的build,程序只会打印一个空白的message.这明显不时你所愿意的,但编译器不能帮你什么,因为你把程序business logic的关键部分放在了条件编译里面.如果你的程序里到处是这种用#if/#endif包围的代码的话,你会发现程序在debugrelease版本的build中有很不一样的表现,而且诊断这些不同的表现是一件非常困难的事.

C#使用更好的方式来解决条件编译,那就是Conditional Attribute. 使用Conditional Attribute可以使你把一些function隔离出来,这些function只有当一个环境变量被定义或者设置成一个特定指的时候才会成为你class的一部分.Conditional Attribute最常见的用途就是把debug的代码放在单独的function. .Net提供了一些用于调试的工具类,现在这段代码就演示了Conditional Attribute如何工作,以及怎样使用Conditional Attribute来调用这些调试工具.

例如你生成了一个Personobject,你加入了一个method来检查它的状态.

            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类用Reflection来得到calling method,虽然比较慢,但是可以极大的简化生成程序流程的工作.在上面的代码里,StackTrace来得到调用CheckState()的方法的名字.剩下的methods都是System.Diagnostics.Debug或者System.Diagnostics.Trace类的成员.Debug.Assert测试一个条件,如果测试失败,那么终止程序并打印出失败的信息.trace.WriteLinedebug console中打印调试的信息. CheckState()就是用来检查Person object的状态,如果状态无效,那么终止程序并打印错误信息.你可以在你的public方法或者Property里调用这个方法来作为pre-, post-condition. 例如:

            public string LastName

            {

                  get

                  {

                        CheckState( );

                        return _lastName;

                  }

                  set

                  {

                        CheckState( );

                        _lastName = value;

                        CheckState( );

                  }

            }

在第一set LastName,CheckStateassert,因为_lastNamenull. Set以后再检查一次,确认已经正确的赋值.

但这种检查会带来很多overhead,这在正式产品中也许是不能接受的.你只想在Debug版本的build中调用这些检查机制.Conditional Attribute就有了用武之地.

            [ Conditional( "DEBUG" ) ]

            private void CheckState( )

            {

                  // same code as above

            }

 

Conditional Attribute保证了只有当编译器检测到了Debug环境变量时才会调用CheckState(). Conditional Attribute并不影响CheckState()本身,而是影响调用CheckState()methods. 在本例中,如果定义了DEBUG,你会得到如下代码:

            public string LastName

            {

                  get

                  {

                        CheckState( );

                        return _lastName;

                  }

                  set

                  {

                        CheckState( );

                        _lastName = value;

                        CheckState( );

                  }

            }

如果没有定义DEBUG,代码如下:

            public string LastName

            {

                  get

                  {

                        return _lastName;

                  }

                  set

                  {

                        _lastName = value;

                  }

            }

CheckState()本身没有任何变化,不管环境变量是什么.从此例中,你可以看到.Net编译和JIT的区别.不管DEBUG是否定义,CheckState()都会被编译并送到Assembly.这看起来好象不是很高效,但这占用的只是一小部分硬盘空间和一些编译时间.如果不调用它的话,它永远不会被装载进内存和JITed.好象它根本就不存在于Assembly之内.这样做有一些性能方面的代价,但却大大的提高了灵活性.你可以自己研究以下.Netdebug.在任何安装.Net framework的机器上你都会在System.dll里发现所有的Debug类的所有方法.环境变量来决定当调用者编译时它们会不会被调用.

你也可以创建依赖于多个环境变量的方法.当有多个Conditional Attribute,这些Attributes之间的关系是OR.如下例,当环境变量是DEBUG或者TRACE,CheckState()会被调用.

            [ Conditional( "DEBUG" ),

            Conditional( "TRACE" ) ]

            private void CheckState( )

如果你想实现AND关系,那就必须在代码中使用preprocessor来定义符号.

#if ( VAR1 && VAR2 )

#define BOTH

#endif

这看起来有些别扭,但你必须这样做.虽然我们又一次使用了#if,但这次没有任何可以执行的代码在#if模块中,你只是定义新的符号.

Conditional Attribute只能应用于整个方法.而且这个方法的返回类型必须是void,你不能对一个method内部的代码块使用Conditional Attribute,也不能把Conditional Attribute应用在返回类型非void的方法上.你必须非常谨慎的设计Conditional 判断逻辑,并把它们隔离出来,放到单独的方法中.这个方法可能会对一个object状态产生微妙的影响,你必须仔细的检查,以避免可能带来的副作用.Conditional Attribute不是完美的,但它比#if/#endif模块要强很多,因为至少你不会错误的删除一些重要的方法调用或者变量的赋值.

本文的例子中使用了DEBUGTRACE符号.你可以扩展到任何你自己定义的符号. 你可以用多种方式来定义这些符号,比如编译器的(比如: /define:DEBUG),操作系统的环境变量设置(比如:  SET DEBUG=1),甚至代码中的pragmas(比如: #define DEBUG, 或者#undef DEBUG).

Conditional Attribute相比于#if/#endif可以生成更加高效的IL.它有只作用于method上的优点,使得你必须更加小心的设计代码.编译器也可以使用Conditional Attribute来帮我们避免很多#if/#endif带来的问题.Conditional Attribute相比于#if/#endif提供了更好的帮助,善用它可以让你的代码更加的健壮.

 

 

 

本系列文章只是作者读书笔记,版权完全属于原作者 (Bill Wagner),任何人及组织不得以任何理由以商业用途使用本文,任何对本文的引用和转载必须通知作者:zphillm@hotmail.com

 

转载于:https://www.cnblogs.com/ZphillM/archive/2005/08/06/208714.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值