首页 > 解决方案 > Swift 嵌套可选值类型(结构)和属性修改

问题描述

我在我的模型中使用了几种值类型,并且这种值类型(结构)具有嵌套其他值类型(结构)的属性。然后拥有根对象,我想在另一个结构中的一些嵌套结构中修改(添加、删除、更新)属性。此外,此属性通常具有可选类型并且可以为 nil。因此,当分配给 var 时,让值类型被复制,我不能使用这个内部结构实例的可选绑定并在以后修改它们。因此,我必须进行此修改的唯一方法如下:

if let cleaningDetails = initialPackage?.cleaningsSchedule?.details?[indexPath.row], cleaningDetails.startTimes == nil {
                        initialPackage?.cleaningsSchedule?.details?[indexPath.row].startTimes = []
                    }

因此,在使用值类型时,它确实是唯一的选择。其他解决方案是什么?更改为类(引用类型)——这种函数式、值类型的编程真的那么棒吗?或者我应该在这个结构上使用更多的变异函数来促进修改?

标签: swiftstructnestedvalue-type

解决方案


首先,您应该减少系统中 Optional 的数量。有多种方法可以处理 Optional-collections(例如,像您建议的那样改变辅助方法),但是 Optional-overuse 会产生很多不必要的复杂性。任何类型的 Collection 都应该是 Optional 是非常罕见的。只有当和“空”意味着不同的东西时,这才有意义nil(这是非常罕见的)。

与其将整个数据模型包装在特定的 JSON API 周围,不如将 JSON 转换为您想要的数据模型。例如,这是一个 JSON 模型,它包含一个必需的 Int,可能包含也可能不包含一个 Array,但在内部,我们希望将“丢失的数组”视为“空”。我们还想在发送空数组之前去掉它们。

import Foundation

let json = Data("""
{
    "y": 1
}
""".utf8)

struct X {
    var y: Int
    var z: [String]
}

extension X: Codable {
    enum CodingKeys: String, CodingKey {
        case y, z
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        y = try container.decode(Int.self, forKey: .y)
        z = try container.decodeIfPresent([String].self, forKey: .z) ?? []
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(y, forKey: .y)
        if !z.isEmpty {
            try container.encode(z, forKey: .z)
        }
    }
}

let decoder = JSONDecoder()
print(try decoder.decode(X.self, from: json))

let encoder = JSONEncoder()
print(String(data: try encoder.encode(X(y: 1, z: [])), encoding: .utf8)!)

这会将所有工作转移到两种方法中,而不是每次访问数据模型时将其分散到整个程序中。自定义编码仍然有点繁琐(并且可能会在编码器步骤中引入细微的错误),所以如果你有很多,你应该看看可以为你编写它们的SwiftGen 。

如果您真的想跟踪密钥是否丢失或为空(因此您可能会以发送给您的相同方式重新编码),那么我可能会以这种方式隐藏可选属性:

struct X: Codable{
    enum CodingKeys: String, CodingKey {
        case y
        case _z = "z"
    }

    var y: Int
    private var _z: [String]?  // The actual `z` we got from the JSON
    var z: [String] { get { return _z ?? [] } set { _z = newValue } }

    init(y: Int, z: [String]?) {
        self.y = y
        self._z = z
    }
}

“真实”z存储在_z其中并且可以重新序列化,但程序的其余部分永远不会看到 Optional。

另一种比较常见的技术是创建一个适配器层,将“JSON 兼容”结构转换为内部数据模型并返回。如果方便的话,这允许您的内部数据模型与 JSON 稍有不同。

当然,您也可以创建辅助方法,但所有这一切的真正关键是不允许 Optionals 泄漏到程序的其余部分,而这些部分并不是真正的可选。如果系统中某处必须存在复杂性,请将其放在解析/编码点,而不是使用点。


推荐阅读