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

JSONEncoder与PropertyListEncoder不遵循Encoder协议?能否泛型封装?

为什么JSONEncoder/PropertyListEncoder不遵循Encoder协议?如何泛型封装它们处理循环引用?

这确实是Swift Codable系统里一个容易让人困惑的点,我刚接触的时候也踩过类似的坑。先给你拆解清楚:

1. 顶层编码器和Encoder协议的定位差异

你说的没错,JSONEncoder和PropertyListEncoder确实没有遵循Encoder协议——因为它们是对外暴露的顶层便利工具,而Encoder协议是给内部编码上下文实例设计的。

拿JSONEncoder举例,它的核心编码逻辑其实是由私有内部类_JSONEncoder实现的,这个_JSONEncoder才是真正遵循Encoder协议的类型。对外的JSONEncoder更像一个"配置容器":你设置的dateEncodingStrategy、keyEncodingStrategy等参数,最终都会传递给内部的_JSONEncoder实例,由它来执行具体的编码流程。

Encoder协议的意义在于定义编码过程的统一接口:不管是JSON、Plist还是你自己写的自定义编码器,只要遵循这个协议,就能和Swift的Codable系统协作,让Encodable类型的encode(to:)方法能正常工作。它是编码逻辑的抽象,而不是顶层工具的抽象。

2. 如何泛型封装JSONEncoder/PropertyListEncoder?

既然直接用Encoder协议不行,我们可以自己定义一个顶层编码器的协议,来统一JSONEncoder和PropertyListEncoder的行为:

// 定义顶层编码器协议,匹配系统编码器的核心方法
protocol TopLevelEncoder {
    associatedtype Output
    func encode<T: Encodable>(_ value: T) throws -> Output
}

// 让系统编码器遵循这个协议
extension JSONEncoder: TopLevelEncoder {}
extension PropertyListEncoder: TopLevelEncoder {}

然后就可以基于这个协议做泛型封装了,比如你的循环引用处理编码器:

class ReferenceSafeEncoder<E: TopLevelEncoder> {
    private let underlyingEncoder: E
    // 用于跟踪已编码的对象,处理循环引用
    private var encodedObjects = [ObjectIdentifier: String]()
    private let referenceKey = "$ref"
    private let idKey = "id"

    init(encoder: E) {
        self.underlyingEncoder = encoder
    }

    func encode<T: Encodable>(_ value: T) throws -> E.Output {
        // 用自定义包装器拦截编码过程,处理循环引用
        let wrappedValue = ReferenceSafeWrapper(value: value, encoder: self)
        return try underlyingEncoder.encode(wrappedValue)
    }

    // 内部方法:生成对象唯一标识或返回已有引用ID
    func referenceId(for object: AnyObject) -> String {
        let id = ObjectIdentifier(object)
        if let existingId = encodedObjects[id] {
            return existingId
        }
        let newId = UUID().uuidString
        encodedObjects[id] = newId
        return newId
    }
}

// 自定义包装器,处理循环引用的编码逻辑
private struct ReferenceSafeWrapper<T: Encodable>: Encodable {
    let value: T
    unowned let encoder: ReferenceSafeEncoder<any TopLevelEncoder>

    func encode(to encoder: Encoder) throws {
        guard let object = value as? AnyObject else {
            // 非引用类型直接编码
            try value.encode(to: encoder)
            return
        }

        let id = self.encoder.referenceId(for: object)
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        if self.encoder.encodedObjects.count > 1 {
            // 如果是已编码过的对象,输出引用标记
            try container.encode(id, forKey: .reference)
        } else {
            // 第一次编码,输出完整对象+唯一ID
            try container.encode(id, forKey: .id)
            try value.encode(to: encoder)
        }
    }

    private enum CodingKeys: String, CodingKey {
        case id = "id"
        case reference = "$ref"
    }
}

这个例子里,我们通过包装器拦截编码过程,用ObjectIdentifier跟踪已编码的引用类型对象,第一次编码时输出完整内容+ID,后续遇到循环引用时输出引用ID,这样就能避免崩溃。

3. 关于循环引用的额外说明

要注意的是,JSON和Plist本身是不支持原生循环引用的,所以我们的处理本质是把循环引用转换成"ID+引用"的结构化格式,解码的时候也需要对应的逻辑来还原引用关系——这意味着你需要同时封装对应的解码器,在解码时根据引用ID查找已解码的对象。

总结

  • Encoder协议是编码上下文的抽象,不是顶层编码器的抽象,所以JSONEncoder不需要遵循它;
  • 可以通过自定义TopLevelEncoder协议来泛型封装系统编码器;
  • 处理循环引用需要在编码过程中跟踪对象,通过自定义包装器或Codable扩展来修改输出结构。

内容的提问来源于stack exchange,提问作者Alexander

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:28:03