Admirer Of Nature

Finding Wonderland of .Net

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

 

Item 3: Prefer the is or as Operators to Casts

C#是强类型语言.我们要尽量避免类型转换.

有时我们必须要在runtime检查一个变量的类型.比如有时你要用到一些.Net framework提供的方法,这些方法需要用到System.Object类型的参数.你需要把这些object (方法的参数)向下cast成其他的类型(类或者interface),这时你有两个基本方式可以选择,一是使用as操作符,二是使用C语言风格的cast.两者也可以结合成一个更加保险的方法,就是先用is 操作符来测试类型的转换,然后再用cast或者as 操作符进行转换.

正确的选择应该是使用as操作符来进行类型转换. as操作符比碰运气型的cast更加的安全,而且在runtime更加的高效. asis操作符并不能进行所有的用户定义的类型转换, 只有当runtime类型和目标类型一致时转换操作才会成功.它们永远不会为了满足程序调用请求而创建一个新的object.

在下例中,你需要把一个object转换成一个MyType的实例,你可以这样实现:

                  object o = Factory.GetObject( );

 

                  // Version one:

                  MyType t = o as MyType;

 

                  if ( t != null )

                  {

                        // work with t, it's a MyType.

                  }

                  else

                  {

                        // report the failure.

                  }

也可以这样写:

                  object o = Factory.GetObject( );

 

                  // Version two:

                  try

                  {

                        MyType t;

                        t = ( MyType ) o;

                        if ( t != null )

                        {

                              // work with T, it's a MyType.

                        }

                        else

                        {

                              // Report a null reference failure.

                        }

                  }

                  catch

                  {

                        // report the conversion failure.

                  }

第一个方法明显更加的简单易读,而且也没有try/catchoverhead.代码更加的高效.我们注意到cast版本不仅要检查转换后的object是否为null,还要catch异常.as操作符版本却不用. 这是因为使用cast, null可以被转换成任何一种reference类型,但是当应用as操作符在一个null reference上时会返回null. 所以as操作符只需检查一下返回的reference是否为null, 而不用catch异常.

as操作符和cast操作符最大的不同是如何对待用户定义的转换. asis操作符只会检查被转换的objectruntime类型,而不做任何其他的工作. 如果这个object不是目标类型,或者不是目标类型的子类型, 操作失败并终止. cast操作符却不同,它会把object转换成目标类型, 这包括所有的numeric转换, 比如从long转换成short, object的一些信息就在这种转换中丢失了.

而且当cast用户自定义类型时也会有同样的问题.比如:

      public class SecondType

      {

            private MyType _value;

 

            // other details elided

 

            // Conversion operator.

            // This converts a SecondType to

            // a MyType, see item 29.

            public static implicit operator

                  MyType( SecondType t )

            {

                  return t._value;

            }

      }

假设我们用Factory.GetObject()生成了一个SecondTypeobject,并把它转换成MyType类型.

                  object o = Factory.GetObject( );

 

                  // o is a SecondType:

                  MyType t = o as MyType; // Fails. o is not MyType

 

                  if ( t != null )

                  {

                        // work with t, it's a MyType.

                  }

                  else

                  {

                        // report the failure.

                  }

 

                  // Version two:

                  try

                  {

                        MyType t1;

                        t = ( MyType ) o; // Fails. o is not MyType

                        if ( t1 != null )

                        {

                              // work with t1, it's a MyType.

                        }

                        else

                        {

                              // Report a null reference failure.

                        }

                  }

                  catch

                  {

                        // report the conversion failure.

                  }

两个版本都会失败.但是cast却执行了用户定义的转换.这种假象使你认为cast成功了.但实际上它是失败的,因为编译器会根据编译时object的类型来生成代码.编译器对运行时object的类型一无所知.它只是把o当作System.Object的一个实例.编译器没有发现从System.ObjectMyType可行的转换.它检查System.ObjectMyType的定义,因为缺少用户定义的转换信息,编译器生成代码来检查o的运行时的类型,然后检查它是不是MyType类型.因为oSecondType类型,所以转换失败.编译器并不检查o在运行时的类型是否可以转换成MyType类型.

如果你想让转换成功,可以这样写代码:

                  object o = Factory.GetObject( );

 

                  // Version three:

                  SecondType st = o as SecondType;

                  try

                  {

                        MyType t;

                        t = ( MyType ) st;

                        if ( t != null )

                        {

                              // work with T, it's a MyType.

                        }

                        else

                        {

                              // Report a null reference failure.

                        }

                  }

                  catch

                  {

                        // report the failure.

                  }

你永远也不应该写这样丑陋的代码,但这也是一个常见的问题.尽管你永远不应该写这样的代码,但你可以用System.Object来当作一个进行转换操作的function的参数,比如:

            object o = Factory.GetObject( );

 

            DoStuffWithObject( o );

 

            private void DoStuffWithObject( object o2 )

            {

                  try

                  {

                        MyType t;

                        t = ( MyType ) o2; // Fails. o is not MyType

                        if ( t != null )

                        {

                              // work with T, it's a MyType.

                        }

                        else

                        {

                              // Report a null reference failure.

                        }

                  }

                  catch

                  {

                        // report the conversion failure.

                  }

            }

