Item 3: Prefer the is or as Operators to Casts
C#是强类型语言.我们要尽量避免类型转换.
有时我们必须要在runtime检查一个变量的类型.比如有时你要用到一些.Net framework提供的方法,这些方法需要用到System.Object类型的参数.你需要把这些object (方法的参数)向下cast成其他的类型(类或者interface),这时你有两个基本方式可以选择,一是使用as操作符,二是使用C语言风格的cast.两者也可以结合成一个更加保险的方法,就是先用is 操作符来测试类型的转换,然后再用cast或者as 操作符进行转换.
正确的选择应该是使用as操作符来进行类型转换. as操作符比碰运气型的cast更加的安全,而且在runtime更加的高效. as和is操作符并不能进行所有的用户定义的类型转换, 只有当runtime类型和目标类型一致时转换操作才会成功.它们永远不会为了满足程序调用请求而创建一个新的object.
在下例中,你需要把一个object转换成一个MyType的实例,你可以这样实现:
object o = Factory.GetObject( );<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
// 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的overhead.代码更加的高效.我们注意到cast版本不仅要检查转换后的object是否为null,还要catch异常.但as操作符版本却不用. 这是因为使用cast时, null可以被转换成任何一种reference类型,但是当应用as操作符在一个null reference上时会返回null. 所以as操作符只需检查一下返回的reference是否为null, 而不用catch异常.
as操作符和cast操作符最大的不同是如何对待用户定义的转换. as和is操作符只会检查被转换的object的runtime类型,而不做任何其他的工作. 如果这个object不是目标类型,或者不是目标类型的子类型, 操作失败并终止. 但cast操作符却不同,它会把object转换成目标类型, 这包括所有的numeric转换, 比如从long转换成short, object的一些信息就在这种转换中丢失了.
而且当cast用户自定义类型时也会有同样的问题.比如:
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,并把它转换成MyType类型.
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.
}
两个版本都会失败.但是cast却执行了用户定义的转换.这种假象使你认为cast成功了.但实际上它是失败的,因为编译器会根据编译时object的类型来生成代码.编译器对运行时object的类型一无所知.它只是把o当作System.Object的一个实例.编译器没有发现从System.Object到MyType可行的转换.它检查System.Object和MyType的定义,因为缺少用户定义的转换信息,编译器生成代码来检查o的运行时的类型,然后检查它是不是MyType类型.因为o是SecondType类型,所以转换失败.编译器并不检查o在运行时的类型是否可以转换成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来当作一个进行转换操作的function的参数,比如:
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.
}
}
用户自定义的转换只作用于编译时object的类型,而不时运行时的类型. 至于运行时是否存在o2和MyType类型的转换, 编译器不知道也根本不关心. 但当st类型不同时,这个语句有着不同的表现:
t = ( MyType ) st;
上面的语句会调用用户自定义的转换,从而造成转换成功的假象. 但使用as操作符的语句却有着一致的表现.所以应该尽量的使用as操作符.如下面的语句:
t = st as MyType;
事实上,如果st和MyType之间不存在继承关系的话,而是通过一个用户自定义的转换来进行类型转换,那么编译器会报告一个错误.
现在你知道了应该尽可能的使用as操作符.但也有一些情况不能使用它.as操作符不能作用于value type上.下面的这个语句不会通过编译:
object o = Factory.GetValue( );
int i = o as int; // Does not compile.
因为int是值类型,永远不能为null.那么如果o不是整数类型的话, i里面应该存什么值呢? 所以你不能使用as操作符.你可以用下面这种变通的方式:
object o = Factory.GetValue( );
int i = 0;
try
{
i = ( int ) o;
}
catch
{
i = 0;
}
但你不必一定这样一来做,不要忘了is操作符,你可以在转换之前先判断o的类型:
object o = Factory.GetValue( );
int i = 0;
if ( o is int )
i = ( int ) o;
如果o不是整数类型,那么is操作符返回false. is操作符作用于null arguments上时,永远返回false;
但你应该只在你不能使用as操作符转换类型时使用is,否则就是重复的, 比如:
// 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操作符来转换类型,那么只需检查返回值是否为null就可以了.
现在你已经明白了as, is和cast,那么foreach循环用的是什么操作符呢?
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
foreach用的实际上是cast操作符.上面的代码可以重写成下面的代码:
public void UseCollection( IEnumerable theCollection )
{
IEnumerator it = theCollection.GetEnumerator( );
while ( it.MoveNext( ) )
{
MyType t = ( MyType ) it.Current;
t.DoStuff( );
}
}
这是因为foreach要用cast来支持value type和reference type. 如果使用as操作符的话,foreach语句仍表现相同的行为,但会抛出BadCastException,因为as不能作用于值类型上.
因为IEnumerator.Current返回一个System.Object类型的object,而这个object不具备转换操作,所以并不能用于这个测试. SecondType类型的collection也不能用于UseCollection()因为转换会失败. Foreach语句并不检查collection中object的运行时类型是否支持这种转换,它只检查IEnumerator.Current所返回的System.Object类型是否支持到目标类型(本例中的MyType)之间的转换.
最后,有时你想知道一个object确切的类型,而不只是关心这个object是否可以转换成目标类型. 因为as操作符对于所有从目标类型继承而来的类型的转换都返回true. 但GetType()方法返回object运行时的类型,它比as或者is操作符提供的测试都要更严格. 它返回的是object的确切类型.
再看一下UseCollection():
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
如果你创建一个叫NewType的类,这个类继承MyType,那么NewType objects的collection也可以在UseCollection()中很好的工作.
public class NewType : MyType
{
// contents elided.
}
如果你的意图是写出一个可以使用所有MyType类型(自身或继承而来)的function时,这没有什么.但如果你的意图是写一个只接受MyType类型自身的function的话,你就要用确切的类型来进行比较. 在本例中,你可以在foreach循环里做. 知道运行时确切的类型只有在做equality测试时是非常重要的.在大多数其他的情况下,as和is操作符提供的isinst比较是语法上正确的.
好的OO经验告诉我们要尽量避免类型的转换,但有时类型转换是必需的.在这种情况下,尽量的使用as和is操作符来表达你的意图. 不同的类型强制转换有不同的规则,但as和is却在绝大多数情况下都是正确的,而且它们只有在object是正确的类型时才转换成功. Cast操作符会带来一些副作用,而且转换的成功与失败往往出乎意料.
本系列文章只是作者读书笔记,版权完全属于原作者 (Bill Wagner),任何人及组织不得以任何理由以商业用途使用本文,任何对本文的引用和转载必须通知作者:zphillm@hotmail.com