[譯]Swift: 利用 Enum 靈活對映多重型別 Data model

沒故事的卓同學發表於2016-08-20

一個欄位中返回了多種相似的型別

先來看下專案中我遇到的一個情況,服務端在人物中返回了一組資料。這些人物有幾個相同的屬性,但是又有各自不同的角色各有的屬性。json資料如下:

"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]
}複製程式碼

這其實就是專案中我原來使用的方案。但是很快就會覺得有點苦逼,因為使用的時候要不斷的型別判斷,然後型別轉換後才能訪問到某個具體型別的屬性:

// 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)
}複製程式碼

利用結構體和協議

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]
}複製程式碼

這裡我們使用了結構體,解析的效能會好一些。但是看起來和前面類的方案差不多。我們並沒有利用上protocol的特點,使用的時候我們還是要進行型別判斷:

// 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)
}複製程式碼

型別轉換的潛在問題

上面的這種型別轉換可能引入潛在的問題。如果後臺此時增加了一個型別對程式碼會產生什麼樣的影響呢?可能想到這種情況提前做了處理,也可能沒有處理導致崩潰。

{
    type: "king"
    name: "Ice King"
    power: "Frost"
}複製程式碼

當我們在寫程式碼的時候,應該考慮到這樣的場景,當有新型別出現時能不能友好的提示哪裡需要處理呢?畢竟swift的設計目標之一就是更安全的語言。

另外一種可能:Enum

我們如何建立一個包含不同型別資料的陣列,然後訪問他們的屬性的時候不用型別轉換呢?

enum Character {
    case hero, princess, civilian
}複製程式碼

當switch一個列舉時,每種case都需要被照顧到,所以使用enum可以很好的避免一些潛在的問題。但是如果只是這樣依然不夠好,我們可以更進一步:

Associated values:關聯值

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中增加一個case,你就不會遺漏需要再修改何處的程式碼,消除了潛在的問題。

Raw Value

enum Character : String { // Error: ❌
    case hero(Hero) 
    case princess(Princess)
    case civilian(Civilian)
}複製程式碼

你可能會發現這個列舉沒有實現RawRepresentable協議,這是因為關聯值型別的列舉不能同時遵從RawRepresentable協議,他們是互斥的。

如何初始化

如果實現了RawRepresentable協議,就會自帶一個利用raw value 初始化的方法。但是我們現在沒有實現這個協議,所以我們需要自定義一個初始化方法。 先定義一個內部使用的列舉表示型別:

enum Character {

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

}複製程式碼

Failable initializers

因為傳回來的json可能出現對映失敗的情況,比如增加的一個新型別,所以這裡的初始化方法是可失敗的。

// enum Character
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)
    }
}複製程式碼

使用列舉解析json

// Model initialisation
if let characters = json["characters"] as? [[String : AnyObject]] {
    self.characters = characters.flatMap { Character(json: $0) }
}複製程式碼

注意這裡使用了flatMap。當一條資料的type不在我們已經定義的範圍內時,Character(json: [String : AnyObject])返回一個nil。我們當然希望過濾掉這些無法處理的資料。所以使用flatMap,flatMap過程中會拋棄為nil的值,所以這裡使用了flapMap。

完成!

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或者泛型,繼承組合的model,使用時再轉換型別的日子了。

One More Thing: 模式匹配

如果只處理列舉中的一種型別,我們會這麼寫:

func printPower(character: Character) {
    switch character {
        case .hero(let hero):
            print(hero.power)
        default: 
            break
}複製程式碼

然而我們可以利用swift提供的模式匹配,用這種更優雅的寫法:

func printPower(character: Character) {
    if case .hero(let hero) = character {
        print(hero.power)
    }
}複製程式碼

github上的原始碼:playgrounds

歡迎關注我的微博:@沒故事的卓同學

相關文章