使用Swift 4中的Decodable和继承

103

如果使用类继承会破坏类的可解性。例如,以下代码:

class Server : Codable {
    var id : Int?
}

class Development : Server {
    var name : String?
    var userId : Int?
}

var json = "{\"id\" : 1,\"name\" : \"Large Building Development\"}"
let jsonDecoder = JSONDecoder()
let item = try jsonDecoder.decode(Development.self, from:json.data(using: .utf8)!) as Development

print(item.id ?? "id is nil")
print(item.name ?? "name is nil") here
输出为:
1
name is nil

现在如果我反过来,名称会被解码,但ID不会。

class Server {
    var id : Int?
}

class Development : Server, Codable {
    var name : String?
    var userId : Int?
}

var json = "{\"id\" : 1,\"name\" : \"Large Building Development\"}"
let jsonDecoder = JSONDecoder()
let item = try jsonDecoder.decode(Development.self, from:json.data(using: .utf8)!) as Development

print(item.id ?? "id is nil")
print(item.name ?? "name is nil")

输出结果为:

id is nil
Large Building Development

而且你不能在两个类中同时使用Codable。


1
有趣。你向苹果提交了一个错误报告吗? - Code Different
1
这不是一个错误,而是一个“未记录的功能”。 :-) 解决方案(一半)的唯一参考在2017 WWDC“基础知识新特性”视频中,详细信息请参见下面的答案。 - Joshua Nozzi
7个回答

117

我认为在继承的情况下,您必须亲自实现Coding。也就是说,您必须在超类和子类中指定CodingKeys,并实现init(from:)encode(to:)。根据WWDC视频(大约在49:28左右,如下图所示),您必须使用超级编码器/解码器调用super。

WWDC 2017 Session 212 Screenshot at 49:28 (Source Code)

required init(from decoder: Decoder) throws {

  // Get our container for this subclass' coding keys
  let container = try decoder.container(keyedBy: CodingKeys.self)
  myVar = try container.decode(MyType.self, forKey: .myVar)
  // otherVar = ...

  // Get superDecoder for superclass and call super.init(from:) with it
  let superDecoder = try container.superDecoder()
  try super.init(from: superDecoder)

}
视频似乎没有展示编码部分(但对于encode(to:)部分,它使用的是container.superEncoder()),但在您的encode(to:)实现中,它的工作方式基本相同。我可以确认这在这个简单案例中有效(请参见下面的演示代码)。
我仍然在尝试解决一个更复杂的模型的一些奇怪行为,我正在将它从NSCoding转换过来,该模型有许多新的嵌套类型(包括structenum),显示出意外的nil行为,这“不应该”出现。只要意识到可能涉及嵌套类型的边缘情况即可。 编辑: 我在测试场景中发现嵌套类型正常运作。我现在怀疑存在某些自引用类的问题(考虑树节点的子代),其集合本身也包含该类的各种子类的实例。简单的自引用类的测试解码正常(即,没有子类),因此我现在集中精力研究为什么子类的案例失败。 更新:2017年6月25日: 最终我向苹果报告了这个错误。rdar://32911973-不幸的是,对于包含Subclass: Superclass元素的Superclass数组进行编码/解码循环将导致数组中的所有元素都被解码为Superclass(子类的init(from:)从未被调用,导致数据丢失或更糟)。
//: Fully-Implemented Inheritance

class FullSuper: Codable {

    var id: UUID?

    init() {}

    private enum CodingKeys: String, CodingKey { case id }

    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)

    }

    func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)

    }

}

class FullSub: FullSuper {

    var string: String?
    private enum CodingKeys: String, CodingKey { case string }

    override init() { super.init() }

    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)
        let superdecoder = try container.superDecoder()
        try super.init(from: superdecoder)

        string = try container.decode(String.self, forKey: .string)

    }

    override func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(string, forKey: .string)

        let superencoder = container.superEncoder()
        try super.encode(to: superencoder)

    }
}

let fullSub = FullSub()
fullSub.id = UUID()
fullSub.string = "FullSub"

let fullEncoder = PropertyListEncoder()
let fullData = try fullEncoder.encode(fullSub)

let fullDecoder = PropertyListDecoder()
let fullSubDecoded: FullSub = try fullDecoder.decode(FullSub.self, from: fullData)

fullSubDecoded 中恢复了超类和子类属性。


