Swift5 不要編碼來自第三方的類型

通常我們都用Codable處理App中和model這類概念有關(guān)的類型。如果這個(gè)類型的屬性都兼容Codable,用起來就不會(huì)有什么問題。但情況并不總是如此,如果model使用了一些不屬于我們自己的類型,這時(shí)的編碼和解碼過程就需要額外注意了。

為了演示這種場景,我們可以用SPM創(chuàng)建兩個(gè)modules:LibsSPM。

let package = Package(
  name: "SPM",
  dependencies: [
  ],
  targets: [
    .target(
      name: "Libs",
      dependencies: []
    ),
    .target(
      name: "SPM",
      dependencies: ["Libs"]),
    .testTarget(
      name: "SPMTests",
      dependencies: ["SPM"]),
  ]
)

然后,在Libs中創(chuàng)建一個(gè)lib.swift,并在其中添加下面的代碼:

import Darwin.C

public struct Point {
  public var x: Double
  public var y: Double

  public init(x: Double, y: Double) {
    self.x = x
    self.y = y
  }
}

現(xiàn)在,假設(shè)這個(gè)Point就是個(gè)來自第三方的類型,我們無法直接修改它的代碼。然后,在main.swift里,定義一個(gè)使用了Point的類型User

struct User {
  var name: String
  var pos: Point

  init(name: String, pos: Point) {
    self.name = name
    self.pos = pos
  }
}

接下來,如果我們希望讓User支持Codable該怎么辦呢?如果直接通過extension完成這件事,編譯器會(huì)阻止你這么干:

extension User: Codable {}

因?yàn)?code>User中的Point并不支持Codable,那如果在main.swift里,也通過extensionPoint也遵從Codable呢?答案是,編譯器也會(huì)阻止你這么干:

extension Point: Codable {}

image

就像圖中說的那樣,盡管Point的兩個(gè)屬性都支持Codable,但編譯器只能在定義Point的文件里,通過extension自動(dòng)合成Codable方法。

那該怎么辦呢?一個(gè)最直接的想法就是自己動(dòng)手寫唄。但這時(shí),先來回答一個(gè)問題:究竟應(yīng)該手工實(shí)現(xiàn)Point還是UserCodable支持呢?

盡管兩種方法事實(shí)上都可行。但作為一條最佳實(shí)踐,最好避免給不屬于自己的類型添加Codable支持。因?yàn)槲覀儫o法預(yù)知這個(gè)類型的原始作者是否會(huì)在未來讓它支持Codable,也無法確定這個(gè)類型官方的Codable方案會(huì)和我們使用的相同。在升級依賴庫的時(shí)候,這些問題對已有的codebase都是潛在的破壞性風(fēng)險(xiǎn)。因此,我們應(yīng)該直接手工實(shí)現(xiàn)UserCodable支持。

接下來,編碼User的時(shí)候,也有兩種不同的方案。一種是“扁平化編碼”:

extension User: Codable {
  private enum CodingKeys: String, CodingKey {
    case name
    case x
    case y
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(pos.x, forKey: .x)
    try container.encode(pos.y, forKey: .y)
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    self.pos = Point(
      x: try container.decode(Double.self, forKey: .x),
      y: try container.decode(Double.self, forKey: .y)
    )
  }
}

這樣一來,編碼的結(jié)果里,就會(huì)隱藏掉Point的存在,變成類似{"name":"11","x":11,"y":10}的形式。

或者,也可以用嵌套容器的方式,把Point編碼進(jìn)來:

extension User: Codable {
  private enum CodingKeys: CodingKey {
    case name
    case pos
  }

  private enum PosCodingKeys: CodingKey {
    case x
    case y
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)

    var posContainer = container.nestedContainer(
      keyedBy: PosCodingKeys.self, forKey: .pos)
    try posContainer.encode(pos.x, forKey: .x)
    try posContainer.encode(pos.y, forKey: .y)
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)

    let posContainer = try container.nestedContainer(
      keyedBy: PosCodingKeys.self, forKey: .pos)
    self.pos = Point(
      x: try posContainer.decode(Double.self, forKey: .x),
      y: try posContainer.decode(Double.self, forKey: .y)
    )
  }
}

這樣,User對象的編碼結(jié)果就會(huì)變成類似{"name":"11","pos":{"x":11,"y":10}}的形式。雖然問題是解決了,但是回頭看看為了支持Codable,我們寫的代碼可著實(shí)不少。更重要的是,這些代碼其實(shí)沒什么“營養(yǎng)”。類的屬性少還好,多了之后,這樣一個(gè)個(gè)的去編碼和解碼的確很麻煩。于是,這樣的話就有點(diǎn)兒尷尬了。既不能直接給Point擴(kuò)展Codable,擴(kuò)展User又太麻煩,還能怎么辦呢?

一個(gè)思路就是,給Point單獨(dú)創(chuàng)建一個(gè)支持Codable的包裝類。并把這個(gè)包裝類放到User里,這樣編譯器就可以自動(dòng)給User合成Codable了。

首先,仿照著Point里我們需要的屬性,創(chuàng)建一個(gè)包裝類:

struct User: Codable {
  private struct _Point: Codable {
    var x: Double
    var y: Double
  }
}

其次,給User添加一個(gè)_Point屬性,并修改對應(yīng)的init方法:

struct User: Codable {
  var name: String
  private var _point: _Point
  var pos: Point

  init(name: String, pos: Point) {
    self.name = name
    self._point = _Point(x: pos.x, y: pos.y)
    self.pos = pos
  }
}

第三,接管pos屬性的賦值和讀取,讀取的時(shí)候,用_point屬性生成一個(gè)新的Point對象;賦值的時(shí)候,把Point的值保存回_point。這樣就實(shí)現(xiàn)了編碼數(shù)據(jù)的時(shí)候,使用內(nèi)部類型_Point,訪問API的時(shí)候,仍舊使用Point的效果:

struct User: Codable {
  var pos: Point {
    get {
      return Point(x: _point.x, y: _point.y)
    }
    set {
      _point = _Point(x: newValue.x, y: newValue.y)
    }
  }
}

第四,定義編碼User時(shí)使用的CodingKey,讓它使用_point的值,以及pos的名字作為key:

private enum CodingKeys: String, CodingKey {
  case name
  case _point = "pos"
}

這樣,Swift就可以自動(dòng)為User生成Codable方法了。最終,User的定義是這樣的:

struct User: Codable {
  var name: String

  private var _point: _Point
  var pos: Point {
    get {
      return Point(x: _point.x, y: _point.y)
    }
    set {
      _point = _Point(x: newValue.x, y: newValue.y)
    }
  }

  private enum CodingKeys: String, CodingKey {
    case name
    case _point = "pos"
  }

  init(name: String, pos: Point) {
    self.name = name
    self._point = _Point(x: pos.x, y: pos.y)
    self.pos = pos
  }

  private struct _Point: Codable {
    var x: Double
    var y: Double
  }
}

但相比之前手工實(shí)現(xiàn)Codable的方式,代碼量明顯要少一些。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容