84 votes

Utilisation de Decodable dans Swift 4 avec Inheritance

L'utilisation de l'héritage de classe doit-elle briser la décodabilité de la classe. Par exemple, le code suivant

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

La sortie est :

1
name is nil

Maintenant, si je fais l'inverse, le nom est décodé mais pas l'identité.

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")

La sortie est :

id is nil
Large Building Development

Et vous ne pouvez pas exprimer Codable dans les deux classes.

1 votes

Intéressant. Avez-vous signalé un bug à Apple ?

1 votes

Ce n'est pas un bug, c'est littéralement une "fonctionnalité non documentée". :-) La seule référence à (la moitié de) la solution était dans la vidéo WWDC 2017 "What's New In Foundation", détaillée dans ma réponse ci-dessous.

101voto

Joshua Nozzi Points 38718

Je crois que dans le cas d'un héritage, vous devez mettre en œuvre Coding vous-même. C'est-à-dire que vous devez spécifier CodingKeys et mettre en œuvre init(from:) y encode(to:) dans la superclasse et la sous-classe. Selon la Vidéo de la WWDC (vers 49:28, image ci-dessous), vous devez appeler super avec l'encodeur/décodeur 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)

}

La vidéo ne semble pas montrer l'aspect encodage (mais c'est container.superEncoder() pour le encode(to:) ) mais cela fonctionne de la même manière dans votre encode(to:) mise en œuvre. Je peux confirmer que cela fonctionne dans ce cas simple (voir le code du terrain de jeu ci-dessous).

Je suis moi-même toujours aux prises avec un comportement étrange dans un modèle beaucoup plus complexe que je suis en train de convertir. NSCoding qui comporte un grand nombre de types nouvellement imbriqués (notamment struct y enum ) qui présente ce phénomène inattendu nil comportement et "ne devrait pas être". Sachez simplement qu'il peut y avoir des cas limites impliquant des types imbriqués.

Edit : Les types imbriqués semblent fonctionner correctement dans mon environnement de test ; je soupçonne maintenant que quelque chose ne va pas avec les classes auto-référencées (pensez aux enfants des nœuds d'arbre) avec une collection d'elle-même qui contient également des instances des différentes sous-classes de cette classe. Un test d'une simple classe auto-référencée se décode bien (c'est-à-dire qu'il n'y a pas de sous-classes). Je concentre donc mes efforts sur la raison pour laquelle le cas des sous-classes échoue.

Mise à jour du 25 juin 17 : J'ai fini par déposer un bug auprès d'Apple à ce sujet. rdar://32911973 - Malheureusement, un cycle de codage/décodage d'un tableau de Superclass qui contient Subclass: Superclass aura pour résultat que tous les éléments du tableau seront décodés en tant que Superclass (la "sous-classe init(from:) n'est jamais appelé, ce qui entraîne une perte de données ou pire).

//: 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)

Les propriétés super- et subclass sont rétablies dans le fichier fullSubDecoded .

4 votes

J'ai pu contourner le problème pour l'instant en convertissant la classe de base en un protocole et en ajoutant des implémentations par défaut à l'extension du protocole pour que la classe dérivée s'y conforme.

0 votes

Même chose que Charlton. Je rencontrais des erreurs EXC_BAD_ACCESS lors du décodage avec une classe de base. J'ai dû passer à une structure de protocole pour contourner ce problème.

7 votes

En fait, container.superDecoder() n'est pas nécessaire. super.init(from : decoder) est suffisant

23voto

devjme Points 146

Trouvé ce lien - Descendez à la section héritage

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)
}

Pour le décodage, j'ai fait ça :

 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 votes

Bel article de blog ! Merci de le partager.

0 votes

Cette réponse fonctionne mieux que la réponse acceptée si vous voulez sauvegarder une variable avec une balise Codable type de sous-classe à UserDefaults.

0 votes

C'est la meilleure réponse ici.

5voto

Igor Muzyka Points 176

Voici une bibliothèque TypePreservingCodingAdapter pour faire exactement cela (peut être installé avec Cocoapods ou SwiftPackageManager).

Le code ci-dessous compile et fonctionne parfaitement avec Swift 4.2 . Malheureusement, pour chaque sous-classe, vous devrez implémenter vous-même l'encodage et le décodage des propriétés.

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

4voto

ShackBurger Points 51

J'ai réussi à le faire fonctionner en faisant en sorte que ma classe de base et mes sous-classes se conforment aux règles suivantes Decodable au lieu de Codable . Si j'ai utilisé Codable il se plantait de façon étrange, par exemple en obtenant une EXC_BAD_ACCESS lors de l'accès à un champ de la sous-classe, alors que le débogueur pouvait afficher toutes les valeurs de la sous-classe sans problème.

De plus, le fait de passer le superDecoder à la classe de base dans le fichier super.init() n'a pas fonctionné. J'ai juste passé le décodeur de la sous-classe à la classe de base.

0 votes

Même astuce : passer le superDecoder à la classe de base dans super.init() n'a pas fonctionné. J'ai juste passé le décodeur de la sous-classe à la classe de base.

0 votes

J'ai rencontré le même problème. Y a-t-il un moyen de résoudre ce problème sans implémenter complètement les méthodes de codage/décodage ? Merci.

0 votes

J'ai essayé cette solution, mais elle n'est plus autorisée. Redundant conformance of 'XYZModel' to protocol 'Decodable'

4voto

Nav Points 1699

Et si vous utilisiez la méthode suivante ?

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"
    }
}

Informations supplémentaires sur la composition : http://mikebuss.com/2016/01/10/interfaces-vs-inheritance/

4 votes

Comment cela résout-il le problème du décodage d'un réseau hétérogène ?

2 votes

Juste pour être clair, ce n'était pas une critique sarcastique. Je reviens sans cesse sur le problème du stockage de collections hétérogènes, en vain. Une solution générique est la meilleure, ce qui signifie que nous ne pouvons pas connaître les types au moment du décodage.

0 votes

Dans Xcode, sous Aide > Documentation du développeur, recherchez un excellent article intitulé "Encodage et décodage des types personnalisés". Je pense que la lecture de cet article vous aidera.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X