[译]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

欢迎关注个人微博:@没故事的卓同窗

相关文章
相关标签/搜索