天天看点

Effective C# 第二版 中文 之03

原则三:使用is 和 as 而不是用强制类型转换

prefer the is or as operators to casts

         投入到C#的怀抱,你就投入到了强类型(strong type)的怀抱(译注:C#是强类型语言)。这在大部分情况下是有好处的。强类型意味着你希望编译器能找出代码中类型不匹配的地方。这也意味着你的应用程序在运行时不用做太多的类型检查。但有些时候,运行时的类型检查是不可避免的。有时你需要写一些使用object作为参数的方法(假设因为框架中定义了这些方法的签名,使你不得不这么做),你很可能需要将这些object转换成其它类型,不管是类(class)还是接口(interface)。你有两个选择:要么使用as操作符,要么使用cast强制转换。你也可以使用一个稳妥的变通方法:先用is对这个类型转换进行测试,再用as或者cast进行转换。

         正确的选择是:在所有能使用as操作符的地方尽可能地使用它。因为与盲目的强制转换比起来,它更安全而且更高效。As 和is操作符都不进行任何用户定义的转换。它们仅在运行时的类型与目标类型相匹配的情况下才返回成功。它们不会为了满足功能而去创造一个新的对象。

         来看一个例子,你想将任意类型的对象(object)转化成MyType的实例(instance),你可以写成这样:

            //Version one

            object o = Factory.GetObject();

            MyType t = o as MyType;

            if(t != null)

            {

                //用t来处理事务

            }

            else

            {

                //报告错误

            }

也可以写成下面这个样子:

            //Version two

            object o = Factory.GetObject();

            try

            {

                MyType t ;

                t=(MyType)o;

                //用t来处理事务                

            }

            catch(InvalidCastException)

            {

                //报告错误                

            }

         你不得不承认第一个版本更简单、更易读。它没有使用try/catch,所以你即节省了运行时开销,也精简了代码。注意,在使用强制转换的版本中,为了捕获异常,你必须额外地去检验类型是否为null。使用强制转换可以将null转化成任何引用类型,但是对于as操作,当指向一个空引用的时候,会返回一个null值。所以,使用强制转换,你必须检验是否为null并捕获异常(译注:但是举得例子里并没有体现啊!)。而使用as,你只需将返回的引用与null值进行比较就可以了。

(译注:说了这么一大段,中心思想就是:用as你只要检验是否为null就可以了,而用强制转换你不但要检验是否为null,还有捕获、处理异常)。

         As和强制转换最大的区别在于他们对用户定义转换的处理。As和is操作符只是检验要转换的运行时类型,并不做其它操作。如果所检验的类型不是目标类型或从目标类型派生出来的类型,它们检验失败。强制转换则相反,它使用转换操作符将一个对象转换成需要的类型。这包括所有的内建数值类型的转换。将long类型转换成short类型,可能会丢失部分数据。

         当你对用户自定义类型进行强制转换的时候,也可能出现问题。看一下下面的类:

    public class SecondType

    {

        private MyType  _value;

        //转换操作符。将一个SecondType转化为MyType。详见原则9

        public static implicit operator MyType(SecondType t)

        {

            return t._value;

        }

    }

         假设在以下这段代码中,有一个SecondType的对象是由Factory.GetObject()函数返回的:

//version one:

            object o = Factory.GetObject();

            //o 是 SecondType:

            MyType t= o as MyType;//转换失败,o不是MyType

            if(t!=null)

            {

            }

            else

            {

            }

            //version two:

            object o = Factory.GetObject();

            try

            {

                MyType t1;

                t1 = (MyType)o;//转换失败,o不是MyType

            }

            catch(InvalidCastException)

            {

            }

         两个版本都转换失败。但是,强制类型转换是是会执行了用户自定义的转换。你可能会想这样的话(既然执行了用户自定义的转换)那强制转换应该是能成功转换的。没错——如果按照这种思路来说,它是应该成功的。但实际上它还是失败了,因为编译器产生的代码是基于编译时对象o的类型的。编译器并不知道在运行时对象o的实际类型,它将o当成一个object的实例。编译器认为没有从object类型转到MyType类型的用户自定义转换。它检查了object和MyType的定义,没有任何的用户自定义转换,编译器生成代码来检查o的类型,并检查它是否是MyType类型。因为o是SecondType类型,所以这样的转换失败。编译器并不会去检验运行时o的实际类型是否可以转换成MyType类型。

         如果按照以下代码的写法,你可以成功地将SecondType类型转换成MyType类型:

