Swift5 支持自定义编码的三种容器-III

我们来研究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")!
}

对于上一节提到的UnkeyedEncodingContainerCodingKey的值就是通过整数表示的:

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
}

从内容结构性上来说,KeyedEncodingContainerProtocolUnkeyedEncodingContainer是一样的。只不过,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)
}

我们把它和UnkeyedEncodingContainerencode对比一下:

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}这样的结果。怎么样?虽然可以正常工作,不过,你应该不太喜欢这样吧 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容

  • 用到的组件 1、通过CocoaPods安装 2、第三方类库安装 3、第三方服务 友盟社会化分享组件 友盟用户反馈 ...
    SunnyLeong阅读 14,607评论 1 180
  • 隆冬将至,从宿舍的窗户玻璃望出去,表面上一片阳光正好,实际上温度很低。冷风透过窗户缝隙嗖嗖作响,像是来自遥远未知大...
    白鹿黄昏阅读 192评论 14 2
  • 乙未仲夏,聚心诸君会于大都冯宅。其地金碧辉煌、光憧影绰、璀璨珠明,内置欧宝莱、碧玺数间珍室,中西合璧、包...
    黄磊的简书阅读 378评论 0 4
  • 首先你得多看,许多人以为新媒体便是没事发发微博,逗逗趣,抽抽奖来吸引用户的话 那你还是适合找文案岗位或者企划岗位,...
    路边卖菜的阅读 160评论 0 0
  • 玉楼冰簟鸳鸯锦,粉融香汗流山枕。 帘外更漏声,敛眉含笑惊。 月影云漠漠,低鬓蝉钗落。 须作一生拚,尽君今日欢。 他...
    籽盐阅读 163评论 0 1