在带有继承的Swift 4中使用Decodable


72

使用类继承是否会破坏类的可解码性。例如下面的代码

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
有趣。您是否向Apple提出了错误?
代码不同

1
这不是错误,实际上是“未记录的功能”。:-)该解决方案的(一半)唯一引用是在2017年WWDC“基础上的新功能”视频中,下面将详细介绍我的答案。
Joshua Nozzi

Answers:


86

我相信在继承的情况下,您必须实现Coding自己。也就是说,你必须指定CodingKeys和执行init(from:),并encode(to:)在这两个超和子类。对于WWDC视频(约49:28,如下图所示),您必须使用超级编码器/解码器调用超级。

WWDC 2017 Session 212屏幕快照49:28(源代码)

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)

}

该视频似乎没有停止显示编码方面(但是是container.superEncoder()针对编码方面的encode(to:)),但是在encode(to:)实现过程中其工作方式几乎相同。我可以在这种简单的情况下确认这项工作(请参见下面的操场代码)。

我仍在自己的怪异行为中挣扎NSCoding,它要转换的模型要复杂得多,该模型具有许多新嵌套的类型(包括structenum),这些类型表现出这种意外的nil行为,并且“不应该”。请注意,可能存在涉及嵌套类型的边缘情况。

编辑:嵌套类型似乎在我的测试操场上工作正常;我现在怀疑自引用类(认为是树节点的子代)有问题,而自引用类本身也包含该类的各种子类的实例。一个简单的自引用类的测试可以很好地解码(即没有子类),因此我现在将精力集中在子类案例失败的原因上。

更新时间:17年6月25日:我最终向苹果提交了一个有关此问题的错误。rdar:// 32911973-不幸的是Superclass,包含Subclass: Superclass元素的数组的编码/解码周期将导致数组中的所有元素被解码为Superclassinit(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)

super和subclass属性都在中还原fullSubDecoded


4
通过将基类转换为协议并向协议扩展添加默认实现并使派生类符合该协议,现在能够解决该问题
Charlton Provatas

与查尔顿相同。使用基类进行解码时遇到EXC_BAD_ACCESS错误。必须转向协议结构来解决它。
哈里·布鲁姆

5
其实container.superDecoder()不需要。super.init(来自:解码器)够了
Lal Krishna

@LalKrishna资料来源?我遵循苹果工程师的建议。
Joshua Nozzi

4
我运行代码swift 4.1。而且在使用superDecoder时出现异常。并且与super.init(from: decoder)
Lal Krishna的

18

找到此链接-转到继承部分

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

}

1
不错的博客文章!谢谢你的分享。
托马斯·卡尔蒙

如果要将Codable子类类型的变量保存到UserDefaults,此答案实际上比接受的答案更好。
塔玛斯·森格尔(TamásSengel),

6

这是一个库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

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
这如何解决解码异构数组的问题?
约书亚·诺齐

1
需要明确的是,这不是刻板的批评。我一直在讨论无用的存储异构集合的问题。通用解决方案是最好的,这意味着我们在解码时无法知道类型。
Joshua Nozzi

在“帮助”>“开发人员文档”下的“ Xcode”中,搜索名为“编码和解码自定义类型”的出色文章。我认为阅读对您有帮助。
Tommie C.

我正在尝试执行此操作,但是在对存储在数组中的数据进行编码时,我一直遇到运行时错误。“致命错误:Array <Parent>不符合Encodable,因为Parent不符合Encodable。” 有什么帮助吗?
Natanel '18年

3
这不是组成。
mxcl

4

通过使我的基类和子类符合Decodable而不是,我能够使其工作Codable。如果使用Codable它,它将以奇怪的方式崩溃,例如EXC_BAD_ACCESS在访问子类的字段时得到a ,但是调试器可以毫无问题地显示所有子类的值。

此外,将superDecoder传递给in中的基类super.init()无效。我只是将解码器从子类传递到基类。


同样的技巧:将superDecoder传递给super.init()中的基类不起作用。我只是将解码器从子类传递到基类。
Jack Song

面临同样的问题。没有完全实现编码/解码方法,有什么方法可以解决这个问题?谢谢
多罗

3

🚀Swift在5.1中引入了Property Wrappers,我实现了一个名为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

@Jens绝对有可能。最初的计划是完善API和JSON序列化的所有用例,然后添加XML不会那么困难。
Dejan Skledar

谢谢!我在github上注视了您的项目。我现在使用MaxDesiatov / XMLCoder,但是看起来确实很有趣!
詹斯

-2
class MessageDecodable:Decodable {
    var message: String?
    var error_code: Int?
}

class PassengerDecodable:MessageDecodable{
    var passenger:TokenDecodable?
}

class TokenDecodable:Decodable {
    var token: String?
    var created: String?
}

2
尽管这段代码可以解决问题,但包括解释如何以及为什么解决该问题的说明,确实可以帮助提高您的帖子质量,并可能导致更多的投票。请记住,您将来会为读者回答问题,而不仅仅是现在问的人。请编辑您的答案以添加说明,并指出适用的限制和假设。评分
嘟嘟声
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.