//version three:

            object o = Factory.GetObject();

            SecondType st = o as SecondType;

            try

            {

                MyType t;

                t= (MyType)st;

            }

            catch(InvalidCastException)

            {

            }

         你永远都不应该写出如此丑陋的代码,但它确实解决了一个常见的问题。你可以将一个object对象作为函数的参数并希望它能够进行适当的类型转换,尽管如此,你还是尽量不要这么做:

            object o = Factory.GetObject();

            DoStuffWithObject(o);

        private static void DoStuffWithObject(Object o)

        {

            try

            {

                MyType t;

                t = (MyType)o;//失败,o不是MyType类型。

            }

            catch

            {

            }

        }

         记住,用户自定义转换操作只对于编译时类型有效,而对于运行时类型无效。运行时有一个从o类型向MyType类型转换也没用,编译器不知道或者不在乎。下面的这个语句,当st声明不同的类型的时候它会产生不同的行为:

                t= (MyType)st;

         而下面这个语句,不管st声明型是什么,它都返回相同的结果。所以,比起强制转换你应该更喜欢as——它一致性更好。事实上,如果它们的类型关系不是继承,并且存在它们间的用户自定义转换操作,下面这个语句将会产生一个编译错误:

                t= st as MyType;

         现在,你知道了在能使用as的地方应该尽可能地使用as,我们再来讨论一下什么时候不能使用as。As操作符不能对值类型进行操作,下面这段代码是无法通过编译的:

            object o = Factory.GetObject();

            int i = o as int;

         这是因为int是值类型,值类型永远都不能为null。所以,如果当o不是一个整形数的时候,i应该存储什么值呢?不管你选什么数,它都是一个有效的整形。所以,as在这种情况下就不能使用。你又挣扎于使用强制转换,实际上,这将是一个装箱/拆箱的转换(详见原则45):

            object o = Factory.GetValue();

            int i = 0;

            try

            {

                i = (int)o;

            }

            catch(InvalidCastException)

            {

                i=0;

            }

         使用异常机制是一种不好的习惯(译注:当然,在你没得选的时候,用异常机制总比不用的好)。你可以使用is来消除可能因类型转换而引发的异常,这样你就可以不使用异常机制了:

            object o = Factory.GetValue();

            int i = 0;

            if(o is int)

                i= (int)o;

         如果o是那些可以转化成int的数据类型的时候,例如double,is操作将返回true,否则返回false。对于null参数,它也是返回false。

         只有当你不能使用as来做类型转换的时候,才能使用is。否则,它将产生冗余:

         //正确,但是冗余

            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循环中,你应该使用哪个呢?foreach循环可以对非泛型的IEnumerable序列进行操作,它拥有内建的强制转换。(如果可能的话,应该尽量使用安全类型的泛型版本,但这些非泛型版本的存在是有历史原因的,他们是用来支持一些晚绑定的情况)

        public void UseCollection(IEnumerable theCollection)

        {

            foreach(MyType t in theCollection)

                t.DoStuff();

        }

         foreach通过强制转换将对象转换成目标类型。上面这段由foreach产生的代码大致与以下这段代码相同:

        public void UseCollection(IEnumerable theCollection)

        {

            Enumerator it = theCollection.GetEnumerator();

            while(it.MoveNext)

            {

                MyType t = (MyType)it.Current;

                t.DoStuff();

            }                

        }

         为了同时支持值类型和引用类型,foreach必须使用强制转换。通过使用强制转换,不管目标类型是什么,foreach都可以表现出同样的行为。尽管如此,因为使用的是强制转换,foreach循环可能会抛出一个InvalidCastException(无效的类型转换)错误。

         IEnumerator.Current返回的是一个System.Object类型的对象,而System.Object又没有强制转换操作符,所以以下这些尝试都是不能成功的。一个SecondType的对象的集合是不能使用前面那个UseCollection函数的,因为它会导致类型转换失败,这个前面我们已经讨论过了。Foreach语句(它使用的是强制转换)并不会在运行时检验集合中的类型可否转换成目标类型(它是在编译时做的)。它只是检查是否可以由(从IEnumerator.Current返回的)System.Object类型转换成循环中定义的变量类型(本例中是MyType)。

         有时候,你想要知道一个对象的确切类型,而不仅仅是它能否从当前类型转换成目标类型。如果当前类型是派生自目标类型的话,is操作符将返回true。GetType()方法能够获取一个对象的运行时的类型。这样它就比is和as更加精确。GetType()返回一个对象的类型,并且可以和另一个具体的类型进行比较。

         再次考虑一下这个函数:

         public void UseCollectionV3(IEnumerable theCollection)

        {

            foreach(MyType t in theCollection)

                t.DoStuff();

        }

         如果你定义一个派生自MyType的类型NewType,NewType的集合就可以正常地使用UseCollection()函数了:

    public class NewType:MyType

    {

    }

         如果你想要写一个函数,它能够对所有MyType实例都有效(包括派生自MyType的),上面的写法没问题。但是,如果你想写一个函数,它只对类型确切为MyType的对象(而不是MyType的子类对象)有效,你应该使用精确的类型比较。在这,你可以在循环的内部去实现。大多数时候,当做相等性检查的时候,运行时的确切的类型是非常重要的(详见原则六)。 而在其它比较的时候,由is和as提供的.isinst(译注:IL指令)比较,从语法上已经正确的了。

         .NET的基础类库(BCL:Base  Class  Library)包含了一个方法,这个方法能够使队列中的元素使用相同的类型操作:Enumerable.Cast<T>()能够转换所有支持典型IEnumerable接口的队列的所有元素。

            IEnumerable collection = new List<int>(){1,2,3,4,5,6,7,8,9,10};

            var small = from int item in collection

                        while item <5 

                        select itme;

            var small2 = collection.Cast<int>().Where(item => item <5 ).select(n => n)

