条款3:操作符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。[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
{
|
// 报告转型失败。
}
在正式的开发中,我们绝不能写如此丑陋的代码,但它却向我们揭示了问题的所在。虽然大家永远都不可能像上面那样写代码,但可以使用一个以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;
|
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]在语义上都是正确的。
|
好的面向对象实践一般都告诫我们要避免转型,但有时候我们别无选择。不能避免转型时,我们应该尽可能地使用C#语言中提供的as和is操作符来更清晰地表达意图。不同的转型方式有不同的规则,is和as操作符绝大多数情况下都能满足我们的要求,只有当被测试的对象是正确的类型时,它们才会成功。一般情况下不要使用强制转型,因为它可能会带来意想不到的负面效应,而且成功或者失败往往在我们的预料之外。