用户自定义的转换只作用于编译时object的类型,而不时运行时的类型. 至于运行时是否存在o2MyType类型的转换, 编译器不知道也根本不关心. 但当st类型不同时,这个语句有着不同的表现:

t = ( MyType ) st;

上面的语句会调用用户自定义的转换,从而造成转换成功的假象. 但使用as操作符的语句却有着一致的表现.所以应该尽量的使用as操作符.如下面的语句:

t = st as MyType;
 

事实上,如果stMyType之间不存在继承关系的话,而是通过一个用户自定义的转换来进行类型转换,那么编译器会报告一个错误.

现在你知道了应该尽可能的使用as操作符.但也有一些情况不能使用它.as操作符不能作用于value type.下面的这个语句不会通过编译:

object o = Factory.GetValue( );

                  int i = o as int; // Does not compile.

因为int是值类型,永远不能为null.那么如果o不是整数类型的话, i里面应该存什么值呢? 所以你不能使用as操作符.你可以用下面这种变通的方式:

                  object o = Factory.GetValue( );

                  int i = 0;

                  try

                  {

                        i = ( int ) o;

                  }

                  catch

                  {

                        i = 0;

                  }

但你不必一定这样一来做,不要忘了is操作符,你可以在转换之前先判断o的类型:

                  object o = Factory.GetValue( );

                  int i = 0;

                  if ( o is int )

                        i = ( int ) o;

如果o不是整数类型,那么is操作符返回false. is操作符作用于null arguments上时,永远返回false;

但你应该只在你不能使用as操作符转换类型时使用is,否则就是重复的, 比如:

                  // correct, but redundant:

                  object o = Factory.GetObject( );

 

                  MyType t = null;

                  if ( o is MyType )

                        t = o as MyType;

上面的代码和下面的代码是等效的:

                  // correct, but redundant:

                  object o = Factory.GetObject( );

 

                  MyType t = null;

                  if ( ( o as MyType ) != null )

                        t = o as MyType;

可以看出,进行了两次转换,低效而且重复.如果你已经决定了要使用as操作符来转换类型,那么只需检查返回值是否为null就可以了.

现在你已经明白了as, iscast,那么foreach循环用的是什么操作符呢?

            public void UseCollection( IEnumerable theCollection )

            {

                  foreach ( MyType t in theCollection )

                        t.DoStuff( );

            }

foreach用的实际上是cast操作符.上面的代码可以重写成下面的代码:

            public void UseCollection( IEnumerable theCollection )

            {

                  IEnumerator it = theCollection.GetEnumerator( );

                  while ( it.MoveNext( ) )

                  {

                        MyType t = ( MyType ) it.Current;

                        t.DoStuff( );

                  }

            }

这是因为foreach要用cast来支持value typereference type. 如果使用as操作符的话,foreach语句仍表现相同的行为,但会抛出BadCastException,因为as不能作用于值类型上.

因为IEnumerator.Current返回一个System.Object类型的object,而这个object不具备转换操作,所以并不能用于这个测试. SecondType类型的collection也不能用于UseCollection()因为转换会失败. Foreach语句并不检查collectionobject的运行时类型是否支持这种转换,它只检查IEnumerator.Current所返回的System.Object类型是否支持到目标类型(本例中的MyType)之间的转换.

最后,有时你想知道一个object确切的类型,而不只是关心这个object是否可以转换成目标类型. 因为as操作符对于所有从目标类型继承而来的类型的转换都返回true. GetType()方法返回object运行时的类型,它比as或者is操作符提供的测试都要更严格. 它返回的是object的确切类型.

再看一下UseCollection():

            public void UseCollection( IEnumerable theCollection )

            {

                  foreach ( MyType t in theCollection )

                        t.DoStuff( );

            }

如果你创建一个叫NewType的类,这个类继承MyType,那么NewType objectscollection也可以在UseCollection()中很好的工作.

      public class NewType : MyType

      {

            // contents elided.

      }

如果你的意图是写出一个可以使用所有MyType类型(自身或继承而来)function,这没有什么.但如果你的意图是写一个只接受MyType类型自身的function的话,你就要用确切的类型来进行比较. 在本例中,你可以在foreach循环里做. 知道运行时确切的类型只有在做equality测试时是非常重要的.在大多数其他的情况下,asis操作符提供的isinst比较是语法上正确的.

好的OO经验告诉我们要尽量避免类型的转换,但有时类型转换是必需的.在这种情况下,尽量的使用asis操作符来表达你的意图. 不同的类型强制转换有不同的规则,asis却在绝大多数情况下都是正确的,而且它们只有在object是正确的类型时才转换成功. Cast操作符会带来一些副作用,而且转换的成功与失败往往出乎意料.

  

 

 

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

posted on 2005-08-06 03:55  Admirer Of Nature  阅读(944)  评论(0编辑  收藏  举报