Swift:類型轉換

wyyu4270 8年前發布 | 4K 次閱讀 Swift Apple Swift開發

每隔一段時間,你都會遇到一些像獨角獸一般前沿的情況,迫使你挑戰你在當前的時代與領域內所積累的一切知識。而就在剛才我成為了這種情況的受害者。

在漢語中,“危機”一詞由兩個字符組成,

一個代表危險,另一個代表機會。

— 約翰·肯尼迪

援引自五十年代末最知名的美國人之一,三十五年后另一個美國人延續了這個話題:

Crisi-tunity!

— 荷馬·辛普森

(譯者注:分別截取了英文單詞 crisis(危險)的前半部分和 opportunity(機會)的后半部分)

危險

之前我有一個機會對我負責的應用中的一個 API 的響應進行回顧、重新建模以及代碼重構,它接受一個 JSON 類型的數據結構作為參數,其中包含有一個數組類型,混合存儲了不同類型的模型。混合存儲的原因是這些模型要在一個 UITableView 中的同一個 section 中按照時間順序展示。這就使得把數據保存在兩個單獨的數組中然后再組合的方案行不通。

為了簡化這個問題,我模擬了一個假的但卻更有趣的 API 響應來展示這個難題:

"characters" : [
    {
        type: "hero",
        name: "Jake",
        power: "Shapeshift"
    },
    {
        type: "hero",
        name: "Finn",
        power: "Grass sword"
    },
    {
        type: "princess",
        name: "Lumpy Space Princess",
        kingdom: "Lumpy Space"
    },
    {
        type: "civilian",
        name: "BMO"
    },
    {
        type: "princess",
        name: "Princess Bubblegum",
        kingdom: "Candy"
    }
]

如你所見,這些對象在某些方面是相似的,但是又包含了一些其他對象沒有的屬性。讓我們來看看一些可能的解決方案。

類和繼承

class Character {
    type: String
    name: String
}
class Hero : Character {
    power: String
}
class Princess : Character {
    kingdom: String
}
class Civilian : Character { 
}
...
struct Model {
    characters: [Character]
}

這是一個完全有效的方案,但是用起來會令人感到沮喪,因為每當我們需要訪問那些模型特有的屬性時,我們都不得不進行類型檢查并且把對象轉換成特定的類型。

// 類型檢測
if model.characters[indexPath.row] is Hero {
    print(model.characters[indexPath.row].name)
}
// 類型檢測及轉換
if let hero = model.characters[indexPath.row] as? Hero {
    print(hero.power)
}

結構體與協議

protocol Character {
    var type: String { get set }
    var name: String { get set }
}
struct Hero : Character {
    power: String
}
struct Princess : Character {
    kingdom: String
}
struct Civilian : Character { 
}
...
struct Model {
    characters: [Character]
}

因為使用了結構體,所以可以從系統層面優化性能,但是使用結構體仍舊與使用超類 非常 相似。因為我們沒有使用協議的任何優勢,目前的情況確實也沒有什么能利用上的,為了訪問類型的特定屬性我們仍需有條件地進行類型判斷和類型轉換。

// Type checking
if model.characters[indexPath.row] is Hero {
    print(model.characters[indexPath.row].name)
}
// Type checking and Typecasting
if let hero = model.characters[indexPath.row] as? Hero {
    print(hero.power)
}

類型轉換

現在你可能認為我非常鄙視類型轉換,但是我沒有。只不過在 這種 情況中,類型轉換有可能向我們的代碼中引入意料之外的副作用。如果向 API 中引入新的 類型 呢?依照我們對響應的解析,它可能什么都不做,也可能做一些事情。當我們編寫代碼時,我們應該保證代碼總是有意義的,因為 Swift的設計原則是安全性優于其他考慮。

{
    type: "king"
    name: "Ice King"
    power: "Frost"
}

機會

我們現在面對的情況是:常規的嘗試最終得到了相同的結果,所以我們被迫在知識的集合之外進行思考,那么不妨讓想法大膽一點或者轉換一下…… 不同的思路

如何創建一個強類型的對象數組,并且訪問它們的屬性時不需要類型轉換?

枚舉

enum Character {
    case hero, princess, civilian
}

因為一個 switch 語句必須是完備的,所以使用枚舉可以有效地清除代碼中的副作用。不過只有枚舉還不夠,我們需要再進一步。

關聯類型

enum Character {
    case hero(Hero) 
    case princess(Princess)
    case civilian(Civilian)
}
...
switch characters[indexPath.row] {
    case .hero(let hero):
        print(hero.power)
    case .princess(let princess):
        print(princess.kingdom)
    case .civilian(let civilian):
        print(civilian.name)
}

