美文网首页Swift iOS开发技术博客
Swift - Codable 解码设置默认值

Swift - Codable 解码设置默认值

作者: z4ywzrq | 来源:发表于2021-06-17 22:05 被阅读0次

    掘金同步更新:https://juejin.cn/user/3378158048121326/posts

    上一篇 Swift - Codable 使用小记 文章中介绍了 Codable 的使用,它能够把 JSON 数据转换成 Swift 代码中使用的类型。本文来进一步研究使用 Codable 解码如何设置默认值的问题。

    解码遇到的问题

    之前的文章中提到了,遇到 JSON 数据中字段为空的情况,把属性设置为可选的,当返回为空对象或 null 时,解析为 nil。
    当我们希望字段为空时,对应的属性要设置一个默认值,我们处理的一种方法是重写 init(from decoder: Decoder) 方法,在 decodeIfPresent 判断设置默认值,代码如下:

    struct Person: Decodable {
        let name: String
        let age: Int
        
        enum CodingKeys: String, CodingKey {
            case name, age
        }
        
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            name = try container.decode(String.self, forKey: .name)
            age = try container.decodeIfPresent(Int.self, forKey: .age) ?? -1
        }
    }
    
    let data = """
    { "name": "小明", "age": null}
    """
    let p = try JSONDecoder().decode(Person.self, from: data.data(using: .utf8)!)
    //Person(name: "小明", age: -1)
    

    这种方法显然很麻烦,需要为每个类型添加 CodingKeys 和 init(from decoder: Decoder) 代码,有没有更好、更方便的方法呢?
    我们先来了解一下 property wrapper 。


    Property Wrapper

    property wrapper 属性包装器,在管理属性如何存储和定义属性的代码之间添加了一层隔离。当使用属性包装器时,你只需在定义属性包装器时编写一次管理代码,然后应用到多个属性上来进行复用。它相当于提供一个特殊的盒子,把属性值包装进去。当你把一个包装器应用到一个属性上时,编译器将合成提供包装器存储空间和通过包装器访问属性的代码。

    例如有个需求,要求属性值不得大于某个数,实现的时候要一个个在属性 set 方法中判断是否大于,然后进行处理,这样很显然很麻烦。这时就可以定义一个属性包装器,在这里进行处理,然后把包装器应用到属性上去,代码如下:

    @propertyWrapper
    struct SmallNumber {
        private var maximum: Int
        private var number: Int
        
        var wrappedValue: Int {
            get { return number }
            set { number = min(newValue, maximum) }
        }
        
        init() {
            maximum = 12
            number = 0
        }
        init(wrappedValue: Int) {
            maximum = 12
            number = min(wrappedValue, maximum)
        }
        init(wrappedValue: Int, maximum: Int) {
            self.maximum = maximum
            number = min(wrappedValue, maximum)
        }
    }
    
    struct SmallRectangle {
        @SmallNumber var height: Int
        @SmallNumber(wrappedValue: 10, maximum: 20) var width: Int
    }
    var rect = SmallRectangle()
    print(rect.height, rect.width) //0 10
    
    rect.height = 30
    print(rect.height) //12
    
    rect.width = 40
    print(rect.width) //20
    
    print(rect)
    //SmallRectangle(_height: SmallNumber(maximum: 12, number: 12), _width: SmallNumber(maximum: 20, number: 20))
    

    上面例子中 SmallNumber 定义了三个构造器,可使用构造器来设置被包装值和最大值, height 不大于 12,width 不大于 20。
    通过打印的内容可看到 _height: SmallNumber(maximum: 12, number: 12),被 SmallNumber 声明的属性,实际上存储的类型是 SmallNumber 类型,只不过编译器进行了处理,对外暴露的类型依然是原来的类型 Int。
    编译器对属性的处理,相当于下面的代码处理方法:

    struct SmallRectangle {
        private var _height = SmallNumber()
        var height: Int {
            get { return _height.wrappedValue }
            set { _height.wrappedValue = newValue }
        }
        //...
    }
    

    将属性 height 包装在 SmallNumber 结构体中,get set 操作的值其实是结构体中 wrappedValue 的值。
    弄清楚这些之后,我们利用属性包装器给属性包装一层,在 Codable 解码的时候操作的是 wrappedValue ,这时我们就可以在属性包装器中进行判断,设置默认值。顺着这个思路下面我们来实现以下。


    设置默认值

    通过前面的分析,大概有了思路,定义一个能够提供默认值的 Default property wrapper ,利用这个 Default 来包装属性,Codable 解码的时候把值赋值 Default 的 wrappedValue,如解码失败就在这里设置默认值。

    初步实现

    初步实现的代码如下:

    @propertyWrapper
    struct Default: Decodable {
        var wrappedValue: Int
        
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            wrappedValue = (try? container.decode(Int.self)) ?? -1
        }
    }
    
    struct Person: Decodable {
        @Default var age: Int
    }
    
    let data = #"{ "age": null}"#
    let p = try JSONDecoder().decode(Person.self, from: data.data(using: .utf8)!)
    print(p, p.age)
    //Person(_age: Default(wrappedValue: -1)) -1
    

    可以看到上面的例子中,JSON 数据为 null,解码到 age 设置了默认值 -1。

    改进代码

    接着我们来改进一下,上面例子只是对 Int 类型的设置了默认值,下面来使用泛型,扩展一下对别的类型支持。
    还有一个问题就是,如果 JSON 中 age 这个 key 缺失的情况下,依然会发生错误,因为我们所使用的解码器默认生成的代码是要求 key 存在的。需要改进一下为 container 重写对于 Default 类型解码的实现。
    改进后的代码如下:

    protocol DefaultValue {
        associatedtype Value: Decodable
        static var defaultValue: Value { get }
    }
    
    @propertyWrapper
    struct Default<T: DefaultValue> {
        var wrappedValue: T.Value
    }
    
    extension Default: Decodable {
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
        }
    }
    
    extension KeyedDecodingContainer {
        func decode<T>(_ type: Default<T>.Type, forKey key: Key) throws -> Default<T> where T: DefaultValue {
            //判断 key 缺失的情况,提供默认值
            (try decodeIfPresent(type, forKey: key)) ?? Default(wrappedValue: T.defaultValue)
        }
    }
    
    
    extension Int: DefaultValue {
        static var defaultValue = -1
    }
    
    extension String: DefaultValue {
        static var defaultValue = "unknown"
    }
    
    struct Person: Decodable {
        @Default<String> var name: String
        @Default<Int> var age: Int
    }
    
    
    let data = #"{ "name": null, "age": null}"#
    let p = try JSONDecoder().decode(Person.self, from: data.data(using: .utf8)!)
    print(p, p.name, p.age)
    //Person(_name: Default<Swift.String>(wrappedValue: "unknown"), _age: Default<Swift.Int>(wrappedValue: -1))
    //unknown  -1
    

    这样如我们需要对某种类型在解码时设置默认值,我们只需要对应的添加个扩展,遵循 DefaultValue 协议,提供一个想要的默认值 defaultValue 即可。
    而且对于 JSON 中 key 缺失的情况,也做了处理,重写了 container.decode() 方法,判断 key 缺失的情况,如 key 缺失,返回默认值。

    设置多种默认值的情况

    有时我们再不同情况下,同种类型的数据需要设置不同的默认值,例如 String 类型的属性,在有的地方默认值需要设置为 "unknown",有的地方则需要设置为 "unnamed",这是我们处理方法如下:

    extension String {
        struct Unknown: DefaultValue {
            static var defaultValue = "unknown"
        }
        struct Unnamed: DefaultValue {
            static var defaultValue = "unnamed"
        }
    }
    
    @Default<String.Unnamed> var name: String
    @Default<String.Unknown> var text: String
    

    这样就实现了不同的情况定义不同的默认值。


    其他问题

    还有一个问题,自定义的数据类型,解码到异常的数据可能导致我们的代码崩溃,还是举之前文章中的例子,枚举类型解析,如下:

    enum Gender: String, Codable {
        case male
        case female
    }
    struct Person: Decodable {
        var gender: Gender
    }
    //{ "gender": "other" }
    

    当 JSON 数据中的 gender 对应的值不在 Gender 枚举的 case 字段中,解码的时候会出现异常,即使 gender 属性是可选的,也会出现异常。要解决这个问题,也可以重写 init(from decoder: Decoder) ,在里面进行判断是否解码异常,然后进行处理。

    相比于使用枚举,其实这里用一个带有 raw value 的 struct 来表示会更好,代码如下:

    struct Gender: RawRepresentable, Codable {
        static let male = Gender(rawValue: "male")
        static let female = Gender(rawValue: "female")
        
        let rawValue: String
    }
    struct XMan: Decodable {
        var gender: Gender
    }
    let mData = #"{ "gender": "other" }"#
    let m = try JSONDecoder().decode(XMan.self, from: mData.data(using: .utf8)!)
    print(m) //XMan(gender: Gender(rawValue: "other"))
    print(m.gender == .male) //false
    

    这样,就算以后为 Gender 添加了新的字符串,现有的实现也不会被破坏,这样也更加稳定。


    References

    https://onevcat.com/2020/11/codable-default/
    https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617
    http://marksands.github.io/2019/10/21/better-codable-through-property-wrappers.html

    相关文章

      网友评论

        本文标题:Swift - Codable 解码设置默认值

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