NSCoding 和 Codable 可以共存吗?

Can NSCoding and Codable co-exist?

在测试新的 Codable 如何与 NSCoding 交互时,我整理了一个游乐场测试,涉及使用包含 Codable 结构的 Class 的 NSCoding。惠特

struct Unward: Codable {
    var id: Int
    var job: String
}

class Akward: NSObject, NSCoding {

    var name: String
    var more: Unward

    init(name: String, more: Unward) {
        self.name = name
        self.more = more
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(more, forKey: "more")
    }

    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
        more = aDecoder.decodeObject(forKey: "more") as? Unward ?? Unward(id: -1, job: "unk")
        super.init()
    }
}

var upone = Unward(id: 12, job: "testing")
var adone = Akward(name: "Adrian", more: upone)

以上内容已被 Playground 接受,不会产生任何编译器错误。

但是,如果我像这样尝试 Saving adone:

let encodeit = NSKeyedArchiver.archivedData(withRootObject: adone)

Playground 立即崩溃并出现错误:

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

为什么? 有没有办法让 NSCoding class 包含 Codable 结构?

您得到的实际错误是:

-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance

这是来自以下行:

aCoder.encode(more, forKey: "more")

问题的原因是more(类型Unward)不符合NSCoding。但是一个Swift struct 不能符合NSCoding。除了符合 NSCoding 之外,您还需要将 Unward 更改为扩展 NSObject 的 class。 None 这影响了符合 Codable 的能力。

这是您更新后的 classes:

class Unward: NSObject, Codable, NSCoding {
    var id: Int
    var job: String

    init(id: Int, job: String) {
        self.id = id
        self.job = job
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(id, forKey: "id")
        aCoder.encode(job, forKey: "job")
    }

    required init?(coder aDecoder: NSCoder) {
        id = aDecoder.decodeInteger(forKey: "id")
        job = aDecoder.decodeObject(forKey: "job") as? String ?? ""
    }
}

class Akward: NSObject, Codable, NSCoding {
    var name: String
    var more: Unward

    init(name: String, more: Unward) {
        self.name = name
        self.more = more
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(more, forKey: "more")
    }

    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
        more = aDecoder.decodeObject(forKey: "more") as? Unward ?? Unward(id: -1, job: "unk")
    }
}

你的测试值:

var upone = Unward(id: 12, job: "testing")
var adone = Akward(name: "Adrian", more: upone)

您现在可以归档和取消归档:

let encodeit = NSKeyedArchiver.archivedData(withRootObject: adone)
let redone = NSKeyedUnarchiver.unarchiveObject(with: encodeit) as! Akward

并且你可以编码和解码:

let enc = try! JSONEncoder().encode(adone)
let dec = try! JSONDecoder().decode(Akward.self, from: enc)

现有答案并没有真正解决互操作性问题,而是展示了如何从 NSCoding 迁移到 Codable

我有一个 use-case,但这不是一个选项,我确实需要在 Codable 上下文中使用 NSCoding。如果您好奇:我需要在我的 Mac 应用程序的 XPC 服务之间发送模型,并且这些模型包含 NSImages。我本可以制作一堆 serialize/deserialize 图像的 DTO,但那会是很多样板。此外,这是 属性 包装器的完美用例。

这是我想出的 属性 包装器:

@propertyWrapper
struct CodableViaNSCoding<T: NSObject & NSCoding>: Codable {
    struct FailedToUnarchive: Error { }

    let wrappedValue: T

