Effective C# Item 3:Prefer the is or as Operators to Casts
C#是一种强类型语言。好的编程习惯意味着我们应该尽量避免进行强制的类型转换。但是有些时候这是难以避免的。转换的方式有两种:一是使用as,二是C语言风格的显式类型转换(cast)。更保险的办法是先使用is来判断转换是否成立再使用as或显式类型转换。
正确的选择应当是使用as进行转换,因为它比强制转换更为安全和高效。as和is操作符不能对用户自定义的转换进行操作。它们只是简单的比较运行时的被转换类型同目标类型是否相匹配。它们永远不会为转换而新建object。
在下面的代码中,我们要将一个object型实例转换为MyType型。
//版本1
MyType t = o as MyType;
if(t != null)
{
//成功
}
else
{
//失败 抛出异常
}
//版本2
try
{
MyType t = (MyType)o;
if(t != null)
{
//成功
}
else
{
//失败 抛出空引用异常
}
}
catch
{
//失败 抛出类型转换异常
}
很明显第一种方式要更简单清晰,也避免了try/catch的消耗。要注意的是显式类型转换后还必须再一次检验,如果转换后的实例为null还要抛出空引用的异常。而对于as来说这种情况同转换失败一样均返回null值,只要简单的判断其是否为null就可以避免可能遇到的所有异常。
二者最大的不同在于对待用户自定义转换的方式。is和as在运行时检验类型是否可以转换,它们不会做任何其他的操作。如果被转换的类型不是特定的类型或其子类则转换失败。而显式类型转换则会进行转换操作将其强制转换为要求类型。这经常会造成数据丢失,例如数据类型之间的转换。
{
private MyType _value;
public static implicit operator MyType(SecondType t)
{
return t._value;
}
}
下例中的Factory.GetObject()会返回一个SecondType型的对象,我们要将它转换为MyType型
//版本1
MyType t = o as MyType; //在这里会失败
if (t != null)
{
//成功 do sth 此例中永远不会运行
}
else
{
//抛出转换异常
}
//版本2
try
{
MyType t1;
t = (MyType)o; //在这里会失败
if (t1 != null)
{
//成功 do sth 此例中永远不会运行
}
else
{
//抛出空引用异常
}
}
catch
{
//抛出类型转换异常
}
上例中两种转换都是失败的。但是显式类型转换进行了用户自定义的转换。你觉得它应该可以成功,可事实是转换失败。因为对于o来说,编译器在编译时是基于object型来生成中间代码的,至于在运行时o到底是什么类型编译器并不清楚,在它看来o就是System.Object的一个实例。编译器会检查System.Object和MyType之间是否存在用户自定义转换。由于自定义转换不存在,编译器将生成在运行中检查o是否为MyType型的代码。在上例中因为o是SencondType型的,所以转换失败。编译器并不会检验o在运行时的类型能否转换为MyType型。
如果修改代码为下例则可成功转换SecondType型到MyType型。
SecondType st = o as SecondType;
try
{
MyType t;
t = (MyType) st;
if (t != null)
{
//成功 do sth
}
else
{
//抛出空引用异常
}
}
catch
{
//抛出类型转换异常
}
我们永远不要写出这样丑陋的代码,这里只是举例说明一下问题。你可以将System.Object型的实例作为函数参数来进行类型转换,尽管你永远不应当这样写。
DoStuffWithObject(o);
private void DoStuffWithObject(object o2)
{
try
{
MyType t = (MyType)o2; // Fails. o is not MyType
if (t != null)
{
//成功 do sth
}
else
{
//抛出空引用异常
}
}
catch
{
//抛出类型转换异常
}
}
应当记住用户自定义转换只作用于编译时的类型,而不是运行时的类型。上例中o2和MyType之间的类型编译器并不清楚也不关心。当被st声明不同类型时,下例中的代码有着不同的行为。
而下例中不论st被声明为什么类型,都能返回同样的结果,因此同显式类型转换相比,使用as是更合理的。事实上,如果两个类型之间不是继承关系,只有自定义转换存在的时候,下例的代码会在编译时报错。
现在我们清楚了应当尽量使用as来进行类型转换,下面我们来说一下什么时候不应该使用它。as不适用于值类型。下例中的代码就不能通过编译。
int i = o as int; //不能通过编译
这是因为int是值类型而且永远不可能为null。如果o不是int型的,那么i里面应该放什么值呢?不论你放入什么,它都会被理解为一个合理的值。因此不能在这里使用as。这里应当使用显式类型转换。
int i = 0;
try
{
i = (int)o;
}
catch
{
i = 0;
}
但是你不必这样进行类型转换。我们可以使用is来排除异常转换的可能行
int i = 0;
if (o is int)
i = (int) o;
如果o是不能被转换为int的类型,例如double,那么is操作就会返回false。如果被转换对象为null则is操作总是返回null。
只有当不能使用as进行转换的时候使用is才是有意义的,否则只是多余的。
MyType t = null;
if (o is MyType)
t = o as MyType;
上例中的代码等同于下例中的代码
MyType t = null;
if ((o as MyType) != null)
t = o as MyType;
这样做是低效且冗余的。如果你要用as来进行类型转换,那么一般来说is就是不需要的了,检查返回是否为null就可以了。
现在我们知道了is,as和显式类型转换的差别,那么在foreach中发挥作用的是它们中的哪一种呢?
{
foreach (MyType t in theCollection)
t.DoStuff();
}
foreach用的是显式类型转换来将对象转换为循环中应用的类型。下例中的代码相当于foreach的手写版本
{
IEnumerator it = theCollection.GetEnumerator();
while (it.MoveNext())
{
MyType t = (MyType)it.Current;
t.DoStuff();
}
}
由于foreach既需要应对值类型,也需要应对引用类型,因此它使用显式类型转换。由于应用了显式类型转换,不论目标类型为什么,foreach都声明同样的操作行为。另外由于使用的显式转换,foreach循环可能会抛出转换失败的异常。
IEnumerator.Current返回一个System.Object类型,而在上例中并没有适用的自定义转换。因为类型转换失败,SecondType类型的集合不能用在UseCollection()函数中。foreach并不会在运行时检验对象集合中的对象类型,它只检验被转换目标的类型(上例中为IEnumerator.Current的返回类型)和循环变量的目的类型(上例中为MyType型)之间的转换是否成立。
有的时候我们不仅要知道某个对象可以转换为什么类型,还想要知道它的确切类型。as操作符对于继承自目标类型的所有类型都会返回true。GetType()方法可以获得对象在运行时的类型,它的检查比is和as要严格。GetType()返回对象的确切类型,还可以同特定类型进行比较。
如果你创建一个名为NewType的继承自MyType的子类,UseCollection函数就可以接受NewType型的参数正常工作。
{
}
如果你向创建一个可以适用于所有MyType及其继承型参数的函数,那么这样就可以了。但是如果你想让你的函数只接受MyText型,就必须进行确切的类型比较。在上例中我们就应当在foreach循环中做这种工作。一般来说在运行时了解其类型在进行比较时是非常重要的。在大部分情况下,使用is和as在语法上是正确的。
在oo设计中我们应当尽量避免类型转换,但是有时候这是无法避免的。这时应当尽量使用is和as来将你的意图表示清晰。不同的强制类型转换有不同的规则。is和as在大部分的情况下都是正确的选择,而且它们只在目标类型转换正确的时候才起作用。相比之下显式类型转换可能会在你意想不到的地方成功或者失败,这将为你的程序带来一些负作用。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录