【问题标题】:Decode all properties in custom init (enumerate over all properties of a class and assign them a value)解码自定义初始化中的所有属性(枚举类的所有属性并为其赋值)
【发布时间】:2019-03-20 00:21:27
【问题描述】:

我目前正在处理一个 API 尚未准备好的项目。所以有时某些属性的类型会发生变化。例如我有这个结构:

struct Animal: Codable {
    var tag: Int?
    var name: String?
    var type: String?
    var birthday: Date?
}

对于这个 json:

{
    "tag": 12,
    "name": "Dog",
    "type": "TYPE1"
}

但在开发中,json 会变成这样:

{
    "tag": "ANIMAL",
    "name": "Dog",
    "type": 1
}

所以我在解码器和 nil 对象 中遇到了一些 类型不匹配 错误。为了防止解码器使整个对象失败,我实现了一个自定义 init 并为任何未知属性设置了 nil ,它就像魅力一样工作(注意:我会处理这些稍后更改,这仅适用于计划外和临时更改):

#if DEBUG
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    tag = (try? container.decodeIfPresent(Int.self, forKey: .tag)) ?? nil
    name = (try? container.decodeIfPresent(String.self, forKey: .name)) ?? nil
    type = (try? container.decodeIfPresent(String.self, forKey: .type)) ?? nil
    birthday = (try? container.decodeIfPresent(Date.self, forKey: .birthday)) ?? nil
}
#endif

但是对于较大的类和结构,我必须手动编写任何属性,这需要时间,更重要的是,有时我错过了要设置的属性!

那么有没有办法枚举所有属性并设置它们? 我知道我可以从容器中获取所有密钥,但不知道如何设置它的相应属性:

for key in container.allKeys {
   self.<#corresponding_property#> = (try? container.decodeIfPresent(<#corresponding_type#>.self, forKey: key)) ?? nil
}

谢谢

【问题讨论】:

  • Codable 的全部意义在于提供一种类型安全和静态的方式来将 Swift 类型编码/解码到 JSON。您正在寻找的动态代码在 Swift/Codable 中是不可能的。此外,您不应通过此自定义 init 来掩盖客户端数据模型和 API 之间的不匹配,而实际上应该解决这些问题。此外,使用自动生成用于发出 API 请求/响应的代码的 API 响应设计工具(例如 Swagger)可以让您避免此类错误。
  • 问题是你的值的 type 不断变化:有时tag 是一个字符串,有时它是一个整数。您将需要比 Optional 方法更多的东西;它处理某物是否存在,而不是它是否具有正确的类型。您需要一个 StringOrInteger 类型,如下所述:stackoverflow.com/questions/47215260/… 或者,正如已经建议的那样,完全放弃 Decodable ,因为如果 API 要像这样跳舞是不合适的。
  • 是的,我知道,但是当没有 API 文档并且它正在开发中时,我们不希望在调试模式下出现这么多的崩溃。实际上我把初始化器放在 #if DEBUG ... #endif @DávidPásztor
  • 如果您无法控制 API,要么等待其开发完成,要么在其实现波动很大时使用模拟服务器。另一方面,如果您可以控制 API 设计,请让其开发人员使用更适合解决此类问题的工具,如前所述。此外,最好将开发中的此类错误捕获为崩溃,而不是使用您的方法掩盖它们并让错误意外泄漏到生产中。
  • 谢谢@DávidPásztor。但问题是关于所有属性的枚举并为它们分配一个值,而不是关于这个用例。我更新了问题的标题以进行更多说明。

标签: ios swift codable decoder


【解决方案1】:

您的特定示例中的问题是您的值的 type 不断变化:有时tag 是一个字符串,有时它是一个整数。您将需要比 Optional 方法更多的东西;它处理某物是否存在,而不是它是否具有正确的类型。您需要一个可以解码和表示字符串或整数的联合类型,如下所示:

enum Sint : Decodable {
    case string(String)
    case int(Int)
    enum Err : Error { case oops }
    init(from decoder: Decoder) throws {
        let con = try decoder.singleValueContainer()
        if let s = try? con.decode(String.self) {
            self = .string(s)
            return
        }
        if let i = try? con.decode(Int.self) {
            self = .int(i)
            return
        }
        throw Err.oops
    }
}

