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

