Effective C# Item 3:Prefer the is or as Operators to Casts

      C#是一种强类型语言。好的编程习惯意味着我们应该尽量避免进行强制的类型转换。但是有些时候这是难以避免的。转换的方式有两种:一是使用as,二是C语言风格的显式类型转换(cast)。更保险的办法是先使用is来判断转换是否成立再使用as或显式类型转换。

      正确的选择应当是使用as进行转换,因为它比强制转换更为安全和高效。as和is操作符不能对用户自定义的转换进行操作。它们只是简单的比较运行时的被转换类型同目标类型是否相匹配。它们永远不会为转换而新建object。

      在下面的代码中,我们要将一个object型实例转换为MyType型。

object o = Factory.GetObject();
//版本1
MyType t = o as MyType;
if(t != null)
{
    
//成功
}

else
{
    
//失败 抛出异常
}


//版本2
try
{
    MyType t 
= (MyType)o;
    
if(t != null)
    
{
        
//成功
    }

    
else
    
{
        
//失败  抛出空引用异常
    }

}

catch
{
    
//失败  抛出类型转换异常
}

      很明显第一种方式要更简单清晰,也避免了try/catch的消耗。要注意的是显式类型转换后还必须再一次检验,如果转换后的实例为null还要抛出空引用的异常。而对于as来说这种情况同转换失败一样均返回null值,只要简单的判断其是否为null就可以避免可能遇到的所有异常。

      二者最大的不同在于对待用户自定义转换的方式。is和as在运行时检验类型是否可以转换,它们不会做任何其他的操作。如果被转换的类型不是特定的类型或其子类则转换失败。而显式类型转换则会进行转换操作将其强制转换为要求类型。这经常会造成数据丢失,例如数据类型之间的转换。

      当使用自定义类型转换进行显式类型转换时也会出现问题。下例中定义了一个类及其自定义转换

public class SecondType
{
    
private MyType _value;
    
public static implicit operator    MyType(SecondType t)
    
{
        
return t._value;
    }

}

      下例中的Factory.GetObject()会返回一个SecondType型的对象,我们要将它转换为MyType型

object o = Factory.GetObject();
//版本1
MyType t = o as MyType; //在这里会失败
if (t != null)
{
    
//成功 do sth  此例中永远不会运行
}
 
else
{
    
//抛出转换异常
}


//版本2
try 
{
    MyType t1;
    t 
= (MyType)o; //在这里会失败
    if (t1 != null)
    
{
        
//成功 do sth  此例中永远不会运行
    }
 
    
else
    
{
        
//抛出空引用异常
    }

}
 
catch
{
    
//抛出类型转换异常
}

      上例中两种转换都是失败的。但是显式类型转换进行了用户自定义的转换。你觉得它应该可以成功,可事实是转换失败。因为对于o来说,编译器在编译时是基于object型来生成中间代码的,至于在运行时o到底是什么类型编译器并不清楚,在它看来o就是System.Object的一个实例。编译器会检查System.Object和MyType之间是否存在用户自定义转换。由于自定义转换不存在,编译器将生成在运行中检查o是否为MyType型的代码。在上例中因为o是SencondType型的,所以转换失败。编译器并不会检验o在运行时的类型能否转换为MyType型。

      如果修改代码为下例则可成功转换SecondType型到MyType型。

object o = Factory.GetObject();
SecondType st 
= o as SecondType;
try 
{
    MyType t;
    t 
= (MyType) st;
    
if (t != null)
    
{
        
//成功 do sth
    }
 
    
else
    
{
        
//抛出空引用异常
    }

}
 
catch
{
    
//抛出类型转换异常
}

      我们永远不要写出这样丑陋的代码,这里只是举例说明一下问题。你可以将System.Object型的实例作为函数参数来进行类型转换,尽管你永远不应当这样写。

object o = Factory.GetObject();
DoStuffWithObject(o);
private void DoStuffWithObject(object o2)
{
    
try 
    
{
        MyType t 
= (MyType)o2; // Fails. o is not MyType
        if (t != null)
        
{
            
//成功 do sth
        }
 
        
else
        
{
            
//抛出空引用异常
        }

    }
 
    
catch
    
{
        
//抛出类型转换异常
    }

}

      应当记住用户自定义转换只作用于编译时的类型,而不是运行时的类型。上例中o2和MyType之间的类型编译器并不清楚也不关心。当被st声明不同类型时,下例中的代码有着不同的行为。

