美文网首页
Codable 学习

Codable 学习

作者: 游城十代2dai | 来源:发表于2023-11-12 16:52 被阅读0次

0x00 Codable 介绍

Codable是从Swift 4开始加入到Swift的标准库组件,其提供了一种非常简单的方式支持模型和数据之间的转换。

当时官方发布的时候就说过,Codable的设计目标有三个:通用性、类型安全性以及减少编码过程的模板代码。

0x01 为什么要去学习 Codable

  1. 解决序列化与反序列化
  2. 替代现有基于 ABI 不稳定的方案
    • SwiftJSON
    • HandyJSON
    • KakaJSON

Swift发布4.0版本之前,官方未提供推荐的 JSON 处理方案,因此我们项目使用了HandyJSON 这套方案

但是, HandyJSON 的实现强依赖于Swift底层内存布局机制,这个机制是非公开、不被承诺、且实践证明一直在随着Swift 版本变动的,HandyJSON 需要跟进 Swift 的每次版本更新,更大的风险是,用户升级 iOS 版本可能会影响这个依赖,导致应用逻辑异常

0x02 怎么样使用 Codable

代码如下:

// 定义一个模型, 支持 Codable 协议
struct Person: Codable {
    let name: String
    let age: Int
    var test: Int?
}

// 解码 JSON 数据
let json = #" {"name":"Tom", "age": 2} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)

print(person)

print("\n")

// 编码导出为 JSON 数据
let data0 = try? JSONEncoder().encode(person)
let dataObject = try? JSONSerialization.jsonObject(with: data0!)
print(dataObject ?? "nil")

print("\n")

let data1 = try? JSONSerialization.data(withJSONObject: ["name": person.name, "age": person.age])
print(String(data: data1!, encoding: .utf8)!)

输出结果如下:

Person(name: "Tom", age: 2, test: nil)


{
    age = 2;
    name = Tom;
}


{"name":"Tom","age":2}

0x03 分析与讨论

上面的使用方式是一个最简单的最直接的 Codable 引用, 其实 Codable 还有很多使用方式以及问题

自定义 Key

使用以前的序列化工具都需要考虑的是自定义 Key, 比如服务器给的是 { "first_name": "Tom" }, 但是 APP 习惯是驼峰命名, 这时候就需要自定义 Key 了

当然只是驼峰命名的话, 系统有封装 decorder.keyDecodingStrategy = .convertFromSnakeCase 即可实现, 后面的嵌套例子会用到, 其他的自定义 Key 就要自己实现了

struct Person: Codable {
    let name: String
    let age: Int
    let firstName: String
    
    enum CodingKeys: String, CodingKey {
        case name, age
        case firstName = "first_name"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        age = try values.decode(Int.self, forKey: .age)
        firstName = try values.decode(String.self, forKey: .firstName)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(firstName, forKey: .firstName)
    }
}

let data = #"{"name": "Tom", "age": 10, "first_name": "James"}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
} catch {
    print(error)
}

Encoder 的三个接口

上面的 func encode(to encoder: Encoder) 里面使用了其中一个, 以下是三个接口

如果模型想要 key -> value, 就使用
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey 接口作为容器, 这样 encode 的结果就是一个可以转换成字符串的 json data

如果模型想要忽略 key 值, 以 value 组成数组的方式 encode , 就使用 func unkeyedContainer() -> UnkeyedEncodingContainer 接口作为容器, 这样的 encode 就是一个当前层级为数组的 data

如果模型只想要在 encode 的时候保留其中一个值或者只有一个值的时候, 使用func singleValueContainer() -> SingleValueEncodingContainer 接口做为容器, 这样 encode 的就是一个单一结果

// 还是上面的代码

// 控制台输出 encode 结果, {"name": "Tom", "age": 10, "first_name": "James"}
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(age, forKey: .age)
    try container.encode(firstName, forKey: .firstName)
}

// 控制台输出 encode 结果, ["Tom",10,"James"]
func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(name)
    try container.encode(age)
    try container.encode(firstName)
}