使用它,我能够使用单个 Animal 结构类型解码您的两个示例:

struct Animal: Decodable {
    var tag: Sint
    var name: String
    var type: Sint
}
let j1 = """
{
    "tag": 12,
    "name": "Dog",
    "type": "TYPE1"
}
"""
let j2 = """
{
    "tag": "ANIMAL",
    "name": "Dog",
    "type": 1
}
"""
let d1 = j1.data(using: .utf8)!
let a1 = try! JSONDecoder().decode(Animal.self, from: d1)
let d2 = j2.data(using: .utf8)!
let a2 = try! JSONDecoder().decode(Animal.self, from: d2)

好的,但是现在假设您甚至不知道键是什么。然后你需要一个 AnyCodingKey 类型,无论它们是什么,它都可以清除键,而不是多个属性,你的 Animal 将有一个字典属性,如下所示:

struct Animal: Decodable {
    var d = [String : Sint]()
    struct AnyCodingKey : CodingKey {
        var stringValue: String
        var intValue: Int?

        init(_ codingKey: CodingKey) {
            self.stringValue = codingKey.stringValue
            self.intValue = codingKey.intValue
        }
        init(stringValue: String) {
            self.stringValue = stringValue
            self.intValue = nil
        }
        init(intValue: Int) {
            self.stringValue = String(intValue)
            self.intValue = intValue
        }
    }
    init(from decoder: Decoder) throws {
        let con = try decoder.container(keyedBy: AnyCodingKey.self)
        for key in con.allKeys {
            let result = try con.decode(Sint.self, forKey: key)
            self.d[key.stringValue] = result
        }
    }
}

所以现在你可以用完整的未知键来解码,其值可以是字符串或整数。同样,这对您提供的 JSON 示例也很有效。

请注意,这与您最初要求做的相反。我没有使用结构属性名称来生成键,而是简单地接受任一类型的任何键,并通过使用字典将其灵活地存储在结构中。您可以还使用新的 Swift 4.2 dynamicMemberLookup 功能在该字典前面放置一个属性外观。但这留给读者作为练习!

【讨论】:

  • 感谢@matt,但正如我所说,我不想处理所有情况,这只是演示其中一种情况的情况,它不会永远改变。
  • 我还没说完。等等。
  • 好的,你去。
  • 感谢您抽出宝贵时间来解释这一点。但正如你所说,这与我最初问的相反。 dynamicMemberLookup 也用于访问结构的成员,如字典、类型安全和点符号。我问的是一个自动化流程,以减少示例初始化程序中的重复代码,例如枚举。
【解决方案2】:

您想要的工具是Sourcery。它是 Swift 的元编程包装器,可以为您编写样板文件,因为您知道自己想写什么,这很乏味(这就是 Sourcery 的理想选择)。 Sourcery 的重要之处在于(尽管有名字),没有魔法。它只是根据其他 Swift 代码为您编写 Swift 代码。当您不再需要它时,这样可以很容易地拔出。

【讨论】:

  • 不错的工具。谢谢。实际上我正在寻找一些纯粹的快速代码。 Sourcery 是一种分析代码并为我编写重复代码的脚本。关键是根本不写它们!干燥。事件不会自动干燥或编写脚本。
  • 我不明白这条评论。预编译器的输出与 DRY 有什么关系?我不明白您所说的“根本不写它们”是什么意思!你的意思是因为 Swift 编译器在特化泛型函数时通常会重复很多代码 DRY 禁止泛型函数?这是一个非常奇怪的要求。 (无论如何,DRY 只是一个模糊的指导方针,比如“最小化内存流失”或“更喜欢 O(1) 算法。”它本身并不是一个目标。)不过,正如指定的那样,今天在 Swift 中没有解决您的问题的方法。不过,Sourcery 可以解决“它很乏味”的问题。
猜你喜欢
  • 2019-04-08
  • 1970-01-01
  • 2017-06-04
  • 1970-01-01
  • 2017-04-02
  • 1970-01-01
  • 1970-01-01
  • 2014-09-11
相关资源
最近更新 更多