改善C#程序的50种方法 条款3:操作符is或as优于强制转型

转载 2007年09月21日 10:56:00

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类型将会导致部分信息丢失。

条款3:操作符is或as优于强制转型  21

 
在我们使用用户自定义的转换时,也会有同样的问题,来看下面的代码:

public class SecondType

{

  private MyType _value;

  // 忽略其他细节。

  // 转换操作符。

  // 将SecondType 转换为MyType,参见条款29。[4]

  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

{

  // 报告转型失败。

}

两个版本的转型操作都失败了。大家应该还记得我前 面说过强制转型会执行用户自定义的转换,有读者据此认为强制转型的那个版本会成功。这么想本身没有错误,只是编译器在产生代码时依据的是对象o的编译时类 型。编译器对于o的运行时类型一无所知——编译器只知道o的类型是System.Object。因此编译器只会检查是否存在将System.Object 转换为MyType的用户自定义转换。它会到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

{

条款3:操作符is或as优于强制转型  24

 
  // 报告转型失败。

}

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

object o = Factory.GetObject( );

DoStuffWithObject( o );

private void DoStuffWithObject( object o2 )

{

  try {

    MyType t;

    t = ( MyType ) o2; // 转型失败,o的类型不是MyType

    if ( t != null )

    {

      // 处理t, t现在的类型为MyType。

    } else

    {

      // 报告空引用失败。

    }

  } catch

  {

    // 报告转型失败。

  }

}

记住,用户自定义的转换操作符只作用于对象的编译时类型,而非运行时类型上。至于o2的运行时类型和MyType之间是否存在转换,并不重要。事实上,编译器对此并不了解,也不关心。对于下面的语句,如果st的声明类型不同,会有不同的行为:

t = ( MyType ) st;

但对于下面的语句,不管st的声明类型是什么,都会产生同样的结果[5]。因此,我们说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

{

  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;

条款3:操作符is或as优于强制转型  26

 
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检查就没有必要了。直接将as操作符的运算结果和null进行比对就可以了,这样比较简单。

既然我们已经明白了is操作符、as操作符和强制转型之间的差别,那么大家猜猜看foreach循环语句中使用的是哪个操作符来执行类型转换呢?

public void UseCollection( IEnumerable theCollection )

{

  foreach ( MyType t in theCollection )

    t.DoStuff( );

}

答案是强制转型。事实上,下面的代码和上面foreach语句编译后的结果是一样的:

public void UseCollection( IEnumerable theCollection )

{

  IEnumerator it = theCollection.GetEnumerator( );

  while ( it.MoveNext( ) )

  {

    MyType t = ( MyType ) it.Current;

    t.DoStuff( );

  }

}

之所以使用强制转型,是因为foreach语句需要同时支持值类型和引用类型。无论转换的目标类型是什么,foreach语句都可以展现相同的行为。但是,由于使用的是强制转型,foreach语句可能产生BadCastException异常[6]

由于IEnumerator.Current返回 的是System.Object,而Object中又没有定义任何的转换操作符,因此转换操作符就不必考虑了。如果集合中是一组SecondType对 象,那么运用在UseCollection()函数中将会出现转型失败,因为foreach语句使用的是强制转型,而强制转型并不关心集合元素的运行时类 型。它只检查在System.Object类(由IEnumerator.Current返回的类型)和循环变量的声明类型MyType之间是否存在转 换。

最后,有时候我们可能想知道一个对象的确切类型, 而并不关心它是否可以转换为另一种类型。如果一个类型继承自另一个类型,那么is操作符将返回true。使用System.Object的GetType ()方法,可以得到一个对象的运行时类型。利用该方法可以对类型进行比is或as更为严格的测试,因为我们可以拿它所返回的对象的类型和一个具体的类型做 对比。

再来看下面的函数:

public void UseCollection( IEnumerable theCollection )

{

  foreach ( MyType t in theCollection )

    t.DoStuff( );

}

如果创建了一个继承自MyType的类NewType,那便可以将一组NewType对象集合应用在UseCollection函数中。

public class NewType : MyType

{

  // 忽略实现细节。

}

如果我们打算编写一个函数来处理所有与 MyType类型兼容的实例对象,那么UseCollection函数所展示的做法就挺好。但如果打算编写的函数只处理运行时类型为MyType的对象, 那就应该使用GetType()方法来对类型做精确的测试。我们可以将这种测试放在foreach循环中。运行时类型测试最常用的地方就是相等判断(参见 条款9)。对于绝大多数其他的情况,as和is操作符提供的.isinst比较[7]在语义上都是正确的。

 

条款4:使用Conditional特性代替#if条件编译  27

 
好的面向对象实践一般都告诫我们要避免转型,但有时候我们别无选择。不能避免转型时,我们应该尽可能地使用C#语言中提供的as和is操作符来更清晰地表 达意图。不同的转型方式有不同的规则,is和as操作符绝大多数情况下都能满足我们的要求,只有当被测试的对象是正确的类型时,它们才会成功。一般情况下 不要使用强制转型,因为它可能会带来意想不到的负面效应,而且成功或者失败往往在我们的预料之外。 

相关文章推荐

建议3: 区别对待强制转型与as和is

建议3: 区别对待强制转型与as和is 在阐述本建议之前,首先需要明确什么是强制转型,以及强制转型意味着什么。从语法结构上来看,类似下面的代码就是强制转型。 secondType = (Secon...
  • houwc
  • houwc
  • 2016年08月29日 11:45
  • 370

改善C#程序的50种方法

为什么程序已经可以正常工作了,我们还要改变它们呢?答案就是我们可以让它们变得更好。我们常常会改变所使用的工具或者语言,因为新的工具或者语言更富生产力。如果固守旧有的习惯,我们将得不到期望的结果。对于C...

改善C#程序,提高程序运行效率的50种方法

改善C#程序,提高程序运行效率的50种方法 转自:http://blog.sina.com.cn/s/blog_6f7a7fb501017p8a.html一、用属性代替可访问的字段   1、.NE...

《Effective C#:改善C#程序的50种方法》读书笔记

《Effective C#中文版:改善C#程序的50种方法》读书笔记

C#高效编程改善C#程序的50种方法 读书笔记

第一部分:C#的语言元素   一、用属性代替可访问的字段   1、.NET数据绑定只支持对属性的数据绑定,而不支持公有数据成员;   2、在属性的get和set访问器中可使用lock添加多线程的...

《Effective C#中文版:改善C#程序的50种方法》读书笔记

一、用属性代替可访问的字段   1、.NET数据绑定只支持数据绑定,使用属性可以获得数据绑定的好处;   2、在属性的get和set访问器重可使用lock添加多线程的支持。   二、readon...

《Effective C#中文版: 改善C#程序的50种方法》De读书笔记(推荐)

其实这本书我都借了好久,一直没有系统的看,所以趁这两天好好看看,顺便总结了一些要点,给那些需要这方面知识而又没有太多时间的IT人一个快速的学习机会。。。。如果要深入学习,请购买该书。   一、用属性...
  • jpr1990
  • jpr1990
  • 2011年08月16日 13:45
  • 1672

《Effective C#中文版:改善C#程序的50种方法》读书笔记

从去年找工作以来,都没什么时间写博客[找工作的体会:建议以后有自己开公司的IT人一定要找IT专业人员做HR,好多公司的HR并不能真正发掘人才,他们形成了太多的偏见,如在学校期间学不了什么东西、只看学校...
  • haylhf
  • haylhf
  • 2012年08月17日 10:24
  • 768

《Effective C#中文版:改善C#程序的50种方法》读书笔记

转自:http://kb.cnblogs.com/page/106722/       从去年找工作以来,都没什么时间写博客[找工作的体会:建议以后有自己开公司的IT人一定要找IT专业人员做HR,好...

《Effective C#中文版:改善C#程序的50种方法》读书笔记

一、用属性代替可访问的字段     1、.NET数据绑定只支持数据绑定,使用属性可以获得数据绑定的好处;     2、在属性的get和set访问器重可使用lock添加多线程的支持。 二、read...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:改善C#程序的50种方法 条款3:操作符is或as优于强制转型
举报原因:
原因补充:

(最多只允许输入30个字)