// 控制台输出 encode 结果, "Tom"
func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(name)
}

Embed 嵌套类型

最常见的数据结构, 一个模型内有多个模型或者模型数组存在, 只要实现了 Codable 协议, 系统会自动为我们完成嵌套内容, 每一层只需要关心自己的 Codable 实现即可

struct Person: Codable, CustomStringConvertible {
    let name: String
    let age: Int
    
    var description: String {
        "name: \(name) age: \(age)"
    }
}

struct Family: Codable, CustomStringConvertible {
    let familyName: String
    let persons: [Person]
    
    var description: String {
        "familyName: \(familyName)\npersons: \(persons)"
    }
}


let data = """
{
    "family_name": "101",
    "persons":[
          {
             "name": "小明",
             "age": 1
          },
          {
             "name": "小红",
             "age": 1
          }
    ]
}
""".data(using: .utf8)!

do {
    let decorder = JSONDecoder()
    decorder.keyDecodingStrategy = .convertFromSnakeCase
    let family = try decorder.decode(Family.self, from: data)
    print(family)
} catch {
    print(error)
}

输出结果为:

familyName: 101
persons: [name: 小明 age: 1, name: 小红 age: 1]

支持日期格式

只要满足 formatter 格式的都会自动转换

struct Person: Codable {
    let birthday: Date
}

//let data = """
//{
//    "birthday": "2022-10-20T14:15:00-0000"
//}
//""".data(using: .utf8)!

//let data = """
//{
//    "birthday": 1666182937
//}
//""".data(using: .utf8)!

let data = """
{
    "birthday": "2022-10-19 20:35:37.000000"
}
""".data(using: .utf8)!

do {
    // create a date formatter
    let dateFormatter = DateFormatter()
    // set time zone
    dateFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai") ?? .current
    // setup formate string for the date formatter
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS"
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(dateFormatter)

    let person = try decoder.decode(Person.self, from: data)
    print(person)
    
    print(dateFormatter.string(from: person.birthday))
} catch {
    print(error)
}

网络请求中序列化问题

网络请求都会有能用枚举表示的, Swift 的枚举和 OC 的不一样, 初始化不了就是 nil, 所以下面的代码会报错, dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "stageId", intValue: nil)], debugDescription: "Cannot initialize StageIDType from invalid Int value 99999", underlyingError: nil))

struct Person: Codable, CustomStringConvertible {
    enum Gender: Int, Codable {
        case male
        case female
    }

    enum StageIDType: Int, Codable {
        case preSchool = 9999
        case primary = 10001
        case junior = 10002
        case senior = 10003
    }

    let name: String
    var gender: Gender = .male
    var stageId: StageIDType = .junior

    var description: String {
        "name: \(name)\ngender: \(gender)\nstageId: \(stageId)"
    }
}

// 不给予默认值, 会报数据错误
// 给予默认值, 依旧会数据错误
let data = #"{"name": "123", "gender": 1, "stageId": 99999}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
} catch {
    print(error)
}

Codable 中意外值

上面的错误, 可以对枚举做二次封装来匹配, 但是这样的封装使用起来会很费事, 需要 switch 等方式取值, 变通一下这种意外方式, 使用结构体+静态属性

struct Person: Codable, CustomStringConvertible {
    enum Gender: Int, Codable {
        case male
        case female
    }
    
    struct StageIDType: RawRepresentable, Codable {
        typealias RawValue = Int
        
        let rawValue: RawValue
        
        static let preSchool: StageIDType = .init(rawValue: 9999)
        static let primary: StageIDType = .init(rawValue: 1001)
        static let junior: StageIDType = .init(rawValue: 1002)
        static let senior: StageIDType = .init(rawValue: 1003)
    }
    
    let name: String
    let gender: Gender
    let stageId: StageIDType
    
    var description: String {
        "name: \(name)\ngender: \(gender)\nstageId: \(stageId)"
    }
}

