Swift4踩坑之Codable协议
WWDC 过去有⼀段时间了,最近终于有时间空闲,可以静下⼼来仔细研究⼀下相关内容。对于开发者来说,本届WWDC 最重要的消息还是得属 Swift 4 的推出。
Swift 经过三年的发展,终于在 API 层⾯趋于稳定。从 Swift 3 迁移代码到 Swift 4 终于不⽤像 2 到 3 那样痛苦了。这对开发者来说实在是个重⼤利好,应该会吸引⼀⼤批对 Swift 仍然处于观望状态的开发者加⼊。
另外 Swift 4 引⼊了许多新的特性,像是 fileprivate 关键字的限制范围更加精确了;声明属性终于可以同时限制类型和协议了;新的KeyPath API 等等,从这些改进我们可以看到,Swift 的⽣态越来越完善,Swift 本⾝也越来越强⼤。
⽽ Swift 4 带来的新特性中,最让⼈眼前⼀亮的,我觉得⾮ Codable 协议莫属,下⾯就来介绍下我⾃⼰对 Codable 协议踩坑的经验总结。
简单介绍
Swift 由于类型安全的特性,对于像 JSON 这类弱类型的数据处理⼀直是⼀个⽐较头疼的问题,虽然市⾯上许多优秀的第三⽅库在这⽅⾯做了不少努⼒,但是依然存在着很多难以克服的缺陷,所以 Codable
协议的推出,⼀来打破了这样的僵局,⼆来也给我们解决类似问题提供了新的思路。
通过查看定义可以看到,Codable 其实是⼀个组合协议,由 Decodable 和 Encodable 两个协议组成:
/// A type that can convert itself into and out of an external representation.
public typealias Codable = Decodable & Encodable
/// A type that can encode itself to an external representation.
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
/// A type that can decode itself from an external representation.
public protocol Decodable {
public init(from decoder: Decoder) throws
}
复制代码
Encodable 和 Decodable 分别定义了 encode(to:) 和 init(from:) 两个协议函数,分别⽤来实现数据模型的归档和外部数据的解析和实例化。最常⽤的场景就是接⼝ JSON 数据解析和模型创建。但是 Codable 的能⼒并不⽌于此,这个后⾯会说。
解析 JSON 对象
先来看 Decodable 对 JSON 数据对象的解析。Swift 为我们做了绝⼤部分的⼯作,Swift 中的基本数据类型⽐如 String、Int、Float 等都已经实现了 Codable 协议,因此如果你的数据类型只包含这些基本数据类型的属性,只需要在类型声明中加上 Codable 协议就可以了,不需要写任何实际实现的代码,这也是 Codable 最⼤的优势所在。
⽐如我们有下⾯这样⼀个学⽣信息的 JSON 字符串:
let jsonString =
"""
{
"name": "⼩明",
"age": 12,
"weight": 43.2
}
"""
复制代码
这时候,只需要定义⼀个 Student 类型,声明实现 Decodable 协议即可,Swift 4 已经为我们提供了默认的实现:
struct Student: Decodable {
var name: String
var age: Int
var weight: Float
}
复制代码
然后,只需要⼀⾏代码就可以将 ⼩明 解析出来了:
let xiaoming = try JSONDecoder().decode(Student.self, from: jsonString.data(using: .utf8)!)
复制代码
这⾥需要注意的是, decode 函数需要外部数据类型为 Data 类型,如果是字符串需要先转换为 Data 之后操作,不过像 之类的⽹络框架,返回数据原本就是 Data 类型的。 另外 decode 函数是标记为 throws 的,如果解析失败,会抛出⼀个异常,为了保证程序的健壮性,需要使⽤ do-catch 对异常情况进⾏处理:
do {
let xiaoming = try JSONDecoder().decode(Student.self, from: data)
json值的类型有哪些
} catch {
// 异常处理
}
复制代码
特殊数据类型
很多时候光靠基本数据类型并不能完成⼯作,往往我们需要⽤到⼀些特殊的数据类型。Swift 对许多特殊数据类型也提供了默认的 Codable 实现,但是有⼀些限制。
枚举
{
...
"gender": "male"
...
}
复制代码
性别是⼀个很常⽤的信息,我们经常会把它定义成枚举:
enum Gender {
case male
case female
case other
}
复制代码
枚举类型也默认实现了 Codable 协议,但是如果我们直接声明 Gender 枚举⽀持 Codable 协议,编译器会提⽰没有提供实现:
其实这⾥有⼀个限制:枚举类型要默认⽀持 Codable 协议,需要声明为具有原始值的形式,并且原始值的类型需要⽀持 Codable 协议:
enum Gender: String, Decodable {
case male
case female
case other
}
复制代码
由于枚举类型原始值隐式赋值特性的存在,如果枚举值的名称和对应的 JSON 中的值⼀致,不需要显式指定原始值即可完成解析。
Bool
我们的数据模型现在新增了⼀个字段,⽤来表⽰某个学⽣是否是少先队员:
{
...
"isYoungPioneer": true
...
}
复制代码
这时候,直接声明对应的属性就可以了:
var isYoungPioneer: Bool
复制代码
Bool 类型原本没什么好讲的,不过因为踩到了坑,所以还是得说⼀说: ⽬前发现的坑是:Bool 类型默认只⽀持 true/false 形式的 Bool 值解析。对于⼀些使⽤ 0/1 形式来表⽰ Bool 值的后端框架,只能通过 Int 类型解析之后再做转换了,或者可以⾃定义实现 Codable 协议。
⽇期解析策略
说了枚举和 Bool,另外⼀个常⽤的特殊类型就是 Date 了,Date 类型的特殊性在于它有着各种各样的格式标准和表⽰⽅式,从数字到字符串可以说是五花⼋门,解析 Date 类型是任何⼀个同类型的框架都必须⾯对的课题。
对此,Codable 给出的解决⽅案是:定义解析策略。JSONDecoder 类声明了⼀个 DateDecodingStrategy 类型的属性,⽤来制定 Date 类型的解析策略,同样先看定义:
/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
case deferredToDate
/// Decode the `Date` as a UNIX timestamp from a JSON number.
case secondsSince1970
/
// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
case millisecondsSince1970
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
case iso8601
/// Decode the `Date` as a string parsed by the given formatter.
case formatted(DateFormatter)
/// Decode the `Date` as a custom value decoded by the given closure.
case custom((Decoder) throws -> Date)
}
复制代码
Codable 对⼏种常⽤格式标准进⾏了⽀持,默认启⽤的策略是 deferredToDate,即从 **UTC 时间200
1年1⽉1⽇ **开始的秒数,对应Date 类型中 timeIntervalSinceReferenceDate 这个属性。⽐如 519751611.125429 这个数字解析后的结果是 2017-06-21 15:26:51
+0000。
另外可选的格式标准有 secondsSince1970、millisecondsSince1970、 等,这些都是有详细说明的通⽤标准,不清楚的⾃⾏⾕歌吧 :)
同时 Codable 提供了两种⽅⾃定义 Date 格式的策略:
formatted(DateFormatter) 这种策略通过设置 DateFormatter 来指定 Date 格式
custom((Decoder) throws -> Date)custom 策略接受⼀个 (Decoder) -> Date 的闭包,基本上是把解析任务完全丢给我们⾃⼰去实现了,具有较⾼的⾃由度
⼩数解析策略
⼩数类型(Float/Double) 默认也实现了 Codable 协议,但是⼩数类型在 Swift 中有许多特殊值,⽐如圆周率(Float.pi)等。这⾥要说的是另外两个属性,先看定义:
/// Positive infinity.
///
/// Infinity compares greater than all finite numbers and equal to other
/// infinite values.
public static var infinity: Double { get }
/// A quiet NaN ("not a number").
///
/// A NaN compares not equal, not greater than, and not less than every
/// value, including itself. Passing a NaN to an operation generally results
/// in NaN.
public static var nan: Double { get }
复制代码
infinity 表⽰正⽆穷(负⽆穷写作:-infinity),nan 表⽰没有值,这些特殊值没有办法使⽤数字进⾏表⽰,但是在 Swift 中它们是确确实实的值,可以参与计算、⽐较等。 不同的语⾔、框架对此会有类似的实现,但是表达⽅式可能不完全相同,因此如果在某些场景下需要解析这样的值,就需要做特殊转换了。
Codable 的实现⽅式⽐较简单粗暴,JSONDecoder 类型有⼀个属性 nonConformingFloatDecodingStrategy ,⽤来指定不⼀致的⼩数转换策略,默认值为 throw, 即直接抛出异常,解析失败。另外⼀个选择就是⾃⼰指定 infinity、-infinity、nan 三个特殊值的表⽰⽅式:
let decoder = JSONDecoder()
// 另外⼀种表⽰⽅式
// ConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "∞", negativeInfinity: "-∞", nan: "n/a")
复制代码
⽬前看来只⽀持这三个特殊值的转换,不过这种特殊值的使⽤场景应该⾮常有限,⾄少在我⾃⼰五六年的开发⽣涯中还没有遇到过。
⾃定义数据类型
纯粹的基本数据类型依然不能很好地⼯作,实际项⽬的数据结构往往是很复杂的,⼀个数据类型经常会包含另⼀个数据类型的属性。⽐如说我们这个例⼦中,每个学⽣信息中还包含了所在学校的信息:
{
"name": "⼩明",
"age": 12,
"weight": 43.2
"school": {
"name": "市第⼀中学",
"address": "XX市⼈民中路 66 号"
}
}
复制代码
这时候就需要 Student 和 School 两个类型来组合表⽰:
struct School: Decodable {
var name: String
var address: String
}
struct Student: Decodable {
var name: String
var age: Int
var weight: Float
var school: School
}
复制代码
由于所有基本类型都实现了 Codable 协议,因此 School 与 Student ⼀样,只要所有属性都实现了 Codable 协议,就不需要⼿动提供任何实现即可获得默认的 Codable 实现。由于 School ⽀持了 Codable 协议,保证了 Student 依然能够获得默认的 Codable 实现,因此,嵌套类型的解析同样不需要额外的代码了。
⾃定义字段
很多时候前后端不⼀定能完全步调⼀致,观念相同。所以往往后端给出的数据结构中会有⼀些⽐较个性的字段名,当然有时候是我们⾃⼰。另外有⼀些框架(⽐如我正在⽤的 Laravel)习惯使⽤蛇形命名法,⽽ iOS 的代码规范推荐使⽤驼峰命名法,为了保证代码风格和平台特⾊,这时候就必须要⾃⾏指定字段名了。
在研究⾃定义字段之前我们需要深⼊底层,了解下 Codable 默认是怎么实现属性的名称识别及赋值的。通过研究底层的 C++ 源代码可以发现,Codable 通过巧(kai)妙(guà)的⽅式,在编译代码时根据类型的属性,⾃动⽣成了⼀个 CodingKeys 的枚举类型定义,这是⼀个以 String 类型作为原始值的枚举类型,对应每⼀个属性的名称。然后再给每⼀个声明实现 Codable 协议的类型⾃动⽣成 init(from:) 和encode(to:) 两个函数的具体实现,最终完成了整个协议的实现。
所以我们可以⾃⼰实现 CodingKeys 的类型定义,并且给属性指定不同的原始值来实现⾃定义字段的解析。这样编译器会直接采⽤我们已经实现好的⽅案⽽不再重新⽣成⼀个默认的。
⽐如 Student 需要增加⼀个出⽣⽇期的属性,后端接⼝使⽤蛇形命名,JSON 数据如下:
{
"name": "⼩明",
"age": 12,
"weight": 43.2
"birth_date": "1992-12-25"
}
复制代码
这时候在 Student 类型声明中需要增加 CodingKeys 定义,并且将 birthday 的原始值设置为 birth_date:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论