在编程中,类型准确是一件必要的事情,一方面保证了大家的理解,另一方面能方便地使用 API。
前言
很多人习惯用 String 代替 URL,但是 URL 并不等同于 String。使用 String 作为类型,不能快速获取 scheme / host / path 等属性,因为这些属性并不是 String 的必要定义,而是 URL 的。
又如,使用数字作为分类,第一反应就是「这是什么东西」,写代码时还得去翻文档注释,远不如枚举(常量)来得直观。
后端由于历史遗留问题,使用了数字作为分类,或者新接口已经使用字符串作为分类,客户端都需要定义强类型的 model,例如 Enum with RawValue,既表意准确,也便于比较。
Codable
Swift 4.0 中增加的 Codable 协议,大大减少了 Model 层的代码量。依赖于编译器的自动合成,如果 model 的成员变量全部实现了 Codable 协议,那么 model 本身声明即实现 Codable 协议,不需要重写 Codable 所需要的方法。
尽管如此方便,WebAPI 的不规范仍然会导致客户端出现这样那样的问题:
- Optional,变量声明非 Optional, WebAPI 返回 null 即 decode 失败。
- URL,WebAPI 返回空字符串即 decode 失败。
- Enum with RawValue,WebAPI 返回新值(客户端版本未定义值)即 decode 失败。
还有 JSONDecoder 本身设计导致的问题:
- 不重写
init(from:)
的情况下,遇到 error 直接抛出到顶层,无法在声明为 Optional 的变量中处理成 nil。
以上种种,不得已对 Model 层作出限制:
- 所有成员变量使用 Optional。
- 重写 URL 的 decodeIfPresent 实现。
- 重写 RawRepresentable 的 decodeIfPresent 实现。
URL
1 2 3 4 5 6 7 8 9
|
struct Model: Codable { var out: Out? }
struct Out: Codable { var url: URL? }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
let url_json_1 = """ { "out": { "url": null } } """
let url_json_2 = """ { "out": { "url": "" } } """
let url_json_3 = """ { "out": { "url": 1 } } """
let url_json_4 = """ { "out": { "url": "https://www.apple.com/" } } """
|
normal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: url_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: url_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: url_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: url_json_4.data(using: .utf8)!)
|
JSONDecoder 在遇到抛出错误的子元素时,解析中断,整个 model 被解析为 nil,而不理会该元素是否被声明为 optional。
try? decode
1 2 3 4 5 6 7 8 9 10 11 12
| extension KeyedDecodingContainer { func decodeIfPresent(_ type: URL.Type, forKey key: Key) throws -> URL? { guard let string = try? decode(String.self, forKey: key), let url = URL(string: string) else { return nil }
return url } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: url_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: url_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: url_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: url_json_4.data(using: .utf8)!)
|
自定义 URL 的 decodeIfPresent 方法后,值为空字符串时解析为 nil,但值为数字时应该抛出错误。
try decodeIfPresent
1 2 3 4 5 6 7 8 9 10 11 12
| extension KeyedDecodingContainer { func decodeIfPresent(_ type: URL.Type, forKey key: Key) throws -> URL? { guard let string = try decodeIfPresent(String.self, forKey: key), let url = URL(string: string) else { return nil }
return url } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: url_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: url_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: url_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: url_json_4.data(using: .utf8)!)
|
Enum
1 2 3 4 5 6 7 8 9 10 11 12 13
|
struct Model: Codable { var out: Out? }
struct Out: Codable { var `in`: In? }
enum In: String, Codable { case apple }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
let enum_json_1 = """ { "out": { "in": null } } """
let enum_json_2 = """ { "out": { "in": "" } } """
let enum_json_3 = """ { "out": { "in": 1 } } """
let enum_json_4 = """ { "out": { "in": "apple" } } """
let enum_json_5 = """ { "out": { "in": "boy" } } """
|
normal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: enum_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: enum_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: enum_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: enum_json_4.data(using: .utf8)!)
let model_5 = try? decoder.decode(Model.self, from: enum_json_5.data(using: .utf8)!)
|
当服务端返回客户端未定义值时,解析中断,整个 model 被解析为 nil。
case unknown
1 2 3 4 5 6 7 8 9 10
| enum In: String, Codable { case apple case unknown
init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try container.decode(String.self) self = Self(rawValue: rawValue) ?? .unknown } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: enum_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: enum_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: enum_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: enum_json_4.data(using: .utf8)!)
let model_5 = try? decoder.decode(Model.self, from: enum_json_5.data(using: .utf8)!)
|
给枚举类型增加 unknown 分支,并自定义 init(from:)
初始化方法,可以把未定义值回退到 unknown。但每一个枚举类型都需要增加 unknown 分支和自定义初始化方法,过于繁琐。
try decode
1 2 3 4 5
| extension KeyedDecodingContainer { func decodeIfPresent<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T? where T: RawRepresentable { return try decode(T.self, forKey: key) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: enum_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: enum_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: enum_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: enum_json_4.data(using: .utf8)!)
let model_5 = try? decoder.decode(Model.self, from: enum_json_5.data(using: .utf8)!)
|
带原始值的枚举类型实现了 RawRepresentable 协议,因此可以自定义 RawRepresentable 的 decodeIfPresent 方法。当使用 try decode
时,null 为值时直接抛出错误。空字符串和未定义值也会抛出错误。
try? decode
1 2 3 4 5
| extension KeyedDecodingContainer { func decodeIfPresent<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T? where T: RawRepresentable { return try? decode(T.self, forKey: key) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: enum_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: enum_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: enum_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: enum_json_4.data(using: .utf8)!)
let model_5 = try? decoder.decode(Model.self, from: enum_json_5.data(using: .utf8)!)
|
当使用 try? decode
时,会吞掉所有抛出的错误,而非法类型会被解析为 nil。
try decodeIfPresent
1 2 3 4 5 6 7 8 9 10 11 12
| extension KeyedDecodingContainer { func decodeIfPresent<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T? where T: RawRepresentable, T.RawValue: Decodable { guard let rawValue = try decodeIfPresent(type.RawValue.self, forKey: key), let value = T(rawValue: rawValue) else { return nil }
return value } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
let decoder = JSONDecoder()
let model_1 = try? decoder.decode(Model.self, from: enum_json_1.data(using: .utf8)!)
let model_2 = try? decoder.decode(Model.self, from: enum_json_2.data(using: .utf8)!)
let model_3 = try? decoder.decode(Model.self, from: enum_json_3.data(using: .utf8)!)
let model_4 = try? decoder.decode(Model.self, from: enum_json_4.data(using: .utf8)!)
let model_5 = try? decoder.decode(Model.self, from: enum_json_5.data(using: .utf8)!)
|
总结
- 在 decode 中,RawValue 类型正确但值错误,个人倾向于回退到 nil,而 RawValue 类型错误,直接 throw。
- 以上方法没有解决类型为
[Element]?
的问题,同时 WebAPI 也可能返回 [null]
。
小尾巴
本以为 propertyWrapper 可以结合 Codable 玩些新花样,例如只改 CodingKey 或者 RawValue,不重写 init(from:)
,然而并不可以。
参考
- 或许你并不需要重写 init(from:) 方法 - kemchenj