原则3:选择is或者as操作符而不是做强制类型转换

JerryXia 发表于 , 阅读 (898)

C#是一个强数据类型语言。好的编程实践意味着当可以避免从一种数据类型强制转化为另种数据类型时,我们应该尽我们的所能来避免它。但在某些时候,运行时类型检测是不可避免的。在C#里,大多数时候你要为调用函数的参数使用System.Object类型,因为Framwork已经为我们定义了函数的原型。你很可能要试图把那些类型进行向下转化为其它类型的接口或者类。你有两个选择:用as运算符,或者,采用旧式的C风格,强制转换。(不管是哪一种,)你还必须对变量进行保护:你可以试着用is进行转换,然而再用as进行转换或者强制转换。

无论何时,正确的选择是用as运算符进行类型转换。因为比起盲目的强制转换它更安全,而且在运行时效率更高。用as和is运算符进行转换时,并不是对所有的用户定义的类型都能完成的。它们只在运行时类型和目标类型匹配的时候,转换才能成功。它们决不会构造一个新的对象来满足(转化)要求。

看一个例子。你写了一段代码,要转换一个任意类型的对象实例到一个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/catch结构,所以你可以同时避免(性能)开销和(多写)代码。我们注意到,强制转换的方法为了检测转换是否把一个null的对象进行强制转换,而不得不添加一个捕获异常的结构。null可以被转换为任意的引用类型,但as操作符就算是转化一个null的引用时,也会(安全的)返回一个null。所以当你用强制类型转换时,就得用一个try/catch结构来捕获转换null时的异常。用as进行转换的时就,就只用简单的检测一下转化后的实例不为null就行了。

(译注:被转换对象和转换后的结果都有可能为null,上面就是对这两种null进行了说明,注意区分。强制转换是不安全的,可能会有异常抛出,因此要用try/catch结构来保证程序正常运行,而as转换是安全的,不会有异常抛出,但在转换失败后,其结果为null)

强制转换与as转换最大的区别表现在如何对待用户定义类型的转换。

与其它运算不一样,as和is运算符在运行时要检测转换目标的类型。如果一个指定对象不是要求转换的类型,或者它是从要求转换类型那里派生的,转换会失败。另一方面,强制转换可以用转换操作把一个对象转换成要求的类型。这还包括对内置数据(built-in
numberic)类型的转换。强制转换一个long到一个short可能会丢失数据。

同样的问题也隐藏在对用户定义类型的转换上。考虑这样的类型:

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()函数返回的是SecondType
类型的数据:

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.
}

两种转换都失败了。但是我告诉过你,强制转化可以在用户定义的类型上完成。你应该想到强制转化会成功。你是对的--(如果)它们跟像你想的一样是会成功的。但是转换失败了,因为你的编译器为对象o产生的代码是基于编译时类型。而对于运行时对象o,编译器什么也不知道,它们被视为System.Obejct类型。编译器认为,不存在System.Object类型到用户类型MyType的转换。它检测了System.Object和MyType的定义。缺少任意的用户定义类型转换,编译器(为我们)生成了用于检测运行时对象o的代码,并且检测它是不是MyType类型。因为对象o是SecondType类型,所以失败了。编译器并不去检测实际运行时对象o是否可以被转换为MyType类型。

如果你使用下面的代码段,你应该可以成功的完成从SecondType到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参数来完成正确的转换:

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.
  }
}

记住,对一个用户定义类型的对象,转换操作只是在编译时,而不是在运行时。在运行时存在介于o2和MyType之间的转换并没有关系,(因为)编译器并不知道也不关心这些。这样的语句有不同的行为,这要取决于对st类型的申明:

t = ( MyType ) st;

(译注:上面说的有些模糊。为什么上面的代码可能会有不同的行为的呢?不同的什么行为呢?主要就是:上面的这个转化,是在编译时还是在运行时!如果st是用户定义的类型,那么上面的转换是在编译时。编译器把st当成为System.Object类型来编译生成的IL代码,因此在运行时是无法把一个Object类型转化为MyType类型的。解决办法就是前面提到的方法,多加一条语句,先把Object类型转换为SecondType,然后再强制转化为MyType类型。但是如果st是内置类型,那么它的转换是在运行时的,这样的转化或许会成功,看后面的说明。因此,类似这样的代码:MyType
m_mytype = (m_secondType as SecondType) as
MyType;是不能通过编译的,提示错误是无法在编译时把SecondType转化为MyType,即使是重写了转换操作符。)

