掘金同步更新: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