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

我们来研究构成__JSONEncoder的另一半,也就是它的Encoder身份。

Encoder约束了什么

既然如此,我们当然应该从Encoder究竟约束了什么开始。这个protocol的定义在这里

public protocol Encoder {
  var codingPath: [CodingKey] { get }
  var userInfo: [CodingUserInfoKey : Any] { get }

  func container<Key>(keyedBy type: Key.Type)
    -> KeyedEncodingContainer<Key>

  func unkeyedContainer() -> UnkeyedEncodingContainer

  func singleValueContainer() -> SingleValueEncodingContainer
}

其中,codingPathuserInfo在之前我们已经看过了。现在的重点,就是Encoder约束的这三个方法,或者说,是这三个方法的返回值:KeyedEncodingContainer<Key> / UnkeyedEncodingContainer / SingleValueEncodingContainer。这些叫做container的类型究竟是做什么的呢?其实它们的名字远比它们实际的作用看起来要高级多了。为了感性了解下这三个容器类型的作用,我们先来看个例子,假设要编码下面这个自定义的类型:

struct User {
  let name: String
  let age: Double
}

我们姑且不说Swift可以为这种类型自动添加Encodable能力这回事儿(因为它的每个属性都遵从Encodable)。现在,就是得自定义它的编码方法,第一种方法,就是用KeyedEncodingContainer

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)
  }
}

用下面的代码测试一下:

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}这样的结果了。参照这个结果以及刚才encode(to:)的实现,不难猜到,所谓的KeyedEncodingContainer,就是可以让我们按照CodingKeys中指定的key,把要编码的值存起来的容器。至于这个CodingKey类型,我们找后找个机会再来说它。现在只要知道用它给container定义key的名字就行了。

接下来,再来看用UnkeyedEncodingContainer来编码User对象会如何,我们把之前的代码改成这样:

public func encode(to encoder: Encoder) throws {
  var container = encoder.unkeyedContainer()
  try container.encode(name)
  try container.encode(age)
}

执行之前相同的测试代码,就会得到这样的结果:["11",11]。看到了吧,既然是UnKeyed,那就是结果中只有值,没有key了呗。这样编码出来的,就是一个只包含值的数组。

至于最后一个SingleValueEncodingContainer,它表示的就是一个编码单一值的容器,我们只能向其中添加一个值:

public func encode(to encoder: Encoder) throws {
  var container = encoder.singleValueContainer()
  try container.encode(name)
  /// Uncomment the following line will cause runtime error.
  /// try container.encode(age)
}

就像上面注释的一样,如果去掉编码age的注释,就会发生运行时错误。但即便是这样,我们之前使用的测试代码还是无法正常工作。因为通过SingleValueEncodingContainer编码出来的结果,是一个NSString,而不是一个NSDictonary或者NSArray。上一节,我们分析encode()方法的时候就看到了,如果box_返回的不是一个NS开头的集合类型,都会发生运行时错误。因此,如果我们把User用了SingleValueEncodingContainer编码,那么测试的时候,就得用这个值手工构建一个集合类型:

let elev = User(name: "11", age: 11)
let data = try JSONEncoder().encode([elev])
let str = String(bytes: data, encoding: .utf8)!

print(str)

这样,就可以得到["11"]这样的结果了。不过这只是一个用于演示的例子,我们当然不会只编码User对象的某一个属性。

看过了这三个例子之后,现在至少可以感性的知道了,如果把JSONEncoder比喻成一台加工数据的机器,所谓Encoder约束的这些方法返回的containers,就好比为这个机器提交的加工规格表单,我们可以填写key-value形式的,可以添加数组形式的,也可以填写加工成一个单一值形式的。

__JSONEncoder中container的实现

理解了刚才这些例子,我们就可以反着追回去看看__JSONEncoder中,这三个方法究竟是如何实现的了。

SingleValueEncodingContainer

我们从最简单的singleValueContainer方法说起,它的定义在这里

fileprivate class __JSONEncoder : Encoder {
  public func singleValueContainer()
    -> SingleValueEncodingContainer {
      return self
  }
}

看到了吧,就是返回它自己,也就是说,__JSONEncoder自身就是一个SingleValueEncodingContainer。那这个SingleValueEncodingContainer又是什么呢?实际上,它是一个定义在这里protocol

public protocol SingleValueEncodingContainer {
  var codingPath: [CodingKey] { get }

  mutating func encodeNil() throws

% for type in codable_types:
  mutating func encode(_ value: ${type}) throws
% end

  mutating func encode<T : Encodable>(_ value: T) throws
}

也就是说,它为nilcodable_types中定义的类型以及任何一个遵从了Encodable的类型,定义了一个编码它们的encode方法。既然__JSONEncoder遵从了SingleValueEncodingContainer,那我们接下来当然就是去看看__JSONEncoder是如何实现这些方法的了。这个extension的定义,在这里。对于protocol中约束的每一类encode,我们各列举一个实现:

extension __JSONEncoder : SingleValueEncodingContainer {
  // MARK: - SingleValueEncodingContainer Methods
  fileprivate func assertCanEncodeNewValue() {
    precondition(self.canEncodeNewValue,
      "Attempt to encode value through single value container when previously value already encoded.")
  }

  public func encodeNil() throws {
    assertCanEncodeNewValue()
    self.storage.push(container: NSNull())
  }

  public func encode(_ value: Bool) throws {
    assertCanEncodeNewValue()
    self.storage.push(container: self.box(value))
  }

  public func encode<T : Encodable>(_ value: T) throws {
    assertCanEncodeNewValue()
    try self.storage.push(container: self.box(value))
  }
}

其中,canEncodeNewValue的实现在这里

fileprivate var canEncodeNewValue: Bool {
    return self.storage.count == self.codingPath.count
}

其实,在官方源代码里,这个计算属性有一大段注释,大家可以稍后自己去看看。现在,只要知道,所谓可以编码新值的要求,就是__JSONEncoder.storage中元素的个数和codingPath中元素的个数相同就行了。

至于是编码nilcodable_types中定义的类型还是任何一个遵从了Encodable的类型,其实它们的逻辑都是一样的,把这些值用box方法打包之后,存入__JSONEncoder.storage就行了。

看到这,想必大家也就明白为什么SingleValueEncodingContainer只能编码一个值了。在向storage中存入元素之后,self.storage.count的值会增加,但是编码进来的值并没有对应的codingPath。当我们继续向其中编码新值的时候,canEncodeNewValue就会返回false,进而precondition就会触发错误了。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,515评论 25 707
  • 这个世界上,最幸福的事是在一个美好的环境下死去 在一个夏至之后的夜晚 看见夕阳的余晖笼罩在城市的西边 看见...
    十嘢阅读 470评论 0 1
  • 说好的早睡早起,仅仅是说好而已。因为我们是害怕晚上的寂寞和对抗不了睡懒觉的惰性。自从有了智能机互联网,世界是连起来...
    猴子不说话阅读 369评论 0 0
  • 大长腿先生是我家儿子小L,常自诩丰神俊秀玉树临风谦谦君子才辩无双引校内无数美女竞折腰。我这个妈尽量客观,他似乎并没...
    步绾阅读 3,524评论 135 95