3

我正在尝试使用 Swift 4 的 Encodable+JSONEncoder 将结构序列化为字符串。该对象可以保存异构值,如 String、Array、Date、Int 等。

除日期外,使用的方法工作正常。JSONEncoder 的dateEncodingStrategy属性没有任何作用。

这是重现 Playground 中行为的片段:

struct EncodableValue:Encodable {
    var value: Encodable

    init(_ value: Encodable) {
        self.value = value
    }

    func encode(to encoder: Encoder) throws {
        try value.encode(to: encoder)
    }
}

struct Bar: Encodable, CustomStringConvertible {
    let key: String?
    let value: EncodableValue?

    var description: String {
        let encoder = JSONEncoder()
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E, d MMM yyyy"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        encoder.dateEncodingStrategy = .formatted(dateFormatter)
        let jsonData = try? encoder.encode(self)
        return String(data: jsonData!, encoding: .utf8)!
    }
}

let bar1 = Bar(key: "bar1", value: EncodableValue("12345"))
let bar2 = Bar(key: "bar2", value: EncodableValue(12345))
let bar3 = Bar(key: "bar3", value: EncodableValue(Date()))

print(String(describing: bar1))
print(String(describing: bar2))
print(String(describing: bar3))

输出:

"{"key":"bar1","value":"12345"}\n"
"{"key":"bar2","value":12345}\n"
"{"key":"bar3","value":539682026.06086397}\n"

对于 bar3 对象:我期待类似的东西"{"key":"bar3","value":"Thurs, 3 Jan 1991"}\n",但它以默认的 .deferToDate 策略格式返回日期。

##编辑1##

所以我在 XCode 9 中运行了相同的代码,它给出了预期的输出,即正确地将日期格式化为字符串。我认为 9.2 对 Swift 4 进行了小幅升级,这打破了这个特性。不知道下一步该怎么做。

##编辑2##

作为临时补救措施,在使用闭包更改为@Hamish 的方法之前,我使用了以下代码段。

struct EncodableValue:Encodable {
    var value: Encodable

    init(_ value: Encodable) {
        self.value = value
    }

    func encode(to encoder: Encoder) throws {
        if let date = value as? Date {
            var container = encoder.singleValueContainer()
            try container.encode(date)
        }
        else {
            try value.encode(to: encoder)
        }

    }
}
4

2 回答 2

10

使用自定义日期编码策略时,编码器会拦截在给定容器中对 a 进行编码的调用Date,然后应用自定义策略

但是,使用您的EncodableValue包装器,您并没有给编码器这样做的机会,因为您直接调用了底层值的encode(to:)方法。使用Date,这将使用其默认表示形式对值进行编码,即其timeIntervalSinceReferenceDate.

要解决此问题,您需要在单个值容器中对基础值进行编码,以触发任何自定义编码策略。这样做的唯一障碍是协议不符合它们自己的事实,因此您不能encode(_:)使用参数调用容器的方法Encodable(因为参数需要 a <Value : Encodable>)。

这个问题的一个解决方案是定义一个Encodable扩展来编码到一个单值容器中,然后你可以在你的包装器中使用它:

extension Encodable {
  fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
    try container.encode(self)
  }
}

struct AnyEncodable : Encodable {

  var value: Encodable

  init(_ value: Encodable) {
    self.value = value
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try value.encode(to: &container)
  }
}

这利用了协议扩展成员有一个隐式<Self : P>占位符的事实,协议扩展在哪里P,并且隐式self参数被键入为这个占位符(长话短说;它允许我们调用encode(_:)具有Encodable一致类型的方法)。

另一种选择是在你的包装器上有一个通用的初始化器,通过存储一个执行编码的闭包来擦除类型:

struct AnyEncodable : Encodable {

  private let _encodeTo: (Encoder) throws -> Void

  init<Value : Encodable>(_ value: Value) {
    self._encodeTo = { encoder in
      var container = encoder.singleValueContainer()
      try container.encode(value)
    }
  }

  func encode(to encoder: Encoder) throws {
    try _encodeTo(encoder)
  }
}

在这两种情况下,您现在都可以使用此包装器来编码异构编码,同时尊重自定义编码策略:

import Foundation

struct Bar : Encodable, CustomStringConvertible {

  let key: String
  let value: AnyEncodable

  var description: String {

    let encoder = JSONEncoder()
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "E, d MMM yyyy"
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    encoder.dateEncodingStrategy = .formatted(dateFormatter)

    guard let jsonData = try? encoder.encode(self) else {
      return "Bar(key: \(key as Any), value: \(value as Any))"
    }
    return String(decoding: jsonData, as: UTF8.self)
  }
}

print(Bar(key: "bar1", value: AnyEncodable("12345")))
// {"key":"bar1","value":"12345"}

print(Bar(key: "bar2", value: AnyEncodable(12345)))
// {"key":"bar2","value":12345}

print(Bar(key: "bar3", value: AnyEncodable(Date())))
// {"key":"bar3","value":"Wed, 7 Feb 2018"}
于 2018-02-07T12:51:54.193 回答
1

您可以消除EncodableValue包装器,并改用泛型:

struct Bar<T: Encodable>: Encodable {
    let key: String
    let value: T?

    var json: String {
        let encoder = JSONEncoder()
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E, d MMM yyyy"
        dateFormatter.locale = Locale(identifier: "en_US_POSIX")
        encoder.dateEncodingStrategy = .formatted(dateFormatter)
        let data = try! encoder.encode(self)
        return String(data: data, encoding: .utf8)!
    }
}

let bar = Bar(key: "date", value: Date())

print(bar.json)

这会产生:

{"key":"date","value":"2018 年 2 月 7 日,星期三"}
于 2018-02-07T08:20:38.743 回答