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

之前看到过的UnkeyedEncodingContainer也是一个protocol,它的定义在这里。除了SingleValueEncodingContainer约束的方法之外,它额外约束了下面这些方法:

public protocol UnkeyedEncodingContainer {
  var count: Int { get }

  mutating func encodeConditional<T : AnyObject & Encodable>(_ object: T) throws

% for type in codable_types:
  /// Encodes the elements of the given sequence.
  ///
  /// - parameter sequence: The sequences whose contents to encode.
  /// - throws: An error if any of the contained values throws an error.
  mutating func encode<T : Sequence>(
    contentsOf sequence: T) throws where T.Element == ${type}
% end

  mutating func encode<T : Sequence>(
    contentsOf sequence: T) throws where T.Element : Encodable

  mutating func nestedContainer<NestedKey>(
    keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey>

  mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer

  mutating func superEncoder() -> Encoder
}

其中:

  • count很简单,表示这个unkeyed container中目前编码进来的值个数;
  • encodeConditional我们姑且把它就当成是一般的encode方法就行了,因为官方也为这个方法提供了默认实现,这个默认实现,就是直接调用了普通的encode方法;

encode(contentsOf:)

接下来的一组encode(contentsOf:)用于把一系列内建类型和遵从了Encodable的类型编码到container里。其实呢,没有这些方法也没关系,我们自己遍历序列然后逐个编码就行了,不过有了它们呢,用起来就方便一些。这些方法的默认实现,在这里

% for type in codable_types:
  public mutating func encode<T : Sequence>(
    contentsOf sequence: T) throws where T.Element == ${type}
  {
    for element in sequence {
      try self.encode(element)
    }
  }
% end

public mutating func encode<T : Sequence>(
  contentsOf sequence: T) throws where T.Element : Encodable
{
  for element in sequence {
    try self.encode(element)
  }
}

看到了吧,跟我们说的是一样的。为了体验这些方法的用法,我们给之前的User添加一个新的属性:

struct User {
  let name: String
  let age: Double
  var tags: [String]
}

把之前的Encodable扩展也进行对应的修改:

extension User: Encodable {
  private enum CodingKeys: CodingKey {
    case name
    case age
    case tags
  }

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

看到这个contentsOf的用法了吧,这样就可以把tags中的值,和name / age一起,编码到同一个数组里了。我们用下面的代码试一下:

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

print(str)

执行下,就可以看到["11",11,"swifter","gamer"]这样的结果了。至于最后的nestedContainernestedUnkeyedContainersuperEncoder,我们得先放饭,等把所有类型的container都看完之后,再来说它们的实现。

至此,UnkeyedEncodingContainer约束的内容就说完了。接下来,我们得回到__JSONEncoder,来看看unkeyedContainer()这个方法究竟做了什么。

__JSONEncoder

unkeyedContainer的实现,定义在这里

public func unkeyedContainer() -> UnkeyedEncodingContainer {
  let topContainer: NSMutableArray

  if self.canEncodeNewValue {
    topContainer = self.storage.pushUnkeyedContainer()
  } else {
      guard let container =
        self.storage.containers.last as? NSMutableArray else {
        preconditionFailure(
          "Attempt to push new unkeyed encoding container when already previously encoded at this path.")
      }

      topContainer = container
  }

  return _JSONUnkeyedEncodingContainer(
    referencing: self,
    codingPath: self.codingPath,
    wrapping: topContainer)
}

简单来说,就是从self.storage中获得一个NSMutableArray,然后用这个数组、__JSONENcoder对象自身以及codingPath,创建了一个_JSONUnkeyedEncodingContainer对象。

_JSONUnkeyedEncodingContainer

而这个_JSONUnkeyedEncodingContainer,就是我们要找的那个遵从了UnkeyedEncodingContainer的类型。它的定义在这里。结合刚才看到的创建方法,我们来看下它的属性:

fileprivate struct _JSONUnkeyedEncodingContainer : UnkeyedEncodingContainer {
  private let encoder: __JSONEncoder
  private let container: NSMutableArray
  private(set) public var codingPath: [CodingKey]

  public var count: Int {
      return self.container.count
  }

  fileprivate init(
    referencing encoder: __JSONEncoder,
    codingPath: [CodingKey],
    wrapping container: NSMutableArray) {
      self.encoder = encoder
      self.codingPath = codingPath
      self.container = container
  }
}

看到了吧,encoder是我们注入的__JSONEncoder对象,container是一个空的NSMutableArraycodingPath__JSONEncoder对象中的codingPath

接下来,就是UnkeyedEncodingContainer中约束的那一堆encode方法了,我们从中找一些具有代表性的来看看:

public mutating func encode(_ value: Bool) throws {
  self.container.add(self.encoder.box(value))
}

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

public mutating func encode<T : Encodable>(_ value: T) throws {
  self.encoder.codingPath.append(_JSONKey(index: self.count))
  defer { self.encoder.codingPath.removeLast() }
  self.container.add(try self.encoder.box(value))
}

而选择的依据,就是:

  • 编码不会抛出异常的内建类型;
  • 编码可能抛出异常的内建类型;
  • 编码任意一个遵从了Encodable的类型;

聊聊codingPath

其实,编码数据的核心逻辑和SingleValueEncodingContainer是一样的,都是把值打包之后,放到容器里。只不过,当打包的值可能抛出异常的时候,存入容器之前,也把对应值的Key存到了codingPath里。但SingleValueEncodingContainer中却没有这样做,我们对比下其编码Double的代码就会看到了:

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

这是为什么呢?要说在这两种容器里打包数据的唯一区别,就是SingleValueEncodingContainer的打包结果,是直接放在__JSONEncoder.storage中的,而UnkeyedEncodingContainer,则是在storage中新开辟了一个NSMutableArray,并把打包的结果放在了这里。把它们的区别用一张图表示,就是这样的:

image

当我们使用SingleValueEncodingContainer的时候,一来,只能存储一个值;二来,这个值会直接存储在__JSONENcoder.storage里,因此,我们完全不需要额外记录这个值在storage中的位置。

UnkeyedEncodingContainer就不同了。我们可以在这个container里放两类东西:

  • 一类是打包进来的值;
  • 另一类是新创建的嵌套容器;

无论是那种情况,如果其中某一个值打包的时候可能发生异常,为了可以在box方法里获得这个位置,我们就得通过__JSONEncoder.codingPath不断的跟踪当前正在打包的值在容器中的位置。这就是为什么这两个容器中,对Double编码处理方式不同的原因了。

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

推荐阅读更多精彩内容

  • 1、请你自我介绍一下你自己? 回答提示: 这是面试官100%会问的问题,一般人回答这个问题过于平常,只说姓名、年龄...
    雷3雷阅读 796评论 0 25
  • 一、面试程序 不同的单位对面试过程的设计会有所不同,有的单位会非常正式,有的单位则相对比较随意,但一般来说,面试可...
    lucas777阅读 9,005评论 2 23
  • 《2018 iOS面试题系列》 1、请你自我介绍一下你自己? 回答提示:这是面试官100%会问的问题,一般人回答这...
    Winny_园球阅读 814评论 0 2
  • 面试宝典V1.0 仔细阅读 把握机会 赢得Offer 1、 请你做一下自我介绍? 3 2、 你觉得你最大的优点是什...
    伊森H阅读 1,350评论 0 20
  • 仔细阅读把握机会赢得Offer 1、请你做一下自我介绍? 2、你觉得你最大的优点是什么? 3、说说你最大的缺点? ...
    胡明哲阅读 2,374评论 0 64