You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 23:00:54