C#是强类型的语言。好的编程习惯是当可以避免类型转换就尽量避免。但有时,运行时类型检查是不可避免的。c#中你常常写些带有System.Object类型的参数的方法,因为框架已经为你定义了方法的原型。你可能会将Object类型向下转型,转成其他类型或类或接口。你有两个选择:用as操作符或老式的C强制转型写法。你得对变量保护:你可以用is操作符测试类型转换,然后再进行转换。
正确的做法总是使用as操作符,因为它不盲目转换,是安全的,而且在运行时效率高。对于as和is操作符,它们并不对所有用户类型有效。在运行时类型匹配目标类型时它们才是有效的,它们从不构造一个新的对象去满足转型。
看一例子,你可能将任意类型的对象转换成MyType类型,你可能会这样写:
强制类型转换与as/is操作符之间最大的区别在于它们对用户类型处理转型的方式。as/is操作符检查目标对象的运行时类型,它们不做任何额外的工作。如果指定的对象不是要求的类型或继承自要求的类型时,转换就会失败。另一方面,强制类型转换总可以把对象转换成目标类型,这也包括一些内置的数据类型转换。把一个long类型转换成short类型会丢失精度。
同样的问题存在于用户类型转换。考虑下面的类型:
如果你用下面的代码片断,那么从SecondType转型至MyType将会成功:
is操作符只用于不能用as进行转换的情况。否则就是多余的:
现在你清楚了is,as,强制类型转换三者之间的差别,那么你会在foreach循环中用哪个呢,而实际上它是使用的哪个操作呢?
因为IEnumerator.Current返回的类型是System.Object,它没有任何转换操作,所以它不符合我们的测试。SecondType对象的集合不能用在前面的方法UseCollection(),因为转换会失败,你已经知道了。foreach语句并不检查集合中的运行时类型对象强制类型转换是否有效,它只检查System.Object类型与循环中定义的类型MyType之间的转换是否有效。
最后,有时你可能比较关注Object的精确类型,而不是是否可把当前类型转换成目标类型。对任何继承自目标类型的类型as操作符返回true。GetType()方法返回对象的运行时类型,它提供比is/as操作符更严格的测试,它返回对象的类型并可以与指定的类型比较。
重新考虑方法:
好的面向对象实践指出应尽量避免类型转换,但有时这是无法避免的。当无法避免类型转换时,你应尽可能地用语言提供的is/as操作符来明确表达你的意图。不同的强制类型转换有不同的规则。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.
}
或者你可能这样写:// 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。所以强制类型转换你必须检查null及捕捉异常,用as操作符只需简单的检查下结果是否为null引用。// 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.
}
强制类型转换与as/is操作符之间最大的区别在于它们对用户类型处理转型的方式。as/is操作符检查目标对象的运行时类型,它们不做任何额外的工作。如果指定的对象不是要求的类型或继承自要求的类型时,转换就会失败。另一方面,强制类型转换总可以把对象转换成目标类型,这也包括一些内置的数据类型转换。把一个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;
}
}
在上面的代码片断中,假设SecondType的一个对象由Factory.GetObject()方法返回:{
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;
}
}
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一无所知,它把o看作是System.Obejct类型的实例。编译器认为并没有从System.Object类型到用户类型MyType的转换。它检查System.Obejct类型和MyType类型的定义,缺少用户类型转换,编译器产生检查运行时o的类型的代码并检查它是不是MyType类型。因为o是SecondType类型的,所以转型失败了。编译器并不去检查实际运行时对象o是否可转型成MyType类型。// 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.
}
如果你用下面的代码片断,那么从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类型的参数的方法来完成正确的转换:// 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.
}
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的声明: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.
}
}
t = ( MyType ) st;
但下面的语句返回同样的结果,无论st是什么类型,所以你要尽量使用as避免强制转换。事实上如果类型间并无继承关系,就算存在用户类型的转换操作,下面的语句会产生一个编译时错误:t = st as MyType;
现在你知道要尽量使用as操作符,那么让我们来讨论下不能使用它的情况。as操作符不能作用于值类型,下面的语句不能通过编译:object o = Factory.GetValue( );
int i = o as int; // Does not compile.
那是因为int是值类型且不会为null。如果o不是整型,i会是什么值呢?不管你选择什么值,你都将得到一个无效的整数,所以as不能使用。你可以坚持用强制转换:int i = o as int; // Does not compile.
object o = Factory.GetValue( );
int i = 0;
try {
i = ( int ) o;
} catch
{
i = 0;
}
其实强制转换是不必要的,你可以用is操作符替换之:int i = 0;
try {
i = ( int ) o;
} catch
{
i = 0;
}
object o = Factory.GetValue( );
int i = 0;
if ( o is int )
i = ( int ) o;
如果o是可以转换为int的其他类型,比如double类型,is操作符也返回false。对于null,它总返回false。int i = 0;
if ( o is int )
i = ( int ) o;
is操作符只用于不能用as进行转换的情况。否则就是多余的:
// correct, but redundant:
object o = Factory.GetObject( );
MyType t = null;
if ( o is MyType )
t = o as MyType;
前面的代码和下面的代码效果是一样的: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检查是多余的。你只需检查as转换的返回值是否为null,就这么简单。object o = Factory.GetObject( );
MyType t = null;
if ( ( o as MyType ) != null )
t = o as MyType;
现在你清楚了is,as,强制类型转换三者之间的差别,那么你会在foreach循环中用哪个呢,而实际上它是使用的哪个操作呢?
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
foreach循环中使用的是强制类型转换。foreach产生的代码等效于下面的代码:{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
public void UseCollection( IEnumerable theCollection )
{
IEnumerator it = theCollection.GetEnumerator( );
while ( it.MoveNext( ) )
{
MyType t = ( MyType ) it.Current;
t.DoStuff( );
}
}
foreach使用强制类型转换同时支持值类型与引用类型。选择使用强制类型转换,foreach语句展现同样的行为,不论目标类型是什么。不管怎样,使用强制类型转换,foreach语句可能产生BadCastException异常。{
IEnumerator it = theCollection.GetEnumerator( );
while ( it.MoveNext( ) )
{
MyType t = ( MyType ) it.Current;
t.DoStuff( );
}
}
因为IEnumerator.Current返回的类型是System.Object,它没有任何转换操作,所以它不符合我们的测试。SecondType对象的集合不能用在前面的方法UseCollection(),因为转换会失败,你已经知道了。foreach语句并不检查集合中的运行时类型对象强制类型转换是否有效,它只检查System.Object类型与循环中定义的类型MyType之间的转换是否有效。
最后,有时你可能比较关注Object的精确类型,而不是是否可把当前类型转换成目标类型。对任何继承自目标类型的类型as操作符返回true。GetType()方法返回对象的运行时类型,它提供比is/as操作符更严格的测试,它返回对象的类型并可以与指定的类型比较。
重新考虑方法:
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
如果你创建一个继承自MyType类型的类NewType,那么NewType对象的集合在这个方法中工作正常:{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
public class NewType : MyType
{
// contents elided.
}
如果你想写一个方法使所有MyType类型的实例都运作正常,上面那个已经很好了。如果你想写一个方法只有精确的MyType类型运作,你只需做精确的类型比较。这里你必须在foreach循环里面实现。大多数时候在为对象做相等测试时知道其精确类型是比较重要的(参见Item9)。在其他的类型比较中,as/is提供的.isinst比较语义上已经基本正确了。{
// contents elided.
}
好的面向对象实践指出应尽量避免类型转换,但有时这是无法避免的。当无法避免类型转换时,你应尽可能地用语言提供的is/as操作符来明确表达你的意图。不同的强制类型转换有不同的规则。is/as语句大多数情况下是正确的语义,它们只有在对象类型被测试为正确的类型时发生。尽可能地用它们来进行转换操作而不是强制类型转换,它们至少如你所期望的成功或失败,而强制类型转换可能产生意想不到的副作用。