Swift 4 Codable协议下反向关系的最佳定义方式咨询
如何用Swift 4的Codable协议定义反向关系?
这个问题确实是Swift Codable使用时很常见的痛点——默认的Codable自动实现会因为循环引用导致编码/解码失败,不过我们有几种靠谱的方案来实现这种双向关联,同时避开陷阱。
核心问题:循环引用与Codable的冲突
当类型A包含B的数组,同时B持有A的引用时,如果直接让两者都自动实现Codable,编码时会陷入无限循环(A要编码B,B要编码A,一直递归下去),而且还会造成内存泄漏。所以我们需要用weak引用避免内存泄漏,同时手动处理Codable的编码/解码逻辑,跳过反向引用的序列化。
方案1:手动实现Codable,自动建立反向关联
这是最推荐的方式,能让反向关联的建立完全自动化,不需要额外的手动步骤。
代码示例
class A: Codable { let id: Int var items: [B] // 定义需要编码/解码的键,排除反向引用 enum CodingKeys: String, CodingKey { case id, items } // 初始化时自动设置B的parent init(id: Int, items: [B]) { self.id = id self.items = items items.forEach { $0.parent = self } } // 手动实现解码逻辑,解码后给B设置parent required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) items = try container.decode([B].self, forKey: .items) // 关键步骤:给每个B关联当前的A实例 items.forEach { $0.parent = self } } // 手动实现编码逻辑,只序列化必要的属性 func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(items, forKey: .items) } } class B: Codable { let name: String // 用weak避免循环引用和内存泄漏 weak var parent: A? // B只序列化自身的属性,parent不参与编码/解码 enum CodingKeys: String, CodingKey { case name } init(name: String) { self.name = name } // 解码时只处理name,parent由A来设置 required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) name = try container.decode(String.self, forKey: .name) } }
为什么这么做?
weak var parent: A?:避免A和B之间的循环引用,保证内存能正常释放。- 手动实现
init(from:)和encode(to:):跳过对parent属性的序列化,避免编码时的无限递归,同时在解码完成后自动给每个B绑定对应的A实例。
方案2:自动Codable + 解码后手动绑定反向关系
如果你的场景比较简单,不想写太多手动Codable的代码,也可以用这种更轻量化的方式:让A和B自动实现Codable,然后在解码完成后手动给B设置parent。
代码示例
class A: Codable { let id: Int var items: [B] } class B: Codable { let name: String weak var parent: A? } // 解码并手动绑定关系 let jsonData = """ { "id": 1, "items": [{"name": "Item1"}, {"name": "Item2"}] } """.data(using: .utf8)! do { var a = try JSONDecoder().decode(A.self, from: jsonData) // 手动遍历设置每个B的parent a.items.forEach { $0.parent = a } // 现在每个B都能访问到对应的A了 } catch { print("解码失败:\(error)") }
优化:用扩展封装绑定逻辑
如果需要多次这么做,可以给A加个扩展,把绑定逻辑封装起来:
extension A { mutating func setupParentRelations() { items.forEach { $0.parent = self } } } // 使用时更简洁 var a = try JSONDecoder().decode(A.self, from: jsonData) a.setupParentRelations()
总结
- 必须用
weak引用反向关联的对象,否则会导致循环引用和内存泄漏。 - 追求自动化选方案1,追求简单快捷选方案2。
- 两种方案都避开了Codable的循环序列化问题,同时实现了A和B的双向关联。
内容的提问来源于stack exchange,提问作者Welton122
相关产品推荐
相关产品推荐