5
目前通过将基类转换为协议并在协议扩展中添加默认实现,使得派生类符合该协议的要求来解决这个问题。 - Charlton Provatas
14
实际上,container.superDecoder()是不必要的。super.init(from: decoder)就足够了。 - Lal Krishna
10
我正在运行Swift 4.1的代码。在使用superDecoder时出现了异常。但是使用super.init(from: decoder)可以正常工作。 - Lal Krishna
2
在编码时,try super.encode(to: container.superEncoder()) 添加了一个超级键。 - Divyesh Makwana
1
如果你需要为每个子类编写这么多的初始化代码,那么你不妨复制粘贴你计划继承的所有变量,这样可以节省更多的行数和时间。 - 6rchid
显示剩余13条评论

32

发现这个链接 - 滚动到继承部分

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

解码时我做了这个:

 required init(from decoder: Decoder) throws {

    try super.init(from: decoder)

    let values = try decoder.container(keyedBy: CodingKeys.self)
    total = try values.decode(Int.self, forKey: .total)
  }

private enum CodingKeys: String, CodingKey
{
    case total

}

2
不错的博客文章!谢谢分享。 - Thomás Pereira
1
如果您想将一个带有Codable子类类型的变量保存到UserDefaults中,那么这个答案实际上比被接受的答案更好用。 - Tamás Sengel
1
这是这里最好的答案。 - Lirik
请在可选值上使用“try?”进行尝试。 - deeJ

12
Swift在5.1版本中引入了属性包装器。我实现了一个名为SerializedSwift的库,利用属性包装器的功能将JSON数据解码和编码为对象。
我的主要目标之一是,使得继承的对象能够直接解码,而无需额外的init(from decoder: Decoder)重写。
import SerializedSwift

class User: Serializable {

    @Serialized
    var name: String
    
    @Serialized("globalId")
    var id: String?
    
    @Serialized(alternateKey: "mobileNumber")
    var phoneNumber: String?
    
    @Serialized(default: 0)
    var score: Int
    
    required init() {}
}

// Inherited object
class PowerUser: User {
    @Serialized
    var powerName: String?

    @Serialized(default: 0)
    var credit: Int
}

它还支持自定义编码键、备用键、默认值、自定义转换类以及未来将包含的许多其他功能。
GitHub (SerializedSwift)上可用。

看起来不错。这个也可以用来编码/解码XML吗?(或者您计划在未来包括它吗?) - Jens
1
@Jens 绝对是可能的。最初的计划是完善 API 和所有 JSON 序列化用例,然后添加 XML 就不会那么困难了。 - Dejan Skledar
谢谢!我在Github上收藏了你的项目。目前我选择了MaxDesiatov /XMLCoder,但它看起来确实很有趣! - Jens
@JoshuaNozzi 谢谢 :) 我希望通过增加新功能来升级项目,以减轻开发人员在标准JSON解码方面的痛苦。 - Dejan Skledar
1
这看起来更像是我所期望的。 - Eman
1
这让我省了很多苦力活。谢谢,伙计! - Renegade

5
我将基类和子类都改为遵守Decodable而不是Codable,这样就能使代码正常运行。如果使用Codable,程序会出现奇怪的崩溃,例如在访问子类字段时出现EXC_BAD_ACCESS,但调试器可以正常显示所有子类值。
此外,在super.init()中将超级解码器传递给基类无法正常工作。我只是将子类的解码器传递给了基类。

同样的技巧:将superDecoder传递给super.init()中的基类并不起作用。我只是将子类的解码器传递给了基类。 - Jack Song
遇到了同样的问题。有没有办法在不完全实现编码/解码方法的情况下解决这个问题?谢谢。 - Doro
1
尝试了这个解决方案,但它不再被允许 => XYZModel 的协议 Decodable 冗余符合。 - Ahmad Mahmoud Saleh

5
以下方式如何使用?
protocol Parent: Codable {
    var inheritedProp: Int? {get set}
}

struct Child: Parent {
    var inheritedProp: Int?
    var title: String?

    enum CodingKeys: String, CodingKey {
        case inheritedProp = "inherited_prop"
        case title = "short_title"
    }
}

关于组合的额外信息:http://mikebuss.com/2016/01/10/interfaces-vs-inheritance/


4
这如何解决异构数组解码的问题? - Joshua Nozzi
2
只是为了明确,这并不是挖苦的批评。我一直在回顾存储杂集集合的问题,但无济于事。最好使用通用解决方案,这意味着我们在解码时无法知道类型。 - Joshua Nozzi
在 Xcode 中,点击“帮助” > “开发者文档”,然后搜索一个很棒的文章,名为“编码和解码自定义类型”。我认为阅读那篇文章会对你有所帮助。 - Tommie C.
@Natanel,你的父类是否符合Codable协议?如果不符合,请进行修改。 - Nav
4
这不是作文。 - mxcl
显示剩余3条评论

