美文网首页iOSiOS开发资源博客积累
[译]Swift:利用Enum灵活映射多重类型Data mode

[译]Swift:利用Enum灵活映射多重类型Data mode

作者: 没故事的卓同学 | 来源:发表于2016-08-20 17:34 被阅读857次

    原文链接: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)
    }
    

    👌!
    现在使用的时候不再需要类型转换了。并且如果增加一种新类型,只要在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

    欢迎关注我的微博:@没故事的卓同学

    相关文章

      网友评论

        本文标题:[译]Swift:利用Enum灵活映射多重类型Data mode

        本文链接:https://www.haomeiwen.com/subject/orsisttx.html