Swift - Codable 解碼設(shè)置默認值

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刁憋,一起剝皮案震驚了整個濱河市滥嘴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌至耻,老刑警劉巖若皱,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異尘颓,居然都是意外死亡走触,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門疤苹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來互广,“玉大人,你說我怎么就攤上這事卧土《荡牵” “怎么了迎瞧?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逸吵。 經(jīng)常有香客問我凶硅,道長,這世上最難降的妖魔是什么扫皱? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任足绅,我火速辦了婚禮,結(jié)果婚禮上韩脑,老公的妹妹穿的比我還像新娘氢妈。我一直安慰自己,他們只是感情好段多,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布首量。 她就那樣靜靜地躺著,像睡著了一般进苍。 火紅的嫁衣襯著肌膚如雪加缘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天觉啊,我揣著相機與錄音拣宏,去河邊找鬼。 笑死杠人,一個胖子當著我的面吹牛勋乾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嗡善,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辑莫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了罩引?” 一聲冷哼從身側(cè)響起摆昧,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蜒程,沒想到半個月后绅你,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡昭躺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年迷雪,在試婚紗的時候發(fā)現(xiàn)自己被綠了木缝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖湖雹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布脚猾,位于F島的核電站,受9級特大地震影響砚哗,放射性物質(zhì)發(fā)生泄漏龙助。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一蛛芥、第九天 我趴在偏房一處隱蔽的房頂上張望提鸟。 院中可真熱鬧,春花似錦仅淑、人聲如沸称勋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赡鲜。三九已至,卻和暖如春庐船,著一層夾襖步出監(jiān)牢的瞬間银酬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工醉鳖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捡硅,地道東北人哮内。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓盗棵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親北发。 傳聞我的和親對象是個殘疾皇子纹因,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容