条款三 : 操作符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操作符绝大多数情况下都能满足我们的要求,只有当被测试的对象是正确的类型时,它们才会成功。一般情况下不要使用强制转型,因为它可能会带来意想不到的负面效应,而且成功或失败往往在我们的预料之外。
原文地址:https://www.cnblogs.com/dm521/p/1187158.html