但下面的转换只会有一种行为,而不管st是什么类型。

所以你应该选择as来转换对象,而不是强制类型转换。实际上,如果这些类型与继承没有关系,但是用户自己定义的转换操作符是存在的,那么下面的语句转换将会得到一个编译错误:t
= st as
MyType;现在你应该明白要尽可能的使用as,下面我们来讨论不能使用as的时候。as运算符对值类型是无效的,下面的代码无法通过编译:

object o = Factory.GetValue( );
int i = o as int; // Does not compile.

这是因为整形(ints)数据是值类型,并且它们永远不会为null。当o不是一个整形的时候,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;

(译注:is和as一样,都是类型转换安全的,它们在任何时候都不会在转换时发生异常,因此可以先用is来安全的判断一下数据类型。与as不同的时,is只是做类型检测并返回逻辑值,不做转换。)

如果o是其它可转化为整形的类型(译注:但o并不是真正的整形),例如double,那么is运算操作会返回false。对于null,is总是返回false。

is只应该在你无法用as进行转换时使用。
另外,这是无意义的冗余:

// 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来转换数据,那么用is来做检测是不必要的。只用检测返回类型是否为null就行了,这很简单。

现在,你已经知道is,as和强制转换之间的区别了。而在foreach的循环中,是使用的哪一种转换呢?

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

foreach循环是用强制转换来完成把一个对象转换成循环可用的类型。上面的循环代码与下面手写的代码(hand-coded)是等效的:

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

foreach须要用强制转换来同时支持对值类型和引用类型的转换。通过选择强制转化,foreach循环就可以采用一样的行为,而不用管(循环)目标对象是什么类型。不管怎样,因为foreach循环是用的强制转换,因些它可能会产生BadCastExceptions的异常。

因为IEnumberator.Current返回一个System.Obejct对象,该对象没有(重写)转换操作符,所以它们没有一个满足(我们在上面做的)测试。

(译注:这里是说,如果你用一个集合存放了SecondType,而你又想用MyType来对它进行foreach循环,那么转换是失败的,原因是在循环时,并不是用SecondType,而是用的System.Object,因此,在foreach循环里做的转换与前面说的:MyType
t = ( MyType )
o;是一样的错误,这里的o是SecondType,但是是以System.Object存在。)

正如你已经知道的,一个存放了SecondType的集合是不能在前面的函数UseCollection中使用循环的,这会是失败的。用强制转换的foreach循环不会在转换时检测循环集合中的对象是否具有有效的运行时类型。它只检测由IEnumerator.Current返回来的System.Object是否可转换为循环中使用的对象类型,这个例子中是MyType类型。

最后,有些时候你想知道一个对象的精确类型,而不仅仅是满足当前可转换的目标类型。as运算符在为任何一个从目标类型上派生的对象进行转换时返回true。GetType()方法取得一个对象的运行时对象,它提供了比is和as更严格的(类型)测试。GetType()返回的类型可以与指定的类型进行比较(,从而知道它是不是我们想要的类型)。

再次考虑这个函数:

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

如果你添加了一个派生自MyType的新类NewType,那么一个存放了NewType类型对象的集合可以很好的在UseCollection函数中工作。

public class NewType : MyType
{
  // contents elided.
}

如果你想要写一个函数,使它对所有MyType类型的实例都能工作,上面的方法是非常不错的。如果你只想写一个函数,只对精确的MyType对象有效,那你必须用精确的类型比较。这里,你可以在foreach循环的内部完成。大多数时候,认为运行时确定对象的精确类型是很重要的,是在为对象做相等测试的时候。在大多数其它的比较中,由is和as提供的.isinst(译注:IL指令)比较,从语义上讲已经是正确的了。

好的面向对象实践告诉我们,你应该避免类型转换,但有些时候我没别无选择。当你无法避免转换时,用(C#)语言为我们提供的is和as运算符来清楚的表达你的意思吧。不同方法的强制转换有不同的规则。从语义上讲,is和as在绝大多转换上正确的,并当目标对象是正确的类型时,
它们总是成功的。应该选择这些基本指令来转换对象,至少它们会如你所期望的那样成功或者失败;而不是选择强制类型转换,这转换会产生一些意想不到的副作用。

添加新评论