    init(wrappedValue: T) { self.wrappedValue = wrappedValue }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)

        let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
        unarchiver.requiresSecureCoding = Self.wrappedValueSupportsSecureCoding

        guard let wrappedValue = T(coder: unarchiver) else {
            throw FailedToUnarchive()
        }

        unarchiver.finishDecoding()

        self.init(wrappedValue: wrappedValue)
    }

    func encode(to encoder: Encoder) throws {
        let archiver = NSKeyedArchiver(requiringSecureCoding: Self.wrappedValueSupportsSecureCoding)
        wrappedValue.encode(with: archiver)
        archiver.finishEncoding()
        let data = archiver.encodedData

        var container = encoder.singleValueContainer()
        try container.encode(data)
    }

    private static var wrappedValueSupportsSecureCoding: Bool {
        (T.self as? NSSecureCoding.Type)?.supportsSecureCoding ?? false
    }
}

下面是我为它编写的简单测试:

import Quick
import Nimble

import Foundation

@objc(FooTests_SampleNSCodingClass)
private class SampleNSCodingClass: NSObject, NSCoding {
    let a, b, c: Int

    init(a: Int, b: Int, c: Int) {
        self.a = a
        self.b = b
        self.c = c
    }

    required convenience init?(coder: NSCoder) {
        self.init(
            a: coder.decodeInteger(forKey: "a"),
            b: coder.decodeInteger(forKey: "b"),
            c: coder.decodeInteger(forKey: "c")
        )
    }

    func encode(with coder: NSCoder) {
        coder.encode(a, forKey: "a")
        coder.encode(b, forKey: "b")
        coder.encode(c, forKey: "c")
    }
}

@objc(FooTests_SampleNSSecureCodingClass)
private class SampleNSSecureCodingClass: SampleNSCodingClass, NSSecureCoding {
    static var supportsSecureCoding: Bool { true }
}

private struct S<T: NSObject & NSCoding>: Codable {
    @CodableViaNSCoding
    var sampleNSCodingObject: T
}

class CodableViaNSCodingSpec: QuickSpec {
    override func spec() {
        context("Used with a NSCoding value") {
            let input = S(sampleNSCodingObject: SampleNSCodingClass(a: 123, b: 456, c: 789))

            it("round-trips correctly") {
                let encoded = try JSONEncoder().encode(input)

                let result = try JSONDecoder().decode(S<SampleNSCodingClass>.self, from: encoded)

                expect(result.sampleNSCodingObject.a) == 123
                expect(result.sampleNSCodingObject.b) == 456
                expect(result.sampleNSCodingObject.c) == 789
            }
        }

        context("Used with a NSSecureCoding value") {
            let input = S(sampleNSCodingObject: SampleNSSecureCodingClass(a: 123, b: 456, c: 789))

            it("round-trips correctly") {
                let encoded = try JSONEncoder().encode(input)

                let result = try JSONDecoder().decode(S<SampleNSSecureCodingClass>.self, from: encoded)

                expect(result.sampleNSCodingObject.a) == 123
                expect(result.sampleNSCodingObject.b) == 456
                expect(result.sampleNSCodingObject.c) == 789
            }
        }
    }
}

一些注意事项:

  1. 如果你需要走另一条路(将 Codable 对象嵌入到 NSCoding 存档中),你可以使用添加到 [=19= 的现有方法]/NSDecoder

  2. 这是为每个对象创建一个新的存档。除了在 encoding/decoding 期间添加相当多的对象分配外,它还可能使结果膨胀(在我的测试中,空存档大约为 220 字节)。

  3. Codable 从根本上说比 NSCoding 更受限制。 Codable 以一种只能处理具有值语义的对象的方式实现。结果:

    • 具有别名的对象图(对同一对象的多次引用)将导致被反对的对象重复
    • 带循环的对象图永远无法解码(会无限递归)

    这意味着您不能真正围绕 NSCoder/NSCoder 类 制作 Encoder/Decoder 包装器(如 NSKeyedArchiver/NSKeyedUnarchiver),而不需要大量记账来检测这些场景和 fatalError。 (这也意味着您不能支持 archiving/unarchiving 任何通用 NSCoding 对象,只能支持那些没有别名或循环的对象)。这就是为什么我采用“制作独立存档并将其编码为 Data”方法的原因。