Swift Codable - как инициализировать необязательное свойство Enum в способе отказа - PullRequest
0 голосов
/ 06 ноября 2018

Я пытаюсь принять протокол Codable для объекта, который должен быть создан из JSON, который мой веб-сервис возвращает в ответ на один из вызовов API.

Одно из свойств имеет тип перечисление и является необязательным: nil означает, что не выбран ни один из параметров, определенных enum.

Константы enum основаны на Int и начинаются с 1, , а не 0:

class MyClass: Codable {

    enum Company: Int {
        case toyota = 1
        case ford
        case gm
    } 
    var company: Company?

Это потому, что значение 0 в соответствующей записи JSON зарезервировано для «не установлено»; т.е. он должен быть сопоставлен с nil при настройке инициализации свойства company из него.

Инициализатор перечисления Swift init?(rawValue:) предоставляет эту функциональность "из коробки": аргумент Int, который не соответствует необработанному значению любого случая, приведет к сбою инициализатора и возврату nil. Кроме того, перечисления на основе Int (и String) можно сделать так, чтобы они соответствовали Codable, просто объявив его в определении типа:

enum Company: Int, Codable {
    case toyota = 1
    case ford
    case gm
} 

Проблема в том, что мой пользовательский класс имеет более 20 свойств, поэтому я действительно очень хочу избежать реализации init(from:) и encode(to:), полагаясь вместо этого на автоматическое поведение, полученное путем предоставления CondingKeys пользовательский тип перечисления.

Это приводит к сбою инициализации всего экземпляра класса, поскольку кажется, что «синтезированный» инициализатор не может сделать вывод, что неподдерживаемое необработанное значение типа enum должно рассматриваться как nil (даже если цель свойство четко помечено как необязательно , т.е. Company?).

Я думаю, что это так, потому что инициализатор, предоставленный Decodable, может выдать, но не может вернуть nil:

// This is what we have:
init(from decoder: Decoder) throws

// This is what I would want:
init?(from decoder: Decoder)

В качестве обходного пути я реализовал свой класс следующим образом: отобразить целочисленное свойство JSON в private , хранимое Int свойство моего класса, которое служит только для хранения, и ввести строго типизированное вычисляемое свойство, которое действует как мост между хранилищем и остальной частью моего приложения:

class MyClass {

   // (enum definition skipped, see above)

   private var companyRawValue: Int = 0

   public var company: Company? {
       set {
           self.companyRawValue = newValue?.rawValue ?? 0
           // (sets to 0 if passed nil)
       }
       get {
           return Company(rawValue: companyRawValue)
           // (returns nil if raw value is 0)
       }
   }

   enum CodingKeys: String, CodingKey {
       case companyRawValue = "company"
   }

   // etc...

Мой вопрос: есть ли лучший (более простой / более элегантный) способ , который:

  1. Не требует ли повторяющиеся свойства, такие как мой обходной путь, и
  2. Не требует ли * полной реализации init(from:) и / или encode(with:), возможно, реализуя их упрощенные версии, которые по большей части делегируют поведение по умолчанию (т. Е. Не требуют всей совокупности вручную инициализация / кодирование каждого свойства)?

Приложение: Существует третье, также не элегантное решение, которое мне не пришло в голову, когда я впервые опубликовал вопрос. Это предполагает использование искусственного базового класса только для автоматического декодирования. Я не буду его использовать, а просто опишу здесь для полноты:

// Groups all straight-forward decodable properties
//
class BaseClass: Codable {
    /*
     (Properties go here)
     */

    enum CodingKeys: String, CodingKey {
        /*
         (Coding keys for above properties go here)
         */
    }

    // (init(from decoder: Decoder) and encode(to:) are 
    // automatically provided by Swift)
}

// Actually used by the app
//
class MyClass: BaseClass {

    enum CodingKeys: String, CodingKey {
        case company
    }

    var company: Company? = nil

    override init(from decoder: Decoder) throws {
        super.init(from: decoder)

        let values = try decoder.container(keyedBy: CodingKeys.self)
        if let company = try? values.decode(Company.self, forKey: .company) {
            self.company = company
        }

    }
}

... Но это действительно безобразный хак. Иерархия наследования классов не должна диктоваться недостатками этого типа.

Ответы [ 2 ]

0 голосов
/ 06 ноября 2018

После поиска документации по протоколам Decoder и Decodable и конкретному классу JSONDecoder, я считаю, что невозможно найти именно то, что я искал. Самое близкое - просто реализовать init(from decoder: Decoder) и выполнить все необходимые проверки и преобразования вручную.


Дополнительные мысли

Подумав над проблемой, я обнаружил несколько проблем с моим текущим дизайном: для начала, отображение значения 0 в ответе JSON на nil не кажется правильным.

Несмотря на то, что значение 0 имеет специфическое значение «не указано» на стороне API, путем принудительного выполнения сбойного init?(rawValue:) По сути, я объединяю все недопустимые значения вместе. Если для какой-либо внутренней ошибки или ошибки сервер вернет (скажем) -7, мой код не сможет обнаружить это и автоматически отобразит его на nil, как если бы он был обозначен 0.

Из-за этого, я думаю, правильный дизайн будет либо:

  1. Отказаться от опции для свойства company и определить enum как:

    enum Company: Int {
       case unspecified = 0
       case toyota
       case ford
       case gm
    }
    

    ... близко соответствует JSON, или,

  2. Сохраните необязательность, но пусть API возвращает JSON, в котором не хватает значения для ключа "company" (так что сохраненное свойство Swift сохраняет свое начальное значение nil) вместо возвращая 0 (я полагаю, что JSON имеет "нулевое" значение, но я не уверен, как JSONDecoder справляется с этим)

Первый вариант требует модификации большого количества кода во всем приложении (изменение вхождений if let... на сравнение с .unspecified).

Второй вариант требует модификации серверного API, который мне не подвластен (и может привести к проблеме миграции / обратной совместимости между серверной и клиентской версиями).

Я думаю, что пока остановлюсь на моем обходном пути, и, возможно, когда-нибудь в будущем приму вариант 1 ...

0 голосов
/ 06 ноября 2018

Я думаю, у меня была проблема, похожая на вашу, если я правильно понимаю. В моем случае я написал оболочку для каждого рассматриваемого перечисления:

struct NilOnFail<T>: Decodable where T: Decodable {

    let value: T?

    init(from decoder: Decoder) throws {
        self.value = try? T(from: decoder) // Fail silently
    }

    // TODO: implement Encodable
}

Тогда используйте это так:

class MyClass: Codable {

    enum Company: Int {
        case toyota = 1
        case ford
        case gm
    } 

    var company: NilOnFail<Company>
...

Предостережение заключается в том, что везде, где вам нужно получить доступ к значению company, вам нужно использовать myClassInstance.company.value

...