在 Swift 4 中使用 Decodable 和继承

Posted

技术标签:

【中文标题】在 Swift 4 中使用 Decodable 和继承【英文标题】:Using Decodable in Swift 4 with Inheritance 【发布时间】:2017-11-17 03:31:11 【问题描述】:

如果使用类继承会破坏类的可解码性。比如下面的代码

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

现在如果我反转这个,name 会解码,但 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。

【问题讨论】:

有趣。您是否向 Apple 提交了错误? 这不是一个错误,它实际上是一个“未记录的功能”。 :-) 该解决方案的唯一参考(一半)是在 2017 年 WWDC“基金会的新功能”视频中,我在下面的回答中有详细说明。 【参考方案1】:

我相信在继承的情况下你必须自己实现Coding。也就是说,您必须在超类和子类中指定CodingKeys 并实现init(from:)encode(to:)。根据WWDC video(大约 49:28,如下图),您必须使用 super 编码器/解码器调用 super。

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 行为和“不应该”。请注意,可能存在涉及嵌套类型的边缘情况。

编辑: 嵌套类型似乎在我的测试操场上运行良好;我现在怀疑自引用类(想想树节点的子节点)有问题,它本身的集合还包含该类的各种子类的实例。一个简单的自引用类的测试可以很好地解码(即没有子类),所以我现在将精力集中在子类案例失败的原因上。

2017 年 6 月 25 日更新:我最终向 Apple 提交了一个关于此问题的错误。 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 中恢复。

【讨论】:

现在能够通过将基类转换为协议并将默认实现添加到协议扩展并使派生类符合它来解决这个问题 和查尔顿一样。使用基类解码时遇到 EXC_BAD_ACCESS 错误。不得不转向协议结构来解决它。 其实container.superDecoder()不需要。 super.init(from: decoder) 就足够了 我运行代码 swift 4.1。我在使用 superDecoder 时遇到了异常。与super.init(from: decoder) 一起工作正常 try super.encode(to: container.superEncoder()) 在编码时添加了超级密钥【参考方案2】:

Found This Link - Go down to inheritance section

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


【讨论】:

不错的博文!谢谢分享。 如果您想将具有Codable 子类类型的变量保存到 UserDefaults,此答案实际上比接受的答案更好。 这是最好的答案。【参考方案3】:

? 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 绝对是可能的。最初的计划是完善 API 和 JSON 序列化的所有用例,然后添加 XML 不会那么难。 谢谢!我在 github 上为你的项目加注了星标。我现在选择了MaxDesiatov /XMLCoder,但它看起来确实很有趣! @JoshuaNozzi 谢谢 :) 我希望用新功能升级项目,以减轻开发人员对标准 JSON 解码的痛苦【参考方案4】:

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

此外,将 superDecoder 传递给super.init() 中的基类也不起作用。我只是将解码器从子类传递到基类。

【讨论】:

同样的技巧:将 superDecoder 传递给 super.init() 中的基类不起作用。我只是将解码器从子类传递到基类。 面临同样的问题。有没有办法在不完全实现编码/解码方法的情况下解决这个问题?谢谢 试过这个解决方案,但现在不允许了 => Redundant conformance of 'XYZModel' to protocol 'Decodable'【参考方案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

【讨论】:

【参考方案6】:

使用下面的方式怎么样?

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/

【讨论】:

如何解决异构数组的解码问题? 明确一点,这不是尖刻的批评。我一直在重新审视存储异构集合的问题,但无济于事。通用解决方案是最好的,这意味着我们无法在解码时知道类型。 在 Xcode 的 Help > Developer Documentation 下,搜索一篇很棒的文章,名为“Encoding and Decoding Custom Types”。我认为阅读会对您有所帮助。 我正在尝试执行此操作,但在对存储在数组中的数据进行编码时,我不断收到运行时错误。 “致命错误:Array 不符合 Encodable,因为 Parent 不符合 Encodable。”有什么帮助吗? 这不是作曲。

以上是关于在 Swift 4 中使用 Decodable 和继承的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 4 中使用 Decodable 解析 JSON

处理包含多种类型的 JSON 数组 - Swift 4 Decodable

使用 Swift 4 的 Decodable 解码 Void

Swift 4 Decodable - 没有与键 CodingKeys 关联的值 [重复]

使用 Swift 4 Decodable 将字符串 JSON 响应转换为布尔值

如何在 Swift 4 中将 Encodable 或 Decodable 作为参数传递?