zoukankan      html  css  js  c++  java
  • 条款三 : 操作符is或as优于强制转型

        C#是一门强类型语言。一般情况下,我们最后避免将一个类型强制转换为其他类型。但是,有时候运行时类型检查是无法避免的。相信大家都写过很多以System.Object类型为参数的函数,因为.NET框架预先为我们定义了这些函数的签名。在这些函数的内部,我们经常要把那些参数向下转型为其他类型,或者是类,或者是接口。对于这种转型,我们通常有两种选择:使用as操作符,或者使用传统C风格的强制转型。另外还有一种比较保险的做法:先使用is来做一个转换测试,然后再使用as操作符或者强制转型。
        正确的选择应该是尽可能的使用as操作符,因为它比强制类型要安全,而且在运行时层面也有比较好的效率。需要注意的是,as和is操作符都不执行任何用户自定义的转换。只用当运行时类型与目标转换类型匹配时,它们才会转换成功。它们永远不会在转换过程中构造新的对象。
    我们来看一个例子。假如需要将一个任意的对象转换为一个MyType的实例。我们可能会像下面这样来做:
         
         object o = Factory.GetObject();
         
    //第一个版本:
         MyType t = o as MyType;
         
    if(t != null)
         
    {
        
    //处理t,t现在的的类型为MyType
         }

         
    else
         
    {
            
    //报告转型失败
         }

     
        或者,也可以像下面这样来做:
        
    object o = Factory.GetObject();
    //第二个版本:
    try
    {
       MyType t;
       t 
    = (MyType) o;
       
    if(t != null)
       
    {
          
    //处理t,t现在的的类型为MyType
       }

       
    else
       
    {
         
    //报告空引用
       }

    }

    catch ()
    {
        
    //报告转型失败
    }


      
        相信大家都同意第一版本的转型代码更简单。其中没有添加额外的try/catch语句,因此也就避免了其带来的负担。注意,第二个版本中除了要捕捉异常外,还要对null的情况进行检查,因为如果o本来就是null,那么强制转型可以将它转换成任何引用类型。但如果是as操作符,且被转换对象为null,那么执行结果将返回null。因此,如果使用强制转型,我们既要检查其是否为null,还要捕捉异常。如果使用as操作符,我们只需要检查返回的引用是否为null就可以了。
        cast和as操作符之间最大的区别就在于如何处理用户自定义的转换。操作符as和is都只检查被转换对象的运行时类型,并不执行其它的操作。如果被转换对象的运行时类型既不是所转换的目标类型,也不是其派生类型,那么转型将告失败。但是,强制转型则会使用转换操作符来执行转型操作,这包括任何内建的数值转换。例如,将一个long类型强制转换为一个short类型就会导致部分信息丢失。
        在我们使用用户自定义的转换时,也会有同样的问题,来看下面的代码:
        
    public class SecondType
    {
       
    private MyType _value;
            
       
    //忽略其他细节
       
    //转换操作符。
       
    //将SecondType转换为MyType,参见条款29。
       public static implicit operator MyType(SecondType t)
       
    {
          
    return t._value;
       }

    }


        假设下面第一行代码中的Factory.GetObject()返回的是一个SecondType对象:
        
    object o = Factory.GetObject();
    //o为一个SecondType:
    MyType t = o as MyType;     //转型失败,o的类型不是MyType。
    if(t != null)
    {
       
    //处理t,t现在的类型为MyType
    }

    else
    {
       
    //报告空引用失败
    }

    //第二个版本
    try
    {
       MyType t1;
       t1 
    = (MyType)o;         //转型失败,o的类型不是MyType。
       if(t1 != null)
       
    {
         
    //处理t1,t1现在的类型为MyType
       }

       
    else
       
    {
          
    //报告空引用失败
        }

    }

    catch (System.Exception e)
    {
      
    //报告转型失败
    }


        两个版本的转型操作都失败了。大家应该还记得我前面说过强制转型会执行用户自定义的转换,有读者据此认为强制转型的那个版本会成功。这么想本身没有错误,只是编译器在产生代码时依据的是对象o的编译时类型。编译器对于o的运行时类型一无所知---编译器只知道o的类型是System.Object。因此编译器只会检查是否存在将System.Object转换为用户自定义转换。它会到System.Object类型和MyType类型的定义中去做这样的检查。由于没有找到任何用户自定义转换,编译器将产生代码来检查o的运行时类型,并将其和MyType进行比对。由于o的运行时类型为SecondType,因此转型将告失败。编译器不会检查在o的运行时类型SecondType和MyType之间是否存在用户自定义的转换。
        当然,如果将上述代码做如下修改,转换就会成功执行:
        
         object o = Factory.GetObject();
         
    //第三个版本 
         SecondType st = o as SecondType;
         
    try
         
    {
             MyType t;
             t 
    = (MyType)st;
             
    if( t != null)
             
    {
                
    //处理t,t现在的类型为MyType
             }

             
    else
             
    {
            
    //报告空引用失败
             }

         }

         
    catch (System.Exception e)
         
    {
            
    //报告转型失败
         }


        在正式的开发中,我们绝不能写如此丑陋的代码,但它却向我们揭示了问题的所在。虽然大家永远都不可能那样写代码,但可以使用一个以System.Object类型为参数的函数,让该函数在内部执行正确的转换。
        
    object o = Factory.GetObject();
            
    DoStuffWithObject(o);
    private void DoStuffWithObject(object o2)

       
    try
       
    {
          MyType t;
          t 
    = (MyType)o2;
          
    if(t != null)
          
    {
             
    //处理t,t现在的类型为MyType
          }

          
    else
          
    {
             
    //报告空引用失败
           }

       }

       
    catch
       
    {
          
    //报告转型失败
        }

    }


        记住,用户自定义的转换操作符只作用于对象的编译时类型,而非运行时类型上。至于o2的运行时类型和MyType之间是否存在转换,并不重要。事实上,编译器对此并不了解,也不关心。对于下面的语句,如果st的声明类型不同,会有不同的行为:
     t = (MyType) st;
        但对于下面的语句,不管st的声明类型是什么,都有同样的结果,因此,我们说as操作符要优于强制转型---它的转型结果相对比较一致。
        但如果as操作符两边的类型没有继承关系,即使存在用户自定义转换操作符,也会产生编译时错误,例如,下面的语句:
     t = st as MyType;
        我们已经知道在转型的时候应该尽可能的使用as操作符。下面我们来谈谈一些不能使用as操作符的情况。首先,as操作符不能用于值类型。例如,下面的代码编译时就会报错:
        
    object o = Factory.GetValue();
    int i = o as int;  //不能通过编译

        这是因为int是一个值类型,所以不可以为null。如果o不是一个整数,那么这个i里面还能存放什么呢?存入的任何值都必须是有效的整数,所以as不能和值类型一起使用。那就只能使用强制转型了:
        
    object o = Factory.GetValue();
    int i = 0;
    try
    {
       i 
    = (int)o;
    }

    catch (System.Exception e)
    {
       i 
    = 0;
    }


        但是,我们也并非只能这样。我们还可以使用is语句来避免其中对异常的检查或强制转型:
        
    object o = Factory.GetValue();
    int i = 0;
    if(o is int)
    {
       i 
    = (int)o;
    }

      
        如果o是某个其它可以转换为int的类型,例如double,那么is操作符将返回false。如果o的值为null,is操作符也将返回false。
        只有当我们不能使用as操作符来进行类型转换时,才应该使用is操作符。否则,使用is将会代来代码的冗余:
        
    //正确,但是冗余
    object o = Factory.GetObject();
    MyType t 
    = null;
    if(o is MyType)
       t 
    = o as MyType;
        
        这种做法显然既不高效,也显得冗余。如果我们打算使用as来做转型,那么再使用is检查就没有必要了。直接将as操作符的运输结果和null进行对比就可以了,这样比较简单。
        既然我们已经明白了is操作符、as操作符和强制转型之间的差别,那么大家猜猜看foreach循环语句中使用的是哪个操作符来执行类型转换呢?
        
    private void UseCollection(IEnumerable theCollection)
    {
       
    foreach (MyType t in theCollection)
          t.DoStuff();
    }


        答案是强制转型。事实上,下面的代码和上面foreach语句编译后的结果是一样的:
        
    public void UseCollection(IEnumerable theCollection)
    {
        IEnumerable it 
    = theCollection.GetEnumerator();
        
    while(it.MoveNext())
            MyType t 
    = (MyType)it.Current;
            t.DoStuff();
    }

            
        之所以使用强制转型,是因为foreach语句需要同时支持值类型和引用类型。无论转换的目标类型是什么,foreach语句都可以展现相同的行为。但是,由于使用的是强制转型,foreach语句可能产生BadCastException异常。
        由于IEnumerator.Current返回的是System.Object,而Object中又没有定义任何的转换操作符,因此转换操作符就不必多虑了。如果集合中是一组SecondType对象,那么运用在UseCollection()函数中将会出现转型失败,因为foreach语句使用的是强制转型,而强制转型并不关心集合元素的运行时类型。它只检查在System.Object类和循环变量的声明类型MyType之间是否存在转换。
        最后,有时候我们可能想知道一个对象的确切类型,而并不关心它是否可以转换为另一种类型。如果一个类型继承自另一个类型,那么is操作符将返回true。使用System.Object的GetType()方法,可以得到一个对象的运行时类型。利用该方法可以对类型进行比is或as更为严格的测试,因为我们可以拿它所返回的对象的类型和一个具体的类型做对比。
        再来看下面的函数:
        
    public void UseCollection(IEnumerable theCollection)
    {
       
    foreach (MyType t in theCollection)
           t.DoStuff();
    }


        如果创建了一个继承自MyType的类NewType,那么便可以将一组NewType对象集合应用在UseCollection函数中。
        如果我们打算编写一个函数来处理所有与MyType类型兼容的实例对象,那么UseCollection函数所展示的做法就挺好。但如果打算编写的函数只处理运行时类型为MyType的对象,那就应该使用GetType()方法来对类型做精确的测试。我们可以将这种测试放在foreach循环中。运行时类型测试最常用的地方就是相等判读(参加条款9)。对于绝大多数其它的情况,as和is操作符提供的.isinst(是as 和 is操作符编译为IL代码后,执行类型比较的关键指令)比较在语义上都是正确的。
        好的面向对象实践一般都告诫我们要避免转型,但有时候我们别无选择。不能避免转型时,我们应该尽可能的使用C#语言提供的as和is操作符来更清晰的表达意图。不同的转型方式有不同的规则,is和as操作符绝大多数情况下都能满足我们的要求,只有当被测试的对象是正确的类型时,它们才会成功。一般情况下不要使用强制转型,因为它可能会带来意想不到的负面效应,而且成功或失败往往在我们的预料之外。
  • 相关阅读:
    Java关键字:transient,strictfp和volatile简介
    freemarker 数字格式化函数
    使用 BeanCopier 复制对象
    扩展Smack Message
    JavaScript 中2个等号与3个等号的区别
    Eclipse 3.5使用dropins的插件安装方式
    常见的HTTP 状态代码
    使用python操作FTP上传和下载
    Python操作redis
    Ubuntu14.04安装redis和简单配置
  • 原文地址:https://www.cnblogs.com/dm521/p/1187158.html
Copyright © 2011-2022 走看看