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

来源:互联网 发布:龙泉宝剑淘宝 编辑:程序博客网 时间:2024/05/01 20:00
 使用const较之于使用readonly的唯一好处就是性能:使用已知常量值的代码效率要比访问readonly值的代码效率稍好一点。但是这其中的效率提升是非常小的,大家应该和其所失去的灵活性进行一番权衡比较。在打算放弃灵活性之前,一定要对两者的性能差别做一个评测。
综上所述,只有当某些情况要求变量的值必须在编译时可用,才应该考虑使用const,例如:特性(attribute)类的参数,枚举定义,以及某些不随组件版本变化而改变的值。否则,对于其他任何情况,都应该优先选择readonly常量,从而获得其所具有的灵活性。
条款3:操作符isas优于强制转型

 

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