= (MyType)st;

      而下例中不论st被声明为什么类型,都能返回同样的结果,因此同显式类型转换相比,使用as是更合理的。事实上,如果两个类型之间不是继承关系,只有自定义转换存在的时候,下例的代码会在编译时报错。

= st as MyType;

      现在我们清楚了应当尽量使用as来进行类型转换,下面我们来说一下什么时候不应该使用它。as不适用于值类型。下例中的代码就不能通过编译。

object o = Factory.GetValue();
int i = o as int//不能通过编译

      这是因为int是值类型而且永远不可能为null。如果o不是int型的,那么i里面应该放什么值呢?不论你放入什么,它都会被理解为一个合理的值。因此不能在这里使用as。这里应当使用显式类型转换。

object o = Factory.GetValue();
int i = 0;
try 
{
    i 
= (int)o;
}
 
catch
{
    i 
= 0;
}

      但是你不必这样进行类型转换。我们可以使用is来排除异常转换的可能行

object o = Factory.GetValue();
int i = 0;
if (o is int)
    i 
= (int) o;

      如果o是不能被转换为int的类型,例如double,那么is操作就会返回false。如果被转换对象为null则is操作总是返回null。

      只有当不能使用as进行转换的时候使用is才是有意义的,否则只是多余的。

object o = Factory.GetObject();
MyType t 
= null;
if (o is MyType)
    t 
= o as MyType;

      上例中的代码等同于下例中的代码

object o = Factory.GetObject();
MyType t 
= null;
if ((o as MyType) != null)
    t 
= o as MyType;

      这样做是低效且冗余的。如果你要用as来进行类型转换,那么一般来说is就是不需要的了,检查返回是否为null就可以了。

      现在我们知道了is,as和显式类型转换的差别,那么在foreach中发挥作用的是它们中的哪一种呢?

public void UseCollection(IEnumerable theCollection)
{
    
foreach (MyType t in theCollection)
        t.DoStuff();
}

      foreach用的是显式类型转换来将对象转换为循环中应用的类型。下例中的代码相当于foreach的手写版本

public void UseCollection(IEnumerable theCollection)
{
    IEnumerator it 
= theCollection.GetEnumerator();
    
while (it.MoveNext())
    
{
        MyType t 
= (MyType)it.Current;
        t.DoStuff();
    }

}

      由于foreach既需要应对值类型,也需要应对引用类型,因此它使用显式类型转换。由于应用了显式类型转换,不论目标类型为什么,foreach都声明同样的操作行为。另外由于使用的显式转换,foreach循环可能会抛出转换失败的异常。

      IEnumerator.Current返回一个System.Object类型,而在上例中并没有适用的自定义转换。因为类型转换失败,SecondType类型的集合不能用在UseCollection()函数中。foreach并不会在运行时检验对象集合中的对象类型,它只检验被转换目标的类型(上例中为IEnumerator.Current的返回类型)和循环变量的目的类型(上例中为MyType型)之间的转换是否成立。

      有的时候我们不仅要知道某个对象可以转换为什么类型,还想要知道它的确切类型。as操作符对于继承自目标类型的所有类型都会返回true。GetType()方法可以获得对象在运行时的类型,它的检查比is和as要严格。GetType()返回对象的确切类型,还可以同特定类型进行比较。

      如果你创建一个名为NewType的继承自MyType的子类,UseCollection函数就可以接受NewType型的参数正常工作。

public class NewType : MyType
{
}

      如果你向创建一个可以适用于所有MyType及其继承型参数的函数,那么这样就可以了。但是如果你想让你的函数只接受MyText型,就必须进行确切的类型比较。在上例中我们就应当在foreach循环中做这种工作。一般来说在运行时了解其类型在进行比较时是非常重要的。在大部分情况下,使用is和as在语法上是正确的。

      在oo设计中我们应当尽量避免类型转换,但是有时候这是无法避免的。这时应当尽量使用is和as来将你的意图表示清晰。不同的强制类型转换有不同的规则。is和as在大部分的情况下都是正确的选择,而且它们只在目标类型转换正确的时候才起作用。相比之下显式类型转换可能会在你意想不到的地方成功或者失败,这将为你的程序带来一些负作用。

      译自   Effective C#:50 Specific Ways to Improve Your C#                      Bill Wagner著

      回到目录

posted on 2006-09-02 09:49  aiya  阅读(923)  评论(5编辑  收藏  举报