首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》4.6 类型转换

关灯直达底部

Swift编译器有严格的类型限制,它会限制什么消息可以发送给某个对象引用。编译器允许发送给某个对象引用的消息是该引用类型所允许的那些消息,包括继承下来的那些。

由于多态的内在一致性法则,对象可以接收到编译器不允许发送的消息。这有时会让我们不知所措。比如,假设在NoisyDog中声明了一个Dog所没有的方法:


class Dog {    func bark {        print(/"woof/")    }}class NoisyDog : Dog {    override func bark {        super.bark; super.bark    }    func beQuiet {        self.bark    }}  

在上述代码中,我们在NoisyDog中增加了一个beQuiet方法。现在来看看调用Dog类型对象的beQuiet方法时会发生什么:


func tellToHush(d:Dog) {    d.beQuiet // compile error}let d = NoisyDogtellToHush(d)  

代码无法编译通过。我们不能向该对象发送beQuiet消息,即便事实上它是个NoisyDog并且具有NoisyDog方法。这是因为,函数体中的引用d的类型为Dog,而Dog是没有beQuiet方法的。这里有点讽刺:我们知道的比编译器还要多!我们知道上述代码是可以正确运行的,因为d实际上是个NoisyDog,只要让代码能够编译通过就行。我们需要通过一种方式告知编译器,“请相信我:当程序真正运行时,它实际上是个NoisyDog,请允许我发送这条消息”。

实际上是有办法做到这一点的,那就是通过类型转换。要想实现类型转换,你需要使用关键字as,后跟真正的类型名。Swift不允许将一种类型转换为不相干的另一种类型,不过可以将父类转换为子类,这叫作向下类型转换。在进行向下类型转换时,你需要在关键字as后面加上一个感叹号,即as!。感叹号提醒你在让编译器做一些它本不会做的事情:


func tellToHush(d:Dog) {    (d as! NoisyDog).beQuiet}let d = NoisyDogtellToHush(d)  

上述代码可以编译通过,并且正常运行。对于该示例来说,更好的写法是下面这样:


func tellToHush(d:Dog) {    let d2 = d as! NoisyDog    d2.beQuiet    d2.beQuiet}let d = NoisyDogtellToHush(d)  

之所以说上面这种写法更好是因为如果还会向该对象发送其他NoisyDog消息,那就不用每次都执行类型转换了,我们可以根据内在一致性类型只转换对象一次,并将其赋给一个变量。既然可以根据类型转换推测出变量的类型(即内在一致性类型),我们就可以向该变量发送多条消息了。

我说过as!运算符的感叹号会提醒你强制编译器进行转换。它还有警告的作用:代码可能会崩溃!原因在于你可能对编译器撒谎。向下类型转换会让编译器放松其严格的类型检查,让你能够正常调用。如果使用类型转换做了错误的声明,那么编译器还是会允许你这么做,不过当应用运行时就会崩溃:


func tellToHush(d:Dog) {    (d as! NoisyDog).beQuiet // compiles, but prepare to crash...!}let d = DogtellToHush(d)  

在上述代码中,我们告诉编译器该对象是个NoisyDog,编译器选择相信我们,并允许我们向该对象发送beQuiet消息。不过事实上,当代码运行时,该对象是个Dog,因此由于该对象并不是NoisyDog,类型转换会失败,程序将会崩溃。

为了防止这种错误,你可以在运行时测试实例的类型。一种方式是使用关键字is。你可以在条件中使用is;判断通过后再转换,这样转换就是安全的了:


func tellToHush(d:Dog) {    if d is NoisyDog {        let d2 = d as! NoisyDog        d2.beQuiet    }}  

结果是这样的:除非d真的是NoisyDog,否则我们不会将其转换为NoisyDog。

解决这个问题的另一种方式是使用Swift的as?运算符。它也会进行向下类型转换,不过提供了失败的选项;因此,它转换的结果是个Optional(你可能已经猜出来了),现在回到了我们熟知的领域,因为我们已经知道如何安全地处理Optional了:


func tellToHush(d:Dog) {    let noisyMaybe = d as? NoisyDog // an Optional wrapping a NoisyDog    if noisyMaybe != nil {        noisyMaybe!.beQuiet    }}  

这与之前的做法相比并没有简洁多少。不过,还记得我们可以通过展开Optional向一个Optional发送消息吧!因此,我们可以省略赋值并将代码压缩到一行:


func tellToHush(d:Dog) {    (d as? NoisyDog)?.beQuiet}  

首先,我们通过as?运算符获取到一个包装了NoisyDog(或者是nil)的Optional。接下来展开该Optional,并向其发送了一条消息。如果d不是NoisyDog,那么该Optional就是nil,消息也不会发送。如果d是NoisyDog,那么该Optional将会展开,消息也会发送出去。这样,代码就是安全的。

回忆一下第3章,对Optional使用比较运算符会自动应用到该Optional所包装的对象上。as!、as?与is运算符的工作方式是一样的。如果有一个包装了Dog的Optional d(也就是说,d是个Dog?对象),那么它实际上会包装一个Dog或NoisyDog;替换法则对Optional类型也适用,因为它对Optional所包装的类型适用。要想知道它到底包装的是什么,你可能会使用is,是吗?毕竟,这个Optional既不是Dog也不是NoisyDog,它是个Optional!好消息是Swift知道你的想法;如果is左边的是个Optional,那么Swift就会认为它是包装在Optional中的值。这样,其工作方式与你期望的就一致了:


let d : Dog? = NoisyDogif d is NoisyDog { // it is!  

如果对Optional使用is,那么如果该Optional为nil,测试就会失败。is实际上做了两件事:它会检查Optional是否为nil,如果不是,那么它会继续检查被包装的值是否是我们所指定的类型。

那么类型转换呢?你不能将Optional转换为任何其他类型。不过,你可以对Optional使用as!运算符,因为Swift知道你的想法;如果as!左侧是Optional,那么Swift就会将其当作被包装的类型。此外,使用as!运算符会做两件事情:Swift首先展开Optional,然后进行类型转换。如下代码可以正常运行,因为d被展开得到d2,它是个NoisyDog:


let d : Dog? = NoisyDoglet d2 = d as! NoisyDogd2.beQuiet  

不过,上述代码并不安全。你不应该在不测试的情况下就进行类型转换,除非你对要做的事情很有把握。如果d为nil,那么第2行代码就会崩溃,因为这时你所展开的是一个nil Optional。如果d是个Dog而非NoisyDog,那么类型转换还是会失败,第2行代码依然会崩溃。这正是as?运算符存在的原因,它是安全的,不过会生成一个Optional:


let d : Dog? = NoisyDoglet d2 = d as? NoisyDogd2?.beQuiet  

还有一种情况会用到类型转换,那就是在进行Swift与Objective-C值交换时(两个类型是相同的)。比如,你可以将Swift String转换为Cocoa NSString,反之亦然。这并不是因为其中一个是另一个的子类,而是因为它们之间可以彼此桥接;它们本质上是相同的类型。在从String转换为NSString时,其实并没有做向下类型转换,你所做的事情并没有什么不安全的,因此可以使用as运算符,不需要使用感叹号。第3章给出了一个示例,介绍了什么情况下需要这么做,如下代码所示:


let s = /"hello/"let range = (s as NSString).rangeOfString(/"ell/") // (1,3), an NSRange  

从String到NSString的转换告诉Swift,在调用rangeOfString时要使用Cocoa,这样结果就是Cocoa了,即一个NSRange而非Swift Range。

Swift与Objective-C中的很多常见类都是通过这种方式桥接的。通常,在从Swift到Objective-C时并不需要进行转换,因为Swift会自动进行转换。比如,Swift Int与Cocoa NSNumber是完全不同的两种类型;不过,你可以在需要NSNumber的地方使用Int,无须进行转换,如下代码所示:


let ud = NSUserDefaults.standardUserDefaultsud.setObject(1, forKey: /"Test/")  

在上述代码中,我们在Objective-C期望NSObject实例的地方使用了Int(即1)。Int并非NSObject实例;它甚至都不是类实例(它是个结构体实例)。不过,Swift发现这个地方需要NSObject,并且确定NSNumber最适合表示Int,于是帮你进行了桥接。因此,存储在NSUserDefaults中的实际上是个NSNumber。

不过,在调用objectForKey:时,Swift并不知道这个值实际上是什么,因此如果需要Int时就得显式进行转换,这里做的就是向下类型转换(稍后将会对其进行详细介绍):


let i = ud.objectForKey(/"Test/") as! Int  

上述转换是正确的,因为ud.objectForKey(/"Test/")会生成一个包装整型的NSNumber,将其转换为Swift Int是可行的,类型之间会桥接起来。不过,如果ud.objectForKey(/"Test/")不是NSNumber(或是nil),那么程序将会崩溃。如果不确定,请使用as?确保安全。