上一篇主要讲述了C++中的类型转换,本篇讲述的是C#中的类型转换。
不同于复杂的C++,C#语言相对简单,其类型转换规则也比较少,主要有两种:as运算符类型转换和cast运算符类型转换。as运算符的方式不会抛出异常,转换失败返回null值,这也决定了as运算符不能用在值类型上;cast则是强制类型转换,用户可以定义自己的隐式或显式类型转换符。cast运算符在转换失败时会抛出异常。因此基于转换安全因素的考虑,as运算符是被推荐使用的,然而cast运算符是不能被完全取代的。下面将对不同类型转换做分别讨论。
as运算符类型转换
as运算符最鲜明的特点是:输入变量(as运算符左侧)所声明的类型和目标类型(as运算符的右侧)必须有继承关系,这个继承关系必须体现在编译时刻。比如下面的代码中类型A和B没有继承关系,因此类型A和B之间的任何转换都是不能通过编译的:
static void Main() { B b = new B(); A a = b as A; //does not complie } class A { } class B { }
只有类型A和B之间有了继承关系,上面的代码才能编译。object类型是一个特殊情况,我们都知道C#中object是所有类型的基类,因此在任何时候声明为object类型的变量都可以用在as运算符的左侧。那么下面的代码运行的时候会发生什么情况呢:
static void Main() { object b = new B(); //b is declared as object type, but holds a reference of type B A a = b as A; } class A { } class B { }
上面的代码可以正确的编译运行,得出变量a的值是null。代码虽然通过object这个“中介”让程序通过编译,但运行时真正的转换是B->A的转换,返回值也便是null了。除了声明为object类型的变量之外,null也总是可以放在as运算符的左侧而让程序正确编译的,其返回值也永远是null。
谈到as运算符就不得不谈is运算符。is运算符的目的是判断指定的变量是否属于某一个类型,注意is运算符判断的是输入变量(is运算符的左侧)的运行类型是否是目标类型(is运算符的右侧)或目标类型的一个子类型,因此它并不能用来判断变量的真正运行时类型(判断真正运行时类型需要用object.GetType()方法)。开发者在使用is和as运算符时,容易犯的一个错误具备如下的典型代码结构:
A a = new A(); if (a is B) { B b = a as B; // do with b }
代码中as运算符应用前的is运算符检查是完全多余的,事实上as运算符的效率比is运算符的效率要高一些,因此正确的做法应该是:
A a = new A(); B b = a as B; if (b != null) { // do with b }
值得一提的是,is运算符没有as运算符那么多限制,可以应用在任何类型上,或者说is运算符的右侧可以是任何类型。
另一个as运算符应用的小技巧也需要提一下:运行失败时返回null的特点决定了as运算符的目标类型不能是值类型,然而Nullable<T>可以弥补值类型变量没有null值的情况,通过一个小小的修改,as运算符就可以应用到值类型了:
object value = 10; int? i = value as int?; if (i != null) { //do with i.Value }
就这样,代码结构跟引用类型转换的代码结构就一致了。
既然as运算符能解决大部分的类型转换问题,那么cast的方式应用在什么情况呢?
cast运算符类型转换
cast也就是所谓的“强制转换”,然而并不是所有的“强制转换”都能通过编译,比如下面的代码是不能通过编译的:
static void Main() { A a = new A(); B b = (B)a; // does not complie } class A { } class B { }
事实上,“强制转换”也是要有条件的,这个条件就是:1)输入变量所声明的类型和目标类型间具有继承关系(跟as运算符条件一样);或2)输入变量所声明的类型中存在用户自定义的类型转换符(转换到目标类型)。对于这两种情况,有一段简单的示例代码如下:
static void Main() { B b = new B(); A a = (A)b; // complie, cast using the cast operator D d = new D(); C c = (C)d; // compile, same as C c = d; } class A { } class B { A _a = new A(); public static implicit operator A(B b) { return b._a; } } class C { } class D : C { }
cast运算符的执行逻辑是:若输入变量所声明的类型存在用户自定义的往目标类型转换的操作符(如上面代码中的B和A),则执行这个自定义转换;否则输入变量所声明的类型和目标类型存在继承关系,此时cast运算符的转换行为同as运算符类似,区别在于cast运算符在转换失败时抛出异常。注意,在第二种情况下,程序是不会检查运行时类型间是否存在用户自定义转换符的,请看下面代码:
static void Main() { object b = new B(); // b is declared as type of object, but holds a reference of type B A a = (A)b; // throw invalid cast exception } class A { } class B { A _a = new A(); public static implicit operator A(B b) { return b._a; } }
代码中类型B到类型A存在用户自定义转换符,但是b被声明为object类型。编译时类型检查得出结果是类型B到类型object间不存在用户自定义的转换符,于是运行时的转换变成了在类型继承层面上的B类型到A类型的的强制转换。因此程序运行时抛出异常。
null永远可以作为cast运算符的输入变量,其转换后的值仍然是null。对于cast还存在一个特殊情况,请看下面的代码:
object value = 10; int i = (int)value;
代码结构跟前面讲述的其它cast例子一样,但object类型变量往int等值类型转换是比较特殊的,是一个unbox(拆箱)的过程,当然把值类型变量赋值到object类型变量的过程是box(装箱)过程,关于box和unbox本文不做详解。
结论
- 与C++中的类型转换相比,C#中类型转换比较简单。
- is运算符做运行时类型检查,用来判断输入变量是否属于目标类型或目标类型的子类型。
- as运算符做运行时类型转换,但编译时必须确定输入变量所声明的类型和目标类型具有继承关系。
- 当输入变量所声明的类型和目标类型间存在用户自定义转换符时,运行时cast运算符直接执行用户自定义的转换;否则运行时cast运算符做强制类型转换,转换失败则抛出异常,在这个过程中程序不会检查运行时类型间是否存在用户自定义转换符。
在某些情况下,有继承关系的类型间做类型转换会产生函数的版本问题,下一篇将讲述的是:版本化的函数(Versioning Method)。