let data = #"{"name": "123", "gender": 1, "stageId": 1004}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
    let data = try JSONEncoder().encode(person)
    print(String(data: data, encoding: .utf8)!)
} catch {
    print(error)
}

输出如下:

name: 123
gender: female
stageId: StageIDType(rawValue: 1004)
{"name":"123","stageId":1004,"gender":1}

Codable 中默认值

在开发的时候, 默认值也很重要, 这里可以考虑用属性包装器, 做一下封装来用, 只是一个思想, 关于 Codable 封装好的三方库也是有一些的, 至于用得上用不上就看开发人员自己选择吧, 我们项目当中暂时还没有这种需求

struct Person: Codable {
    enum Gender: Int, Codable {
        case male
        case female
    }
    
    struct StageIDType: RawRepresentable, Codable {
        typealias RawValue = Int
        
        let rawValue: RawValue
        
        static let preSchool: StageIDType = .init(rawValue: 9999)
        static let primary: StageIDType = .init(rawValue: 1001)
        static let junior: StageIDType = .init(rawValue: 1002)
        static let senior: StageIDType = .init(rawValue: 1003)
    }
    
    let name: String
    let gender: Gender
    let stageId: StageIDType
    
    @Default<Bool>(true)
    var canPlayBall: Bool
}

protocol DefaultValuable {
    associatedtype Value: Codable
    static var defaultValue: Value { get }
}

extension Bool: DefaultValuable {
    static let defaultValue = true
}

extension Int: DefaultValuable {
    static var defaultValue: Int {
        100
    }
}

@propertyWrapper
struct Default<T: DefaultValuable>: Codable {
    var wrappedValue: T.Value
    
    init(_ wrappedValue: T.Value) {
        self.wrappedValue = wrappedValue
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

extension KeyedDecodingContainer {
    func decode<T>(
        _ type: Default<T>.Type,
        forKey key: Key
    ) throws -> Default<T> where T: DefaultValuable {
        return try decodeIfPresent(type, forKey: key) ?? Default(T.defaultValue)
    }
}

let data = #"{"name": "123", "gender": 1, "stageId": 1001, "canPlayBall": "12"}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
    
    let person1 = Person(name: "dsad", gender: .male, stageId: .senior)
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let data = try encoder.encode(person1)
    let json = String(data: data, encoding: .utf8)!
    print(json)
} catch {
    print(error)
}

输出结果如下:

Person(name: "123", gender: __lldb_expr_119.Person.Gender.female, stageId: __lldb_expr_119.Person.StageIDType(rawValue: 1001), _canPlayBall: __lldb_expr_119.Default<Swift.Bool>(wrappedValue: true))
{
  "gender" : 0,
  "stageId" : 1003,
  "name" : "dsad",
  "canPlayBall" : true
}

0x04 总结

有兴趣的可以看看下面引用中的源码链接, 里面的代码很好, 其中 Codable.swift 实现了接口协议以及基础数据类型的 encoder decoder 的默认实现, JSONEncoder.swift 实现了具体功能

通过反射和内存操作的那些库, 比如 HandyJSON 优势是可以设置默认值, 模型定义的 key 在 json 中不存在不会报错, 会忽略, 而 Codable 需要可选值才能标识忽略, Codable 不自定义 decode 都加问号(基础数据类型), 或者自定义 decode 并添加默认值

利用系统提供的便利性, 尽量在 Codable 处使用嵌套并拥有基础数据类型, 这样编译器会在编译的时候生成模版代码

使用了 Codable 就尽量不要使用字典, Codable 意味着具象类型要以对象为单位, toJSONObject 的方式只用来给服务器上传即可

多态的时候尽量使用协议来实现映射


引用:

Codable 使用小记

用 Codable 协议实现快速 JSON 解析

Codable源码剖析

源码解读——Codable

Property Wrapper 为 Codable 解码设定默认值

如何优雅的使用Swift Codable协议

Codable保姆级攻略

Codable.swift 源码

JSONEncoder.swift 源码

相关文章

网友评论

      本文标题:Codable 学习

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