5
这里有一个库TypePreservingCodingAdapter,可以做到这一点(可以使用Cocoapods或SwiftPackageManager安装)。
下面的代码可以与Swift 4.2编译并正常工作。不幸的是,对于每个子类,您都需要自己实现属性的编码和解码。
import TypePreservingCodingAdapter
import Foundation

// redeclared your types with initializers
class Server: Codable {
    var id: Int?

    init(id: Int?) {
        self.id = id
    }
}

class Development: Server {
    var name: String?
    var userId: Int?

    private enum CodingKeys: String, CodingKey {
        case name
        case userId
    }

    init(id: Int?, name: String?, userId: Int?) {
        self.name = name
        self.userId = userId
        super.init(id: id)
    }

    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        let container = try decoder.container(keyedBy: CodingKeys.self)

        name = try container.decodeIfPresent(String.self, forKey: .name)
        userId = try container.decodeIfPresent(Int.self, forKey: .userId)
    }

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

        try container.encode(name, forKey: .name)
        try container.encode(userId, forKey: .userId)
    }

}

// create and adapter
let adapter = TypePreservingCodingAdapter()
let encoder = JSONEncoder()
let decoder = JSONDecoder()

// inject it into encoder and decoder
encoder.userInfo[.typePreservingAdapter] = adapter
decoder.userInfo[.typePreservingAdapter] = adapter

// register your types with adapter
adapter.register(type: Server.self).register(type: Development.self)


let server = Server(id: 1)
let development = Development(id: 2, name: "dev", userId: 42)

let servers: [Server] = [server, development]

// wrap specific object with Wrap helper object
let data = try! encoder.encode(servers.map { Wrap(wrapped: $0) })

// decode object back and unwrap them force casting to a common ancestor type
let decodedServers = try! decoder.decode([Wrap].self, from: data).map { $0.wrapped as! Server }

// check that decoded object are of correct types
print(decodedServers.first is Server)     // prints true
print(decodedServers.last is Development) // prints true

0

Swift 5

编译器仅为直接采用 Codable 协议的类型合成可解码的代码,因此您可以观察继承中单个类型的解码。

但是,您可以尝试使用 KeyValueCoding 包(https://github.com/ikhvorost/KeyValueCoding)进行下一步通用方法,该包提供对所有属性元数据的访问,并允许动态获取/设置纯 Swift 类型的任何属性。这个想法是制作一个基本的 Coding 类,它采用了 KeyValueCoding 并在 init(from: Decoder) 中实现所有可用属性的解码:

class Coding: KeyValueCoding, Decodable {
    
    typealias DecodeFunc = (KeyedDecodingContainer<_CodingKey>, _CodingKey) throws -> Any?
    
    struct _CodingKey: CodingKey {
      let stringValue: String
      let intValue: Int?

      init(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = Int(stringValue)
      }

      init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
      }
    }
    
    static func decodeType<T: Decodable>(_: T.Type) -> (type: T.Type, f: DecodeFunc)  {
        (T.self,  { try $0.decode(T.self, forKey: $1) })
    }
    
    static var decodeTypes: [(Any.Type, DecodeFunc)] = [
        decodeType(Int.self),
        decodeType(Int?.self),
        decodeType(String.self),
        decodeType(String?.self),
        // Other types to support...
    ]
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: _CodingKey.self)
        try container.allKeys.forEach { codingKey in
            let key = codingKey.stringValue
            guard let property = (properties.first { $0.name == key }),
                let item = (Self.decodeTypes.first { property.type == $0.0 })
            else {
                return
            }
            var this = self
            this[key] = try item.1(container, codingKey)
        }
    }
}

decodeTypes变量中提供所有支持的解码类型是很重要的。

使用方法:

class Server: Coding {
    var id: Int?
}

class Development : Server {
    var name: String = ""
}

class User: Development {
    var userId: Int = 0
}

func decode() {
    let json = "{\"id\": 1, \"name\": \"Large Building Development\", \"userId\": 123}"
    do {
        let user = try JSONDecoder().decode(User.self, from:json.data(using: .utf8)!)
        print(user.id, user.name, user.userId) // Optional(1) Large Building Development 123
    }
    catch {
        print(error.localizedDescription)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接