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

IlaMetts 8年前發布 | 10K 次閱讀 Swift Apple Swift開發

原文鏈接: Swift: Typecasing

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

先來看下項目中我遇到的一個情況,服務端在人物中返回了一組數據。這些人物有幾個相同的屬性,但是又有各自不同的角色各有的屬性。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)
}

:ok_hand:!

現在使用的時候不再需要類型轉換了。并且如果增加一種新類型,只要在enum中增加一個case,你就不會遺漏需要再修改何處的代碼,消除了潛在的問題。

Raw Value

enum Character : String { // Error: :x:
    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

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

 

 

來自:http://www.jianshu.com/p/87255dc14331

 

Save

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