讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 4.6 類型轉換 >

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 = NoisyDog
tellToHush(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 = NoisyDog
tellToHush(d)
  

上述代碼可以編譯通過,並且正常運行。對於該示例來說,更好的寫法是下面這樣:


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

之所以說上面這種寫法更好是因為如果還會向該對像發送其他NoisyDog消息,那就不用每次都執行類型轉換了,我們可以根據內在一致性類型只轉換對像一次,並將其賦給一個變量。既然可以根據類型轉換推測出變量的類型(即內在一致性類型),我們就可以向該變量發送多條消息了。

我說過as!運算符的感歎號會提醒你強制編譯器進行轉換。它還有警告的作用:代碼可能會崩潰!原因在於你可能對編譯器撒謊。向下類型轉換會讓編譯器放鬆其嚴格的類型檢查,讓你能夠正常調用。如果使用類型轉換做了錯誤的聲明,那麼編譯器還是會允許你這麼做,不過當應用運行時就會崩潰:


func tellToHush(d:Dog) {
    (d as! NoisyDog).beQuiet // compiles, but prepare to crash...!
}
let d = Dog
tellToHush(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? = NoisyDog
if 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? = NoisyDog
let d2 = d as! NoisyDog
d2.beQuiet
  

不過,上述代碼並不安全。你不應該在不測試的情況下就進行類型轉換,除非你對要做的事情很有把握。如果d為nil,那麼第2行代碼就會崩潰,因為這時你所展開的是一個nil Optional。如果d是個Dog而非NoisyDog,那麼類型轉換還是會失敗,第2行代碼依然會崩潰。這正是as?運算符存在的原因,它是安全的,不過會生成一個Optional:


let d : Dog? = NoisyDog
let d2 = d as? NoisyDog
d2?.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.standardUserDefaults
ud.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?確保安全。