对于将从 JSON 解析的对象模型的属性,我应该使用可选的吗?

Should I use optional for properties of object models that will be parsed from JSON?

我的 iOS 应用程序有一个非常常见的设置:它向 API 服务器发出 HTTP 查询,服务器响应 JSON 对象。然后将这些 JSON 个对象解析为适当的 Swift 个对象。

最初我将属性分为必需属性和可选属性,主要基于我的 API 服务器的数据库要求。例如,idemailname 是必填字段,因此它们使用非可选类型。其他可以在数据库中NULL,所以是可选类型

class User {
  let id: Int
  let email: String
  let profile: String?
  let name: String
  let motive: String?
  let address: String?
  let profilePhotoUrl: String?
}

最近,我开始怀疑这到底是不是一个好的设置。我发现虽然某些属性可能始终在数据库中,但这并不意味着这些属性将始终包含在 JSON 响应中。

例如,在用户个人资料页面中,需要所有这些字段才能正确显示视图。因此,JSON 响应将包括所有这些字段。但是,对于列出用户名的视图,我不需要 emailid,并且 JSON 响应可能也不应该包含这些属性。不幸的是,这将在将 JSON 响应解析为 Swift 对象时导致错误并使应用程序崩溃,因为应用程序期望 idemailname 始终不是-无。

我正在考虑将 Swift 对象的所有属性更改为可选项,但感觉就像放弃了这种特定于语言的功能的所有好处。此外,无论如何,我将不得不编写更多行代码来在应用程序的其他地方解开所有这些可选值。

另一方面,JSON 对象本质上与 Swift 的严格静态类型和 nil-checking 的互操作性不强,因此最好简单地接受这种烦恼。

我应该过渡到每个 属性 作为可选的模型吗?或者,还有更好的方法?我很感激这里的任何评论。

您可以通过三种方式进行此操作:

  1. 始终发送所有 JSON 数据,并保留您的属性非可选。

  2. 将所有属性设为可选。

  3. 使所有属性成为非可选属性,并编写您自己的 init(from:) 方法来为缺失值分配默认值,如 .

    中所述

所有这些都应该有效; "best" 是基于意见的,因此超出了 Stack Overflow 答案的范围。选择最适合您特定需求的一种。

首先要问:“列出用户姓名的视图”的元素是否需要与“用户个人资料页面”背后的模型对象是同一类对象?也许不是。也许您应该专门为用户列表创建一个模型:

struct UserList: Decodable {

    struct Item: Decodable {
        var id: Int
        var name: String
    }

    var items: [Item]

}

(虽然问题说 JSON 响应可能不包括 id,但它似乎不是没有 ids 的用户列表特别有用,所以我在这里要求它。)

如果您真的希望它们是同一类对象,那么您可能希望将用户建模为具有服务器始终发送的核心属性,以及一个可能为 nil 的“详细信息”字段:

class User: Decodable {
    let id: Int
    let name: String
    let details: Details?

    struct Details: Decodable {
        var email: String
        var profile: String?
        var motive: String?
        var address: String?
        var profilePhotoUrl: String?
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        details = container.contains(.email) ? try Details(from: decoder) : nil
    }

    enum CodingKeys: String, CodingKey {
        case id
        case name

        case email // Used to detect presence of Details
    }
}

请注意,我创建 Details,如果它存在,使用 Details(from: decoder),而不是通常的 container.decode(Details.self, forKey: .details)。我使用 Details(from: decoder) 来做到这一点,这样 Details 的属性与 User 的属性来自同一个 JSON 对象,而不需要嵌套对象。

如果服务器为其他属性提供 Null 值,您可以使用可选值和安全解包。或者在展开时,如果值为 nil

,则可以将空字符串分配给 属性
profile = jsonValue ?? ""

其他情况,因为其他属性是字符串数据类型,您可以将默认值指定为空字符串

class User {
  let id: Int
  let email: String
  let profile: String = ""
  let name: String
  let motive: String = ""
  let address: String = ""
  let profilePhotoUrl: String = ""
}

我更喜欢可选属性,因为您不能保证 JSON 值始终存在,响应 属性 名称的任何更改都会使您的应用程序崩溃。

如果您不使用可选值,您必须在解析时控制参数并添加默认值,如果您想要一个无崩溃的应用程序。你不会知道它是 nil 还是来自服务器的空字符串。

可选值是你最好的朋友。

object mapper 用于可变和非可变属性。

realm-swift 默认非可选值。

我通常将所有非关键属性设为可选,然后有一个可失败的初始化程序。这使我能够更好地处理 JSON 格式中的任何更改或损坏的 API 合同。

例如:

class User {
  let id: Int
  let email: String
  var profile: String?
  var name: String?
  var motive: String?
  var address: String?
  var profilePhotoUrl: String?
}

这意味着我将永远不会拥有没有 ID 或电子邮件的用户对象(假设这两个对象始终需要与用户相关联)。如果我得到一个没有 ID 或电子邮件的 JSON 负载,User class 中的 Initializer 将失败并且不会创建用户对象。然后我对失败的初始化器进行错误处理。

我宁愿拥有一个带有可选属性的 swift class 而不是一堆带有空字符串值的属性。

是的,如果 属性 在 API 中不是必需的,你应该使用可选的,如果你想在强制性的 属性 中有一些值,那么分配空白值:

