Swift 可解码 nestedContainer 和向后兼容性

Swift Decodable nestedContainer and backwards compatibility

我只是偶然发现了一些东西,但我找不到任何地方记录的行为,所以我想检查它是否正确。

场景:

我制作了一个 Swift 应用程序来下载和解析 JSON 文件。请注意,实际实现更复杂,因此需要自定义实现来解码(在下面的示例中不是这种情况,但它表现出与我想要展示的行为相同的行为)

对象看起来像这样:

enum ScreenName: String, Decodable, CaseIterable {
    case screen1 = "screen1"
    case screen2 = "screen2"
}

struct ScreenContents: Decodable {
 ...
}

struct Screens {
    let screens: [ScreenName: ScreenContents]

    enum CodingKeys: String, CodingKey {
        case screens
    }
}

数据:

{
  "screens":{
    "screen1":{..},
    "screen2":{..}
  }
}

加载 Screens 的实现:

extension Screens: Decodable {

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let screensContainer = try container.nestedContainer(keyedBy: ScreenName.self, forKey: .screens)

        for enumKey in screensContainer.allKeys {
               ....
        }
}

这很好用。从 URL 下载的数据已解析,一切正常。

现在我想发布我的应用程序的新版本,我在这里介绍 case screen3 = "screen3"。对于新版本的应用程序,没有问题,该项目已添加到枚举中,服务器上的 JSON 已更新,一切正常。

但是我担心向后兼容性。由于生产中的应用程序的当前版本不知道 screen3,因此 Swift 无法将传入的字符串转换为枚举,会发生什么? init(from: decoder) 会抛出异常吗?

所以我尝试用上面的代码加载一个更新的文件,其中包含 screen3。令我惊讶的是,它没有抛出任何错误。

实际发生的是

let screensContainer = try container.nestedContainer(keyedBy: ScreenName.self, forKey: .screens)

忽略了 screen3 条目,只返回 screen1screen2

这显然是个好消息,因为我的向后兼容性问题已得到解决,但是我希望看到它得到某种文档的支持,因此在使用它之前我不依赖于错误或其他怪癖,并且我没找到。

有人能帮忙吗?

谢谢

CodingKey 的全部意义在于它列出了您感兴趣的键。键控容器是数据视图,可能不包含所有数据(在您的情况下它不包含 screen3). documentation for allKeys 涉及到这一点(强调):

Different keyed containers from the same decoder may return different keys here, because it is possible to encode with multiple key types which are not convertible to one another. This should report all keys present which are convertible to the requested type.

您请求的类型是ScreenName,它是一个枚举,支持两个特定的字符串。所以这些是你唯一能得到的东西。但 CodingKey 不必是枚举。例如,我可以构建一个 struct CodingKey(我一直这样做,并希望将其添加到 stdlib)。

struct StringKey: CodingKey, Hashable, CustomStringConvertible {
    var description: String {
        return stringValue
    }

    let stringValue: String
    init(_ string: String) { self.stringValue = string }
    init?(stringValue: String) { self.init(stringValue) }
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
}

这是一个 CodingKey,它可以接受任何字符串(我用它来解码任意 JSON,其中密钥可能事先不知道)。我可以构建另一个只接受以某个前缀开头的字符串并为其他所有内容返回 nil 的字符串。 CodingKeys可以做各种各样的事情。

但最终规则是密钥容器仅包含可转换为请求的 CodingKey 的密钥条目。

所以是的,这是正式向后兼容的。你不只是幸运。这就是它的工作原理。