掘金同步更新:https://juejin.cn/user/3378158048121326/posts
上一篇 Swift - Codable 使用小記 文章中介紹了 Codable 的使用,它能夠把 JSON 數(shù)據(jù)轉(zhuǎn)換成 Swift 代碼中使用的類型站绪。本文來進一步研究使用 Codable 解碼如何設(shè)置默認值的問題慌盯。
解碼遇到的問題
之前的文章中提到了,遇到 JSON 數(shù)據(jù)中字段為空的情況撰茎,把屬性設(shè)置為可選的汤求,當返回為空對象或 null 時挨稿,解析為 nil旭绒。
當我們希望字段為空時,對應(yīng)的屬性要設(shè)置一個默認值焦人,我們處理的一種方法是重寫 init(from decoder: Decoder) 方法挥吵,在 decodeIfPresent 判斷設(shè)置默認值,代碼如下:
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 屬性包裝器,在管理屬性如何存儲和定義屬性的代碼之間添加了一層隔離袋倔。當使用屬性包裝器時雕蔽,你只需在定義屬性包裝器時編寫一次管理代碼,然后應(yīng)用到多個屬性上來進行復(fù)用宾娜。它相當于提供一個特殊的盒子批狐,把屬性值包裝進去。當你把一個包裝器應(yīng)用到一個屬性上時前塔,編譯器將合成提供包裝器存儲空間和通過包裝器訪問屬性的代碼嚣艇。
例如有個需求,要求屬性值不得大于某個數(shù)华弓,實現(xiàn)的時候要一個個在屬性 set 方法中判斷是否大于食零,然后進行處理,這樣很顯然很麻煩寂屏。這時就可以定義一個屬性包裝器贰谣,在這里進行處理,然后把包裝器應(yīng)用到屬性上去迁霎,代碼如下:
@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 定義了三個構(gòu)造器冈爹,可使用構(gòu)造器來設(shè)置被包裝值和最大值, height 不大于 12欧引,width 不大于 20频伤。
通過打印的內(nèi)容可看到 _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 結(jié)構(gòu)體中鸵膏,get set 操作的值其實是結(jié)構(gòu)體中 wrappedValue 的值。
弄清楚這些之后怎炊,我們利用屬性包裝器給屬性包裝一層谭企,在 Codable 解碼的時候操作的是 wrappedValue ,這時我們就可以在屬性包裝器中進行判斷评肆,設(shè)置默認值债查。順著這個思路下面我們來實現(xiàn)以下。
設(shè)置默認值
通過前面的分析瓜挽,大概有了思路盹廷,定義一個能夠提供默認值的 Default property wrapper ,利用這個 Default 來包裝屬性久橙,Codable 解碼的時候把值賦值 Default 的 wrappedValue俄占,如解碼失敗就在這里設(shè)置默認值。
初步實現(xiàn)
初步實現(xiàn)的代碼如下:
@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 數(shù)據(jù)為 null缸榄,解碼到 age 設(shè)置了默認值 -1。
改進代碼
接著我們來改進一下祝拯,上面例子只是對 Int 類型的設(shè)置了默認值碰凶,下面來使用泛型,擴展一下對別的類型支持鹿驼。
還有一個問題就是欲低,如果 JSON 中 age 這個 key 缺失的情況下,依然會發(fā)生錯誤畜晰,因為我們所使用的解碼器默認生成的代碼是要求 key 存在的砾莱。需要改進一下為 container 重寫對于 Default 類型解碼的實現(xiàn)。
改進后的代碼如下:
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
這樣如我們需要對某種類型在解碼時設(shè)置默認值腊瑟,我們只需要對應(yīng)的添加個擴展,遵循 DefaultValue 協(xié)議块蚌,提供一個想要的默認值 defaultValue 即可闰非。
而且對于 JSON 中 key 缺失的情況,也做了處理峭范,重寫了 container.decode() 方法财松,判斷 key 缺失的情況,如 key 缺失,返回默認值辆毡。
設(shè)置多種默認值的情況
有時我們再不同情況下菜秦,同種類型的數(shù)據(jù)需要設(shè)置不同的默認值,例如 String 類型的屬性舶掖,在有的地方默認值需要設(shè)置為 "unknown"球昨,有的地方則需要設(shè)置為 "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
這樣就實現(xiàn)了不同的情況定義不同的默認值眨攘。
其他問題
還有一個問題主慰,自定義的數(shù)據(jù)類型,解碼到異常的數(shù)據(jù)可能導(dǎo)致我們的代碼崩潰鲫售,還是舉之前文章中的例子共螺,枚舉類型解析,如下:
enum Gender: String, Codable {
case male
case female
}
struct Person: Decodable {
var gender: Gender
}
//{ "gender": "other" }
當 JSON 數(shù)據(jù)中的 gender 對應(yīng)的值不在 Gender 枚舉的 case 字段中龟虎,解碼的時候會出現(xiàn)異常,即使 gender 屬性是可選的沙庐,也會出現(xiàn)異常鲤妥。要解決這個問題,也可以重寫 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 添加了新的字符串鹊汛,現(xiàn)有的實現(xiàn)也不會被破壞蒲赂,這樣也更加穩(wěn)定。
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