我们来研究KeyedEncodingContainer
。但为了研究这个容器,我们得从CodingKey
开始。一直以来,我们都把它弱化成了用于标记某种位置的对象。但现在,是时候看看这个类型的细节了。为什么呢?因为KeyedEncodingContainer
正是需要通过它来生成保存在容器中的key。
Codingkey
CodingKey
是一个protocol
,它的定义在这里:
public protocol CodingKey :
CustomStringConvertible, CustomDebugStringConvertible
{
var stringValue: String { get }
init?(stringValue: String)
var intValue: Int? { get }
init?(intValue: Int)
}
简单来说,可以把CodingKey
看成一个记录位置的值。这个值可以是字符串形式的,也可以是整数形式的。然后,再来看下之前我们见到过的_JSONKey
,它的定义在这里:
fileprivate struct _JSONKey : CodingKey {
public var stringValue: String
public var intValue: Int?
public init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
public init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
public init(stringValue: String, intValue: Int?) {
self.stringValue = stringValue
self.intValue = intValue
}
fileprivate init(index: Int) {
self.stringValue = "Index \(index)"
self.intValue = index
}
fileprivate static let `super` = _JSONKey(stringValue: "super")!
}
对于上一节提到的UnkeyedEncodingContainer
,CodingKey
的值就是通过整数表示的:
self.encoder.codingPath.append(_JSONKey(index: self.count))
执行下面的代码:
_ = try JSONEncoder().encode([
Double.infinity
])
就会在控制台看到下面这样的错误:
Swift.EncodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)]
现在就能理解这个_JSONKey(stringValue: "Index 0", intValue: 0)
表达的位置信息了吧。
KeyedEncodingContainer
理解了_JSONKey
之后,我们来看最后一个容器:KeyedEncodingContainer
。首先要介绍的,是protocol KeyedEncodingContainerProtocol
,它定义在这里:
public protocol KeyedEncodingContainerProtocol {
associatedtype Key : CodingKey
var codingPath: [CodingKey] { get }
mutating func encodeNil(forKey key: Key) throws
% for type in codable_types:
mutating func encode(_ value: ${type}, forKey key: Key) throws
% end
mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws
mutating func encodeConditional<T : AnyObject & Encodable>(
_ object: T, forKey key: Key) throws
% for type in codable_types:
mutating func encodeIfPresent(_ value: ${type}?, forKey key: Key) throws
% end
mutating func encodeIfPresent<T : Encodable>(
_ value: T?, forKey key: Key) throws
mutating func nestedContainer<NestedKey>(
keyedBy keyType: NestedKey.Type, forKey key: Key
) -> KeyedEncodingContainer<NestedKey>
mutating func nestedUnkeyedContainer(
forKey key: Key) -> UnkeyedEncodingContainer
mutating func superEncoder() -> Encoder
mutating func superEncoder(forKey key: Key) -> Encoder
}
从内容结构性上来说,KeyedEncodingContainerProtocol
和UnkeyedEncodingContainer
是一样的。只不过,KeyedEncodingContainerProtocol
约束的encode
方法里,多了一个forKey
参数而已。因此,我们就不再重复了。
而我们要说的KeyedEncodingContainer
正是一个遵从了KeyedEncodingContainerProtocol
的类型,它的定义在这里:
public struct KeyedEncodingContainer<K : CodingKey> :
KeyedEncodingContainerProtocol
{
public typealias Key = K
internal var _box: _KeyedEncodingContainerBase<Key>
public init<Container : KeyedEncodingContainerProtocol>(
_ container: Container) where Container.Key == Key
{
_box = _KeyedEncodingContainerBox(container)
}
}
emm... 又有一个新类型进入了我们的视野:_KeyedEncodingContainerBox
。如果你在源代码里继续看下去就会发现,KeyedEncodingContainerProtocol
约束的所有方法的实现,都转发到了这个对象:
public mutating func encode<T : Encodable>(
_ value: T, forKey key: Key) throws
{
try _box.encode(value, forKey: key)
}
_KeyedEncodingContainerBox
那接下来要做的,当然就是追到_KeyedEncodingContainerBox
了。它的定义在这里:
internal final class _KeyedEncodingContainerBox<
Concrete : KeyedEncodingContainerProtocol
> : _KeyedEncodingContainerBase<Concrete.Key> {
typealias Key = Concrete.Key
internal var concrete: Concrete
internal init(_ container: Concrete) {
concrete = container
}
}
在研究它之前,我们先来看看它的基类:_KeyedEncodingContainerBase
。它的定义在这里:
internal class _KeyedEncodingContainerBase<Key : CodingKey> {
internal init(){}
deinit {}
internal var codingPath: [CodingKey] {
fatalError("_KeyedEncodingContainerBase cannot be used directly.")
}
/// ...
}
虽然它表面上没有遵从KeyedEncodingContainerProtocol
,但它却实现了其约束的所有方法,只不过这些方法都没有实际的功能。也就是说,它是一个“样板基类”。那这个_KeyedEncodingContainerBox
自然就是照这这个样板派生出来的一个有实际功能的派生类了。在这个类的实现里,它把KeyedEncodingContainerProtocol
约束的所有方法,都转发给了自己的类模版参数:
override internal func encode<T : Encodable>(
_ value: T, forKey key: Key) throws
{
try concrete.encode(value, forKey: key)
}
追到这里,由KeyedEncodingContainer
牵扯出来的类型总算是都看完了。这一大圈折腾下来,最终实现的效果,就是只要在构建KeyedEncodingContainer
的时候,给它注入一个遵从了KeyedEncodingContainerProtocol
的对象,KeyedEncodingContainer
就可以按照这个对象定义的方式,来编码数据了。
并且,看到这,我们也就应该明白为什么_KeyedEncodingContainerBox
明明实现了KeyedEncodingContainerProtocol
约定的方法,但却没有明确声明自己遵从了这个协议了,其实它就是一个跑龙套的,我们并不能把它注入到KeyedEncodingContainer
里 :)
回到JSONEncoder
说完了KeyedEncodingContainer
的“成分”,我们就可以回到_JSONEncoder
了,它是如何实现Encoder
约束的下面这个方法的呢?
func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key>
这个方法的实现,在这里:
public func container<Key>(keyedBy: Key.Type)
-> KeyedEncodingContainer<Key> {
let topContainer: NSMutableDictionary
if self.canEncodeNewValue {
topContainer = self.storage.pushKeyedContainer()
} else {
guard let container =
self.storage.containers.last as? NSMutableDictionary else {
preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
}
topContainer = container
}
let container = _JSONKeyedEncodingContainer<Key>(
referencing: self,
codingPath: self.codingPath,
wrapping: topContainer)
return KeyedEncodingContainer(container)
}
抛开各种条件的判断和错误处理的部分,它的核心逻辑是这样的:首先,从_JSONEncoder.storage
中开辟一个新的NSMutableDictionary
;其次,用这个NSMutableDictionary
创建了一个_JSONKeyedEncodingContainer
,我们不难猜测这应该是一个遵从了KeyedEncodingContainerProtocol
的方法,这里进行的就是实际的数据编码工作。
为什么这么肯定呢?因为在最后,它返回的KeyedEncodingContainer
对象里,注入的正是这个_JSONKeyedEncodingContainer
对象。于是,我们只要追到_JSONKeyedEncodingContainer
去看看,所有的秘密就都解开了。
_JSONKeyedEncodingContainer
_JSONKeyedEncodingContainer
的定义,在这里:
fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey>
: KeyedEncodingContainerProtocol {
typealias Key = K
private let encoder: __JSONEncoder
private let container: NSMutableDictionary
private(set) public var codingPath: [CodingKey]
fileprivate init(
referencing encoder: __JSONEncoder,
codingPath: [CodingKey],
wrapping container: NSMutableDictionary) {
self.encoder = encoder
self.codingPath = codingPath
self.container = container
}
}
怎么样,看到这里,是不是有很多眼熟的东西?__JSONEncoder
我们见过了,codingPath
我们也见过了,container
是我们创建_JSONKeyedEncodingContainer
时注入的NSMutableDictionary
。
并且,_JSONKeyedEncodingContainer
果然是一个遵从了KeyedEncodingContainerProtocol
的类型。那么,剩下的,就是看看它实现的那些encode
方法了:
public mutating func encode(_ value: Int, forKey key: Key) throws {
self.container[_converted(key).stringValue] = self.encoder.box(value)
}
public mutating func encode(_ value: Double, forKey key: Key) throws {
self.encoder.codingPath.append(key)
defer { self.encoder.codingPath.removeLast() }
self.container[_converted(key).stringValue] = try self.encoder.box(value)
}
我们把它和UnkeyedEncodingContainer
中encode
对比一下:
public mutating func encode(_ value: Double) throws {
self.encoder.codingPath.append(_JSONKey(index: self.count))
defer { self.encoder.codingPath.removeLast() }
self.container.add(try self.encoder.box(value))
}
看到了吧,其实核心逻辑是一样的。差别只是这次我们向container
中保存结果的时候,使用的是行如_converted(key).stringValue
这样的key。
再说CodingKey
那么,这个_converted(key).stringValue
又是什么呢?实际上,这是_JSONKeyedEncodingContainer
的一个内部方法,它的定义在这里:
private func _converted(_ key: CodingKey) -> CodingKey {
switch encoder.options.keyEncodingStrategy {
case .useDefaultKeys:
return key
case .convertToSnakeCase:
let newKeyString =
JSONEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue)
return _JSONKey(stringValue: newKeyString, intValue: key.intValue)
case .custom(let converter):
return converter(codingPath + [key])
}
}
还记得之前我们说过,JSONEncoder
里有一个key生成策略的配置么?它的用处就是在这里,默认情况下KeyedEncodingContainer
中使用的key值,就是CodingKey
对应的stringValue
,但我们也可以使用case .convertToSnakeCase
或者使用更为自由的自定义方法case .custom
。至于这两个case
的用法,大家去看下keyEncodingStrategy
的代码就会用了,我们就不再这里重复了。
最后,在结束这一节之前,我们再来回顾下之前定义过的User
:
struct User {
let name: String
let age: Double
}
extension User: Encodable {
private enum CodingKeys: CodingKey {
case name
case age
}
public 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)
}
}
为了让它是一个Encodable
的类型,我们明确定义了一个遵从CodingKey
的类型作为key,明确定义了encode(to:)
定义编码过程。在我看来,对于现阶段的Swift来说,明确定义用到的key和encode(to:)
方法是编码自定义类型最明确和安全的做法,你应该一直如此。
虽然,在一些情况下,编译器可以为自定义类型自动生成编码和解码的方法。不过这个过程仍旧不属于Swift标准库的一部分,而是在编译器内部酝酿的(因为Swift还没有足够强大的类型反射能力)。这也就意味着,这个自动合成的功能存在未来存在着诸多不确定性,不同的开发者也可能对此有不同的认知,甚至一些服务端API的改动会影响这个机制的工作。
遵从CodingKey的类型必须是enum么?
对于上面这个例子的另外一个问题,就是:用于定义key的CodingKey
的类型必须是enum
么?当然不是,只不过用enum
有一些好处,编译器可以自动为我们生成CodingKey
约束的init
方法,可以自动用case
的值作为stringValue
。对比下面这个使用struct
的例子,你就明白了。
在extension User
里,添加下面的代码:
private struct CodingKeys1: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
var intValue: Int?
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
然后,把encode(to:)
方法改成这样:
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys1.self)
try container.encode(name, forKey: CodingKeys1(stringValue: "name")!)
try container.encode(age, forKey: CodingKeys1(stringValue: "age")!)
}
用下面的代码测试下:
let elev = User(name: "11", age: 11)
let data = try JSONEncoder().encode(elev)
let str = String(bytes: data, encoding: .utf8)!
print(str)
仍旧可以得到{"name":"11","age":11}
这样的结果。怎么样?虽然可以正常工作,不过,你应该不太喜欢这样吧 :)