美文网首页
Swift5 支持自定义编码的三种容器-II

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

作者: 醉看红尘这场梦 | 来源:发表于2019-12-14 21:04 被阅读0次

    之前看到过的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编码处理方式不同的原因了。

    相关文章

      网友评论

          本文标题:Swift5 支持自定义编码的三种容器-II

          本文链接:https://www.haomeiwen.com/subject/zryhnctx.html