这是Swift数据解析方案的系列文章:
Swift数据解析(第四篇) - SmartCodable 上
Swift数据解析(第四篇) - SmartCodable 下
七. 派生关系
1. super.encode(to: encoder)
来看一个这样的场景,我们有一个Point2D的类,包含x和y两个属性,用来标记二维点的位置。 另有继承于Point2D的Point3D,实现三维点定位。
kotlin
class Point2D: Codable {
var x = 0.0
var y = 0.0
}
class Point3D: Point2D {
var z = 0.0
}
1. z 去哪里了?
ini
let point = Point3D()
point.x = 1.0
point.y = 2.0
point.z = 3.0
guard let value = point.encode() else { return }
print(value)
此时的打印结果是:
{
"x" : 1,
"y" : 2
}
相信你已经反应过来了:子类没有重写父类的 encode 方法,默认使用的父类的 encode 方法。子类的属性自然没有被 encode。
2. x 和 y 的去哪了?
我们将代码做这样的修改:
swift
class Point2D: Codable {
var x = 0.0
var y = 0.0
private enum CodingKeys: CodingKey {
case x
case y
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.x, forKey: .x)
try container.encode(self.y, forKey: .y)
}
}
class Point3D: Point2D {
var z = 0.0
enum CodingKeys: CodingKey {
case z
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.z, forKey: .z)
}
}
此时的打印结果是:
{
"z" : 3
}
相信你应该非常清楚这个原因:子类重写了父类的 decode 实现,导致父类的 encode 没有执行。
3. x, y 和 z 都在了
swift
class Point3D: Point2D {
var z = 0.0
enum CodingKeys: CodingKey {
case z
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.z, forKey: .z)
}
}
此时的打印结果是:
{
"x" : 1,
"y" : 2,
"z" : 3
}
在子类的 encode 方法里面调用了父类的 encode 方法,完成了子类和父类的属性编码。
2. super.init(from: decoder)
再将打印的值解码成模型。
swift
class Point2D: Codable {
var x = 0.0
var y = 0.0
enum CodingKeys: CodingKey {
case x
case y
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.x = try container.decode(Double.self, forKey: .x)
self.y = try container.decode(Double.self, forKey: .y)
}
}
class Point3D: Point2D {
var z = 0.0
enum CodingKeys: CodingKey {
case z
case point2D
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.z = try container.decode(Double.self, forKey: .z)
try super.init(from: decoder)
}
}
let json = """
{
"x" : 1,
"y" : 2,
"z" : 3
}
"""
guard let point = json.decode(type: Point3D.self) else { return }
print(point.x)
print(point.y)
print(point.z)
此时的打印结果是:
1.0
2.0
3.0
3. superEncoder & superDecoder
上面的案例中,父类和子类共享一个 container,这不利于我们我们区分。使用superEncoder创建新的容器:
swift
class Point3D: Point2D {
var z = 0.0
enum CodingKeys: CodingKey {
case z
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.z, forKey: .z)
let superEncoder = container.superEncoder()
try super.encode(to: superEncoder)
}
}
输出信息是:
{
"z" : 3,
"super" : {
"x" : 1,
"y" : 2
}
}
同样我们使用superDecoder用来编码:
swift
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.z = try container.decode(Double.self, forKey: .z)
try super.init(from: container.superDecoder())
}
输出信息是:
1.0
2.0
3.0
理解输出信息中的 super 的含义
我们来看一个示例:这是一个班级的信息,其包含班级号,班长,学生成员的信息。
swift
struct Student: Encodable {
var name: String
enum CodingKeys: CodingKey {
case name
}
}
struct Class: Encodable {
var numer: Int
var monitor: Student
var students: [Student]
init(numer: Int, monitor: Student, students: [Student]) {
self.numer = numer
self.monitor = monitor
self.students = students
}
}
let monitor = Student(name: "小明")
let student1 = Student(name: "大黄")
let student2 = Student(name: "小李")
var classFeed = Class(numer: 10, monitor: monitor, students: [student1, student2])
guard let value = classFeed.encode() else { return }
print(value)
// 输出信息是:
{
"numer" : 10,
"monitor" : {
"name" : "小明"
},
"students" : [
{
"name" : "大黄"
},
{
"name" : "小李"
}
]
}
重写Class类的encode方法:
swift
func encode(to encoder: Encoder) throws {
}
// 报错信息:顶级类没有编码任何值
Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level Class did not encode any values.", underlyingError: nil)
只创建容器:
swift
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
}
// 输出信息是:
{
}
当前container的superEncoder
swift
enum CodingKeys: CodingKey {
case numer
case monitor
case students
// 新增一个key
case custom
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var superEncoder = container.superEncoder()
}
// 输出信息是:
{
"super" : {
}
}
键值编码容器 和 无键编码容器的下 的区别
swift
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// 在monitor字典容器中
var monitorContainer = container.nestedContainer(keyedBy: Student.CodingKeys.self, forKey: .monitor)
// 在monitor容器下,新生成一个编码器
let monitorSuperEncoder = monitorContainer.superEncoder()
// 在students数组容器中
var studentsContainer = container.nestedUnkeyedContainer(forKey: .students)
for student in students {
let studentsSuperEncoder = studentsContainer.superEncoder()
}
}
// 打印信息
{
"monitor" : {
"super" : {
}
},
"students" : [
{
},
{
}
]
}
相信你已经体味到 super 的含义了。
在当前层级下生效 : 使用superEncoder 或 superDecoder 在 当前层级下 生成一个新的Encoder 或 Decoder。
由调用者 container 决定生成的结构:
- 如果是 KeyedEncodingContainer 对应的是字典类型,形成 { super: { } } 这样的结构。
- 如果是 UnkeyedEncodingContainer 对应的是数组结构。
- SingleValueEncodingContainer 没有 super。
4. 指定编码父类的key
系统还提供一个方法:
swift
public mutating func superEncoder(forKey key: KeyedEncodingContainer<K>.Key) -> Encoder
我们可以通过调用指定key的方法,创建一个新的编码器:
swift
class Point3D: Point2D {
var z = 0.0
enum CodingKeys: CodingKey {
case z
case point2D
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.z, forKey: .z)
let superEncoder = container.superEncoder(forKey: .point2D)
try super.encode(to: superEncoder)
}
}
// 输出信息是:
{
"z" : 3,
"point2D" : {
"x" : 1,
"y" : 2
}
}
当然我们也可以通过自定义CodingKey实现指定Key:
swift
class Point2D: Codable {
var x = 0.0
var y = 0.0
struct CustomKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
init(_ stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.stringValue = ""
}
}
}
class Point3D: Point2D {
var z = 0.0
enum CodingKeys: CodingKey {
case z
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CustomKeys.self)
try container.encode(self.z, forKey: CustomKeys.init("z"))
let superEncoder = container.superEncoder(forKey: CustomKeys("point2D"))
try super.encode(to: superEncoder)
}
}
八. 枚举
遵循Codable协议的枚举,也可以自动将数据转化为枚举值。
csharp
struct EnumFeed: Codable {
var a: SexEnum
var b: SexEnum
}
enum SexEnum: String, Codable {
case man
case women
}
let json = """
{
"a": "man",
"b": "women"
}
"""
guard let feed = json.decode(type: EnumFeed.self) else { return }
print(feed
1. 枚举映射失败的异常
如果未被枚举的值出现(将数据中b的值改为 "unkown"),decode的时候会抛出DecodingError。 哪怕声明为可选,一样会报错。
yaml
▿ DecodingError
▿ dataCorrupted : Context
▿ codingPath : 1 element
- 0 : CodingKeys(stringValue: "b", intValue: nil)
- debugDescription : "Cannot initialize SexEnum from invalid String value unkown"
- underlyingError : nil
2. 兼容方案
重写init方法,自定义映射
swift
struct EnumFeed: Codable {
var a: SexEnum
var b: SexEnum?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.a = try container.decode(SexEnum.self, forKey: .a)
if try container.decodeNil(forKey: .b) {
self.b = nil
} else {
let bRawValue = try container.decode(String.self, forKey: .b)
if let temp = SexEnum(rawValue: bRawValue) {
self.b = temp
} else {
self.b = nil
}
}
}
}
使用协议提供映射失败的默认值
苹果官方给了一个解决办法: 使用协议提供映射失败的默认值
swift
public protocol SmartCaseDefaultable: RawRepresentable, Codable {
/// 使用接收到的数据,无法用枚举类型中的任何值表示而导致解析失败,使用此默认值。
static var defaultCase: Self { get }
}
public extension SmartCaseDefaultable where Self.RawValue: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(RawValue.self)
self = Self.init(rawValue: rawValue) ?? Self.defaultCase
}
}
使此枚举继承本协议即可
typescript
enum SexEnum: String, SmartCaseDefaultable {
case man
case women
static var defaultCase: SexEnum = .man
}
九. 特殊格式的数据
1. 日期格式
ini
let json = """
{
"birth": "2000-01-01 00:00:01"
}
"""
guard let data = json.data(using: .utf8) else { return }
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
do {
let feed = try decoder.decode(Person.self, from: data)
print(feed)
} catch {
print("Error decoding: (error)")
}
struct Person: Codable {
var birth: Date
}
更多关于dateDecodingStrategy的使用请查看 DateDecodingStrategy 。
java
public enum DateDecodingStrategy : Sendable {
case deferredToDate
case secondsSince1970
case millisecondsSince1970
case iso8601
case formatted(DateFormatter)
@preconcurrency case custom(@Sendable (_ decoder: Decoder) throws -> Date)
}
2. 浮点数
php
let json = """
{
"height": "NaN"
}
"""
struct Person: Codable {
var height: Float
}
guard let data = json.data(using: .utf8) else { return }
let decoder = JSONDecoder()
decoder.nonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "+∞", negativeInfinity: "-∞", nan: "NaN")
do {
let feed = try decoder.decode(Person.self, from: data)
print(feed.height)
} catch {
print("Error decoding: (error)")
}
// 输出: nan
当Float类型遇到 NaN时候,如不做特殊处理,会导致解析失败。可以使用nonConformingFloatDecodingStrategy 兼容不符合json浮点数值当情况。
更多信息请查看:
arduino
public enum NonConformingFloatDecodingStrategy : Sendable {
case `throw`
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
}
3. Data格式
有一个这样的base64的数据
csharp
let json = """
{
"address": "aHR0cHM6Ly93d3cucWl4aW4uY29t"
}
"""
struct QXBWeb: Codable {
var address: Data
}
guard let data = json.data(using: .utf8) else { return }
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
do {
let web = try decoder.decode(QXBWeb.self, from: data)
guard let address = String(data: web.address, encoding: .utf8) else { return }
print(address)
} catch {
print("Error decoding: (error)")
}
// 输出: https://www.qixin.com
更多关于dataDecodingStrategy的信息,请查看DataDecodingStrategy。
less
public enum DataDecodingStrategy : Sendable {
case deferredToData
/// 从base64编码的字符串解码' Data '。这是默认策略。
case base64
@preconcurrency case custom(@Sendable (_ decoder: Decoder) throws -> Data)
}
4. URL格式
Codable可以自动将字符串映射为URL格式。
ini
struct QXBWeb: Codable {
var address: URL
}
let json = """
{
"address": "https://www.qixin.com"
}
"""
guard let data = json.data(using: .utf8) else { return }
let decoder = JSONDecoder()
do {
let web = try decoder.decode(QXBWeb.self, from: data)
print(web.address.absoluteString)
} catch {
print("Error decoding: (error)")
}
但是要注意 数据为 ""的情况。
yaml
▿ DecodingError
▿ dataCorrupted : Context
▿ codingPath : 1 element
- 0 : CodingKeys(stringValue: "address", intValue: nil)
- debugDescription : "Invalid URL string."
- underlyingError : nil
我们来看系统内部是如何进行解码URL的:
swift
if T.self == URL.self || T.self == NSURL.self {
guard let urlString = try self.unbox(value, as: String.self) else {
return nil
}
guard let url = URL(string: urlString) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Invalid URL string."))
}
decoded = (url as! T)
}
解码URL分为两步: 1. urlString是否存在。 2. urlString是否可以转成URL。
可以使用这个方法兼容,URL没法提供默认值,只能设置为可选。
swift
struct QXBWeb: Codable {
var address: URL?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
let str = try container.decode(String.self, forKey: .address)
if let url = URL(string: str) {
self.address = url
} else {
self.address = nil
}
} catch {
print(error)
self.address = nil
}
}
}
十. 关于嵌套结构
除了解码元素,UnkeyedDecodingContainer
和 KeyedDecodingContainer
还提供了一些其他有用的方法,例如nestedContainer(keyedBy:forKey:)
和nestedUnkeyedContainer(forKey:)
,用于解码嵌套的容器类型。
- nestedContainer: 用于生成解码字典类型的容器。
- nestedUnkeyedContainer: 用于生成解码数组类型的容器。
nestedContainer的使用场景
在Swift中,nestedContainer
是一种用于处理嵌套容器的方法。它是KeyedDecodingContainer
和UnkeyedDecodingContainer
协议的一部分,用于解码嵌套的数据结构。
swift
///返回存储在给定键类型的容器中的给定键的数据。
///
/// -参数类型:用于容器的键类型。
/// -参数key:嵌套容器关联的键。
/// -返回:一个KeyedDecodingContainer容器视图。
/// -抛出:' DecodingError. 'typeMismatch ',如果遇到的存储值不是键控容器。
public func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey
当你需要解码一个嵌套的容器时,你可以使用nestedContainer
方法。这个方法接受一个键(对于KeyedDecodingContainer
)或者一个索引(对于UnkeyedDecodingContainer
),然后返回一个新的嵌套容器,你可以使用它来解码嵌套的值。
我们将图左侧的数据,解码到图右侧的Class模型中。 Class 模型对应的数据结构如图所示。
我们先来创建这两个模型: Class 和 Student
swift
struct Student: Codable {
let id: String
let name: String
}
struct Class: Codable {
var students: [Student] = []
// 数据中的key结构和模型中key结构不对应,需要自定义Key
struct CustomKeys: CodingKey {
var intValue: Int? {return nil}
var stringValue: String
init?(intValue: Int) {return nil}
init?(stringValue: String) {
self.stringValue = stringValue
}
init(_ stringValue: String) {
self.stringValue = stringValue
}
}
}
Class是一个模型,所以对应的应该是一个KeyedDecodingContainer容器。
swift
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomKeys.self)
let keys = container.allKeys
var list: [Student] = []
for key in keys {
let nestedContainer = try container.nestedContainer(keyedBy: CustomKeys.self, forKey: key)
let name = try nestedContainer.decode(String.self, forKey: .init("name"))
let student = Student(id: key.stringValue, name: name)
list.append(student)
}
self.students = list
}
container.allKeys的值为:
php
▿ 3 elements
▿ 0 : CustomKeys(stringValue: "2", intValue: nil)
- stringValue : "2"
▿ 1 : CustomKeys(stringValue: "1", intValue: nil)
- stringValue : "1"
▿ 2 : CustomKeys(stringValue: "3", intValue: nil)
- stringValue : "3"
通过遍历allKeys得到的key,对应的数据结构是一个字典: { "name" : xxx }。
php
let nestedContainer = try container.nestedContainer(keyedBy: CustomKeys.self, forKey: key)
let name = try nestedContainer.decode(String.self, forKey: .init("name"))
let student = Student(id: key.stringValue, name: name)
这样就得Student的全部数据。id的值就是key.stringValue,name的值就是nestedContainer容器中key为"name"的解码值。
根据这个思路,我们也可以很容易的完成 encode 方法
swift
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CustomKeys.self)
for student in students {
var keyedContainer = container.nestedContainer(keyedBy: CustomKeys.self, forKey: .init(student.id))
try keyedContainer.encode(student.name, forKey: .init("name"))
}
}
nestedUnkeyedContainer的使用场景
nestedUnkeyedContainer
是 Swift 中的 KeyedDecodingContainer
协议的一个方法,它允许你从嵌套的无键容器中解码一个值的数组,该容器通常是从 JSON 或其他编码数据中获取的。
swift
/// 返回为给定键存储的数据,以无键容器的形式表示。
public func nestedUnkeyedContainer(forKey key: KeyedDecodingContainer<K>.Key) throws -> UnkeyedDecodingContainer
有一个这样的数据结构,解码到 Person 模型中:
rust
let json = """
{
"name": "xiaoming",
"age": 10,
"hobbies": [
{
"name": "basketball",
"year": 8
},
{
"name": "soccer",
"year": 2
}
]
}
"""
struct Person {
let name: String
let age: Int
var hobbies: [Hobby]
struct Hobby {
let name: String
let year: Int
}
}
使用系统自带嵌套解析能力
rust
struct Person: Codable {
let name: String
let age: Int
var hobbies: [Hobby]
struct Hobby: Codable {
let name: String
let year: Int
}
}
使用自定义解码
swift
struct Person: Codable {
let name: String
let age: Int
var hobbies: [Hobby]
struct Hobby: Codable {
let name: String
let year: Int
enum CodingKeys: CodingKey {
case name
case year
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
// 解码嵌套的数组
var nestedContainer = try container.nestedUnkeyedContainer(forKey: .hobbies)
var tempHobbies: [Hobby] = []
while !nestedContainer.isAtEnd {
if let hobby = try? nestedContainer.decodeIfPresent(Hobby.self) {
tempHobbies.append(hobby)
}
}
hobbies = tempHobbies
}
}
使用完全自定义解码
swift
struct Person: Codable {
let name: String
let age: Int
var hobbies: [Hobby]
struct Hobby: Codable {
let name: String
let year: Int
enum CodingKeys: CodingKey {
case name
case year
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
var hobbiesContainer = try container.nestedUnkeyedContainer(forKey: .hobbies)
var tempItems: [Hobby] = []
while !hobbiesContainer.isAtEnd {
let hobbyContainer = try hobbiesContainer.nestedContainer(keyedBy: Hobby.CodingKeys.self)
let name = try hobbyContainer.decode(String.self, forKey: .name)
let year = try hobbyContainer.decode(Int.self, forKey: .year)
let item = Hobby(name: name, year: year)
tempItems.append(item)
}
hobbies = tempItems
}
}
十一. 其他一些小知识点
1. 模型中属性的didSet方法不会执行
swift
struct Feed: Codable {
init() {
}
var name: String = "" {
didSet {
print("被set了")
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
}
}
let json = """
{
"name": "123"
}
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
在Swift中,使用Codable解析完成后,模型中的属性的didSet方法不会执行。
这是因为didSet方法只在属性被直接赋值时触发,而不是在解析过程中。
Codable协议使用编码和解码来将数据转换为模型对象,而不是通过属性的直接赋值来触发didSet方法。
如果您需要在解析完成后执行特定的操作,您可以在解析后手动调用相应的方法或者使用自定义的初始化方法来处理。