class User {
  let id: Int?
  let email: String? = ""
  let profile: String?
  let name: String? = ""
  let motive: String?
  let address: String?
  let profilePhotoUrl: String?
}

我建议保留所有 non-scalar(String, Custom Types etc) properties 为可选,scalar(Int, Float, Double etc) 为非可选(有一些例外),方法是分配默认值和空数组集合。例如,

class User {
    var id: Int = 0
    var name: String?
    var friends: [User] = []
    var settings: UserSettings?
}

这可以确保无论服务器发生什么情况,应用程序都不会崩溃。我更喜欢异常行为而不是崩溃。

在我看来,我会选择 2 个解决方案中的 1 个:

  1. 将我的 init funcJSON 编辑为 object,使用所有道具的默认对象值初始化 (id = -1, email = ''),然后使用可选的读取 JSON检查。
  2. 为该特定案例创建一个新的 class/struct

前提:

Partial representing is a common pattern in REST. Does that mean all properties in Swift need to be optionals? For example, the client might just need a list of user ids for a view. Does that mean that all the other properties (name, email, etc) need to be marked as optional? Is this good practice in Swift?

在模型中将属性标记为可选仅表示密钥可能会出现也可能不会出现。它允许 reader 一眼就知道模型的某些事情。
如果您只为不同的 API 响应结构维护一个通用模型并将所有属性设为可选,那么这是否是一个好的做法是非常值得商榷的。
我已经这样做了,它咬人了。有时还好,有时就是不够清晰。

为多个 API 保留一个模型就像设计一个具有许多 UI 元素的 ViewController,并根据特定情况确定应显示哪些 UI 元素与否。
这增加了新开发人员的学习曲线,因为涉及更多的系统理解。


我的 2 美分:

假设我们继续 Swift 的 Codable 用于 encoding/decoding 模型,我会把它分解成单独的模型,而不是维护一个包含所有选项的通用模型 &/或默认值。

我做出决定的原因是:

  1. 分离的清晰度

    • 每个模型都有特定用途
    • 更清洁的自定义解码器的范围
      • 当 json 结构需要一些预处理时很有用
  2. 考虑 API 特定的附加键 可能会在以后出现。

    • 如果此用户列表 API 是唯一需要更多键(例如朋友数量或其他一些统计信息)的用户列表怎么办?
      • 我是否应该继续加载单个模型以支持不同的情况,而附加键仅出现在一个 API 响应中,而不出现在另一个响应中?
      • 如果第三个 API 旨在获取用户信息,但这次的目的略有不同怎么办?我应该用更多的键来加载相同的模型吗?
    • 对于单个模型,随着项目的继续进行,事情可能会变得混乱,因为现在非常 API 基于案例的关键可用性。由于所有都是可选的,我们将有很多可选的绑定&也许在这里和那里有一些快捷的 nil 合并,我们本来可以首先使用专用模型来避免的。
  3. 编写模型很便宜,但维护案例却不是。

但是,如果我很懒惰,并且强烈感觉未来不会发生疯狂的变化,我会继续将所有键设为可选并承担相关费用。

这实际上取决于您处理数据的方式。如果您通过 "Codable" class 处理数据,那么您必须编写自定义解码器以在未获得某些预期值时抛出异常。像这样:

 class User: Codable {
    let id: Int
    let email: String
    let profile: String?
    let name: String
    let motive: String?
    let address: String?
    let profilePhotoUrl: String?

     //other methods (such as init, encoder, and decoder) need to be added below.
    }

因为我知道如果我没有得到最低要求的参数,我将需要 return 一个错误,你需要类似错误枚举的东西:

    enum UserCodableError: Error {
         case missingNeededParameters
         //and so on with more cases
    }

您应该使用编码密钥来使服务器保持一致。在用户对象内部执行此操作的方法如下:

    fileprivate enum CodingKeys: String, CodingKey {
       case id = "YOUR JSON SERVER KEYS GO HERE"
       case email
       case profile
       case name
       case motive
       case address
       case profilePhotoUrl
    }

然后,你需要编写你的解码器。一种方法是这样的:

    required init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    guard let id = try? values.decode(Int.self, forKey: .id), let email = try? values.decode(String.self, forKey: .email), let name = try? values.decode(String.self, forKey: .name) else {
        throw UserCodableError.missingNeededParameters
    }

    self.id = id
    self.email = email
    self.name = name

    //now simply try to decode the optionals
    self.profile = try? values.decode(String.self, forKey: .profile)
    self.motive = try? values.decode(String.self, forKey: .motive)
    self.address = try? values.decode(String.self, forKey: .address)
    self.profilePhotoUrl = try? values.decode(String.self, forKey: .profilePhotoUrl)
}

注意事项:您也应该编写自己的编码器以保持一致。

所有这些都可以,只需像这样一个漂亮的调用语句:

    if let user = try? JSONDecoder().decode(User.self, from: jsonData) {
        //do stuff with user
    }

这可能是处理此类问题最安全、swift 最面向对象的方法。

与其他选项相比,我更喜欢使用可失败的初始化程序。

因此,将必需的属性保留为非可选属性,并且仅当它们出现在响应中时才创建对象(您可以使用 if-let 或 gaurd-let 在响应中检查这一点),否则创建对象失败.

使用这种方法,我们可以避免将非可选值设为可选值,避免在整个程序中处理它们时感到痛苦。

此外,可选项并不适用于防御性编程,因此不要通过将 "non-optional" 属性作为可选项来滥用可选项。