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

一個字段中返回了多種類似的類型

先來看下項目中我遇到的一個狀況,服務端在人物中返回了一組數據。這些人物有幾個相同的屬性,可是又有各自不一樣的角色各有的屬性。json數據以下:git

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

那麼咱們能夠怎麼解析這樣的數據呢?github

利用類和繼承

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

這其實就是項目中我原來使用的方案。可是很快就會以爲有點苦逼,由於使用的時候要不斷的類型判斷,而後類型轉換後才能訪問到某個具體類型的屬性:json

// 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的特色,使用的時候咱們仍是要進行類型判斷:swift

// 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

咱們如何建立一個包含不一樣類型數據的數組,而後訪問他們的屬性的時候不用類型轉換呢?dom

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,你就不會遺漏須要再修改何處的代碼,消除了潛在的問題。ui

Raw Value

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

你可能會發現這個枚舉沒有實現RawRepresentable協議,這是由於關聯值類型的枚舉不能同時聽從RawRepresentable協議,他們是互斥的。spa

如何初始化

若是實現了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

歡迎關注個人微博:@沒故事的卓同窗

相關文章
相關標籤/搜索