如何编码和解码带有派生关系的model

2020-04-26  本文已影响0人  醉看红尘这场梦

这一节,我们来看当model存在继承关系的时候,是如何与JSON完成自动转换的,它的默认行为,和我们想象的有点儿不太一样。

假设,我们有下面这两个类:

class Point2D: Codable {
    var x = 0.0
    var y = 0.0
}

class Point3D: Point2D {
    var z = 0.0
}

由于这两个类包含的都是简单属性,我们希望直接让它们遵从Codable,实现JSON到model的自动转化,但事情却并不如我们想象的这么简单。在接下来的内容里,为了方便观察对象编码的结果,我们定义了一个全局encode函数,在后面的视频中,我们也会使用它:

func encode<T>(of model: T) throws where T: Codable {

    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted

    let data = try encoder.encode(model)
    print(String(data: data, encoding: .utf8)!)
}

这基本就是我们之前手动编码对象的封装,很简单。唯一要注意的就是我们在最后,对泛型参数T进行了类型约束,要求它遵从Codable。接下来,我们创建一个Point3D对象,然后对它编码:

var p1 = Point3D()
p1.x = 1
p1.y = 1
p1.z = 1

encode(of: p1)

这段代码会得到什么结果呢?你可能会想,当然是包含xyz的JSON啊。但执行一下,你会看到这样的结果:

{
  "x" : 1,
  "y" : 1
}

z呢?如果你第一次执行这段代码,一定会感到奇怪。默认Codable中的默认encode方法并不能正确处理派生类对象。因此,当我们的model是派生类时,要自己编写对应的编码和解码的方法。

先来看编码,我们先实现基类的部分:

class Point2D: Codable {
    var x = 0.0
    var y = 0.0

    private enum CodingKeys: String, CodingKey {
        case x
        case y
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(x, forKey: .x)
        try container.encode(y, forKey: .y)
    }
}

唯一需要注意的,就是基类中的CodingKeys被修饰成了private。因为派生类中,也要定义它自己的CodingKeys,我们要避免它被派生类继承。

然后是派生类的部分:

class Point3D: Point2D {
    var z = 0.0

    private enum CodingKeys: String, CodingKey {
        case z
    }

    override func encode(to encoder: Encoder) throws {
        var container =
            encoder.container(keyedBy: CodingKeys.self)
        try container.encode(z, forKey: .z)
    }
}

现在,你应该想:这下总该没问题了吧。但如果执行下,你就会发现这样的结果:

{
  "z" : 1
}

这时,也许你马上就意识到问题所在了,在派生类的encode方法里,先调用基类版本的encode就应该正确了:

override func encode(to encoder: Encoder) throws {
    try super.encode(to: encoder)
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(z, forKey: .z)
}

重新执行一下,就会得到正确的结果了:

{
  "x" : 1,
  "y" : 1,
  "z" : 1
}

在派生类的实现里,基类和派生类的属性共享了同一个container。但这样的方式并不利于我们了解其中哪部分属于基类,哪部分属于派生类。为了能在编码后的结果中方便区分,我们得让它们使用不同的容器:

override func encode(to encoder: Encoder) throws {
    var container =
        encoder.container(keyedBy: CodingKeys.self)
    try super.encode(to: container.superEncoder())

    try container.encode(z, forKey: .z)
}

这里,我们使用了container.superEncoder()获得了派生类中,专门用于编码基类的容器,并把它传递给了基类的编码方法。现在,重新执行一下,我们会看到下面这样的结果:

{
  "super" : {
    "x" : 1,
    "y" : 1
  },
  "z" : 1
}

其中,基类和派生类的部分就很清楚了。但我们还可以进一步改进这个结果,把派生类中的CodingKeys改成这样:

private enum CodingKeys: String, CodingKey {
    case z
    case point_2d
}

然后,在派生类的encode方法里,指定编码基类时的key:

override func encode(to encoder: Encoder) throws {
    var container =
        encoder.container(keyedBy: CodingKeys.self)
    try super.encode(to:
        container.superEncoder(forKey: .point_2d))

    try container.encode(z, forKey: .z)
}

重新执行下,结果就会这样:

{
  "z" : 1,
  "point_2d" : {
    "x" : 1,
    "y" : 1
  }
}

理解了编码的过程之后,解码的过程是类似的,无非就是把共享容器,或使用基类独立容器的代码写在init(from decoder: Decoder)方法里,大家可以试着自己写写,我们就不再重复了。

上一篇下一篇

猜你喜欢

热点阅读