再見了,類型轉換…你好,類型轉換???!

現在我們消除了那些只有所有類型轉換都失敗時才會暴露的潛在問題,實施了嚴格的類型驗證,這會引導我們寫出有意料之中的代碼。

原始值

enum Character : String { // Error
    case hero(Hero) 
    case princess(Princess)
    case civilian(Civilian)
}

你可能已經注意到了,上例中的 Character 枚舉不能遵守 RawRepresentable 協議,這是因為一個枚舉在擁有關聯類型的同時不能再遵守 RawRepresentable 協議,兩者是互斥的。

初始化

init

所以如果我們不能通過原始值初始化,我們只能定義自己的構造器,因為原本的原始值初始化方式全部是由 RawRepresentable 協議來完成的,它提供了一個構造器以及一個供我們訪問的 rawValue 。

枚舉類型

enum Character {
    private enum Type : String {
        case hero, princess, civilian
        static let key = "type"
    }
}

一個枚舉,在另一個枚舉內部…這是一個枚舉嵌套。

我們需要更深入…

哇,嗯,好吧謝謝再見

(譯者注:原文用了一個詼諧的組合詞 kthxbai 表示 OK, thank you, goodbye)

在我們使用構造器之前必須預設類型,最好的辦法是使用枚舉,因為當我們嘗試初始化一個對象時,如果出現傳入的 rawValue 不匹配枚舉的任何一個 case 的情況,我們需要讓本次初始化過程失敗。用我們的 JSON 數據格式為例,它有 key 關鍵字來校驗類型,但是不必每次都使用這個關鍵字。在 JSON 對象中你只需要一個唯一的屬性用來校驗你想要建模的類型即可。

允許失敗的構造器

如果 Type 枚舉使用 rawValue 的初始化方式失敗了,那么會引起 Character 對象的初始化失敗。我們將為關聯類型中的每一個成員定義一個相似的允許失敗的構造器,因為 Swift 的編譯器不允許我們使用一個沒有值的枚舉對象,除非該值被聲明為可選型。

// 枚舉字符
init?(json: [String : AnyObject]) {
    guard let 
        string = json[Type.key] as? String,
        type = Type(rawValue: string)
        else { return nil }
    switch type {
        case .hero:
            guard let hero = Hero(json: json) 
            else { return nil }

            self = .hero(hero)

        case .princess:
            guard let princess = Princess(json: json) 
            else { return nil }
            self = .princess(princess)      
        case .civilian:
            guard let civilian = Civilian(json: json) 
            else { return nil }
            self = .civilian(civilian)
    }
}

解析

// 初始化
if let characters = json["characters"] as? [[String : AnyObject]] {
    self.characters = characters.flatMap { Character(json: $0) }
}

當我們解析 JSON 數據的時候,因為 Character 枚舉使用了允許失敗的構造器,必須刪除數組中的 nil 值,所以這里使用了 flatMap 方法,這樣我們的數組就只包含那些非空的值了。

類型轉換

switch model.characters[indexPath.row] {
    case .hero(let hero):
        print(hero.power)

    case .princess(let princess):
        print(princess.kingdom)

    case .civilian(let civilian):
        print(civilian.name)
}

現在你已經掌握這個技巧了,我們讓自己的代碼風格從意料之外的變成了意料之中的,并且在這個過程中學到了新的東西。在此之前,我們還不得不使用由 Any 、 AnyObject 或者其他一些通用的可繼承或者可組合的模型所組成的數組,在使用這樣的數組前需要進行類型檢查和類型轉換。

福利:使用模糊匹配進行類型轉換

if case

假設我們有一個函數需要傳入一個參數值,該參數是 Character 類型的,我們只關心在某一個特定的情況下的處理方案。在這種情況下使用 switch 可能被視為過度使用,因為相關的語法很多并且需要處理所有其他的情況:

func printPower(character: Character) {
    switch character {
        case .hero(let hero):
            print(hero.power)
        default: 
            break
}

我們可以使用模式匹配作為替代,并通過使用 if 或 guard 語句縮短我們的代碼,使其更加簡潔:

func printPower(character: Character) {
    if case .hero(let hero) = character {
        print(hero.power)
    }
}

像往常一樣,我在 GitHub 上為你提供了一個 playgrounds ,同時,如果你手邊沒有 Xcode 的話,請查閱這個 Gist 。

如果你喜歡今天閱讀的內容,你可以查看我的其他文章,如果你打算在自己的項目中使用這種方式,請給我發一條 tweet,或者在 推ter 上關注我,這會讓我感到很開心。

 

 

 

 

來自:http://swift.gg/2016/11/29/swift-typecasing/

 

 本文由用戶 wyyu4270 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!