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操作符绝大多数情况下都能满足我们的要求,只有当被测试的对象是正确的类型时,它们才会成功。一般情况下不要使用强制转型,因为它可能会带来意想不到的负面效应,而且成功或失败往往在我们的预料之外。
  • 相关阅读:
    封装成帧、帧定界、帧同步、透明传输(字符计数法、字符串的首尾填充法、零比特填充的首尾标志法、违规编码法)
    计算机网络之数据链路层的基本概念和功能概述
    物理层设备(中继器、集线器)
    计算机网络之传输介质(双绞线、同轴电缆、光纤、无线电缆、微波、激光、红外线)
    计算机网络之编码与调制
    0953. Verifying an Alien Dictionary (E)
    1704. Determine if String Halves Are Alike (E)
    1551. Minimum Operations to Make Array Equal (M)
    0775. Global and Local Inversions (M)
    0622. Design Circular Queue (M)
  • 原文地址:https://www.cnblogs.com/dm521/p/1187158.html
Copyright © 2011-2022 走看看