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

如何使用Codable序列化存在引用循环的自定义类?

用Codable处理存在引用循环的自定义类

当然可以用Codable搞定有循环引用的自定义类!你遇到的EXC_BAD_ACCESS确实是循环引用导致的——Codable默认的自动合成实现会无限递归遍历引用关系,最终引发内存访问错误。下面我给你一步步拆解解决方法:

核心思路:通过唯一标识符打破递归

Codable自动合成的编码/解码逻辑会直接遍历所有属性,遇到循环引用时就会无限递归下去。我们的解决办法是:给每个实例分配一个唯一标识符,编码时只存标识符而非整个实例;解码时先把所有实例创建出来,再根据标识符关联引用。

1. 给类添加唯一标识符

首先,给你的自定义类加一个uuid属性,用来唯一标记每个实例:

import Foundation

class Person: Codable {
    let uuid = UUID()
    let name: String
    var spouse: Person? // 强引用形成循环
    
    init(name: String) {
        self.name = name
    }
    
    // 接下来手动实现Codable方法
}

2. 自定义编码逻辑

在encode(to:)方法里,我们不直接编码spouse实例,而是编码它的uuid(如果存在的话):

// 定义编码键
enum CodingKeys: String, CodingKey {
    case uuid, name, spouseUUID
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(uuid, forKey: .uuid)
    try container.encode(name, forKey: .name)
    // 只编码spouse的uuid,而非整个实例
    try container.encode(spouse?.uuid, forKey: .spouseUUID)
}

3. 分两步完成解码

解码的关键是先创建所有实例,再关联引用,我们需要借助Decoder的userInfo来传递一个临时字典,用来存储已解码的实例:

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.uuid = try container.decode(UUID.self, forKey: .uuid)
    self.name = try container.decode(String.self, forKey: .name)
    
    // 先暂存spouse的uuid,后面再关联
    let spouseUUID = try container.decodeIfPresent(UUID.self, forKey: .spouseUUID)
    
    // 从userInfo中取出临时字典,把当前实例存进去
    guard let instancesDict = decoder.userInfo[CodingUserInfoKey(rawValue: "PersonInstances")!] as? NSMutableDictionary else {
        throw DecodingError.dataCorruptedError(
            forKey: .uuid,
            in: container,
            debugDescription: "No instances dictionary provided in decoder userInfo"
        )
    }
    instancesDict[uuid] = self
    
    // 根据uuid从字典中找到对应的spouse实例,完成关联
    if let spouseUUID = spouseUUID {
        self.spouse = instancesDict[spouseUUID] as? Person
    }
}

4. 完整的编码解码流程

编码直接用JSONEncoder即可,解码时要先创建临时字典并传入JSONDecoder的userInfo:

// 创建带循环引用的实例
let alice = Person(name: "Alice")
let bob = Person(name: "Bob")
alice.spouse = bob
bob.spouse = alice

// 编码
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode([alice, bob])
print(String(data: data, encoding: .utf8)!)

// 解码
let decoder = JSONDecoder()
let instancesDict = NSMutableDictionary()
// 把临时字典存入userInfo
decoder.userInfo[CodingUserInfoKey(rawValue: "PersonInstances")!] = instancesDict
let decodedPeople = try decoder.decode([Person].self, from: data)

// 验证引用关系
print(decodedPeople[0].spouse?.name ?? "No spouse") // 输出 Bob
print(decodedPeople[1].spouse?.name ?? "No spouse") // 输出 Alice

扩展说明

  • 如果是多类之间的复杂循环引用,只需要给每个涉及的类都添加uuid,解码时为每个类维护对应的实例字典,或者用一个统一的[UUID: Any]字典存储所有实例
  • 要是不想手动写所有Codable逻辑,也可以用一些第三方库简化,但手动实现的方式最灵活,能完全掌控整个流程
  • 注意:值类型(struct)不会有循环引用问题,只有引用类型(class)才需要处理这种场景

内容的提问来源于stack exchange,提问作者A. Vage

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.20 06:55:17