如何在PATCH请求中合理使用Codable结构体?
在PATCH请求中使用Codable结构体的最优实践
在处理PATCH请求时,核心需求是精准追踪Codable结构体的属性变更,只提交更新的字段。下面是几种实用方案,比给所有属性加didSet更优雅:
方案一:原始值对比法(简单直接)
给结构体保存一个初始化时的原始副本,需要生成PATCH请求时,对比当前实例和原始副本的属性差异,收集变更字段。
示例代码:
struct User: Codable { var id: String var name: String var email: String // 存储初始状态的副本 private var original: User? // 解码时自动保存原始值 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) name = try container.decode(String.self, forKey: .name) email = try container.decode(String.self, forKey: .email) original = self } // 手动初始化也同步保存原始值 init(id: String, name: String, email: String) { self.id = id self.name = name self.email = email original = self } // 生成PATCH请求的参数字典 func changedFields() -> [String: Any]? { guard let original = original else { return nil } var changes = [String: Any]() if name != original.name { changes["name"] = name } if email != original.email { changes["email"] = email } return changes.isEmpty ? nil : changes } // 提交成功后重置原始值,方便下次追踪 mutating func resetOriginal() { original = self } }
优缺点:无需额外包装,逻辑集中;但属性较多时,对比代码会有点繁琐,不过大部分业务场景下完全够用。
方案二:通用变更追踪包装器(优雅复用)
创建一个通用的包装类,把需要追踪变更的属性包裹起来,由包装器自动管理变更状态,避免重复写didSet。
示例代码:
// 通用变更追踪包装器 class ChangeTracking<T: Equatable>: Codable { private(set) var originalValue: T var currentValue: T { didSet { hasChanged = currentValue != originalValue } } private(set) var hasChanged: Bool = false init(value: T) { originalValue = value currentValue = value } // 重置变更状态 func reset() { originalValue = currentValue hasChanged = false } // 实现Codable协议 func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(currentValue) } required init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() originalValue = try container.decode(T.self) currentValue = originalValue } } // 使用包装器的业务结构体 struct User: Codable { var id: String var name: ChangeTracking<String> var email: ChangeTracking<String> // 生成PATCH请求参数 func changedFields() -> [String: Any]? { var changes = [String: Any]() if name.hasChanged { changes["name"] = name.currentValue } if email.hasChanged { changes["email"] = email.currentValue } return changes.isEmpty ? nil : changes } // 批量重置所有属性的变更状态 mutating func resetChanges() { name.reset() email.reset() } }
优缺点:变更逻辑完全复用,属性越多越能体现优势;唯一小缺点是访问属性时需要通过.currentValue,不过习惯后影响不大。
不推荐:全局didSet方案
如果硬要使用didSet,虽然能实现需求,但代码冗余度极高,维护成本大,除非特殊场景否则不建议:
struct User: Codable { var id: String var name: String { didSet { updateChangedField("name", oldValue: oldValue, newValue: name) } } var email: String { didSet { updateChangedField("email", oldValue: oldValue, newValue: email) } } private(set) var changedFields = Set<String>() private mutating func updateChangedField(_ key: String, oldValue: String, newValue: String) { if newValue != oldValue { changedFields.insert(key) } else { changedFields.remove(key) } } func patchParameters() -> [String: Any]? { var params = [String: Any]() for field in changedFields { switch field { case "name": params["name"] = name case "email": params["email"] = email default: break } } return params.isEmpty ? nil : params } }
总结
- 属性较少、场景简单:优先用原始值对比法
- 属性较多、需要复用追踪逻辑:优先用通用包装器方案
- 尽量避免给每个属性加
didSet,代码冗余且不易维护
内容的提问来源于stack exchange,提问作者aneuryzm
相关产品推荐
相关产品推荐