(译注:以上这段代码是什么?我还真看不懂,有哪位同学看懂了的,麻烦解释一下。)

         上面的查询产生了与最后一行一样的代码调用。在上面的两种情况中,Cast<T>方法将每一个item都转化成目标类型队列中的一个成员。Enumerable.Cast<T>是用旧的强制转换,而不是as操作符。使用旧的强制转换就意味着Cast<T>不必具有一个类型约束。如果使用as操作符就受到约束,相比较于实现多种不同的Cast<T>方法,BCL团队宁愿选择创建一个单一的只使用强制转换的方法。你在自己的代码中也要做这样的权衡。在一些场合,你需要转换一些泛型参数对象的类型,你就需要权衡一下对类型的必要的一些约束与采用不同处理方法进行转换的利弊了。(有约束会更安全,但同时就需要提供不同的转换方法。你需要在安全和方便之间做一个选择)

在C#4.0,通过使用动态和运行时类型检查,能够规避类型系统。这是第五章——Dynamic  programming  in  C#——中所要讨论的问题。(译注:原书的结构是每几个原则会组成一个章,比如原则1-11是第一章:C#  Language  Idioms。我会在所有原则翻译完后,重新整理一下书的结构,发一个完整版的)。很少有途径去处理一个对象是基于希望了解它的行为而不是了解它详细的类型或是接口支持。你需要知道什么时候要使用这个技术而什么时候要避免。

好的面向对象编程思想告诉我们应该尽量避免类型转换,但是,总是会有一些异类存在。当你不可避免的要使用类型转换的时候,应该用as和is来更清晰地表达你的意图。不同的类型转换方法有着不同的规则,is和as操作符几乎总是拥有正确的语义,他们只有在类型被证明是正确的时候才会转换成功。更喜欢那些强制转换语句,它会拥有一些意外的影响,不管成功或是失败,你都不会对它抱有太大希望。(即不确定性更强)都tem询产生了与最后一行一样的代码调用,