Лучшая практика для реализации отказоустойчивого инициализатора в Swift

100

С помощью следующего кода я пытаюсь определить простой класс модели и его сбойный инициализатор, который принимает словарь (json-) в качестве параметра. Инициализатор должен вернуться, nilесли имя пользователя не определено в исходном json.

1. Почему код не компилируется? В сообщении об ошибке говорится:

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

В этом нет смысла. Зачем мне инициализировать эти свойства, когда я планирую вернуться nil?

2. Правильный ли мой подход или есть другие идеи или общие шаблоны для достижения моей цели?

class User: NSObject {

    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        if let value: String = dictionary["user_name"] as? String {
            userName = value
        }
        else {
           return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()
    }
}
Кай Хуппманн
источник
У меня была аналогичная проблема, с моей я пришел к выводу, что следует ожидать каждое значение словаря, и поэтому я принудительно развернул значения. Если собственности там нет, я поймаю ошибку. Кроме того, я добавил canSetCalculablePropertiesлогический параметр, позволяющий моему инициализатору вычислять свойства, которые могут или не могут быть созданы на лету. Например, если dateCreatedключ отсутствует, и я могу установить свойство на лету, потому что canSetCalculablePropertiesпараметр имеет значение true, я просто устанавливаю его на текущую дату.
Адам Картер

Ответы:

71

Обновление: из журнала изменений Swift 2.2 (выпущено 21 марта 2016 г.):

Инициализаторы назначенных классов, объявленные как сбойные или вызывающие, теперь могут возвращать nil или вызывать ошибку, соответственно, до полной инициализации объекта.


Для Swift 2.1 и ранее:

Согласно документации Apple (и ошибке вашего компилятора), класс должен инициализировать все свои сохраненные свойства перед возвратом nilиз неудачного инициализатора:

Для классов, однако, отказавший инициализатор может вызвать сбой инициализации только после того, как все сохраненные свойства, введенные этим классом, были установлены на начальное значение и произошло любое делегирование инициализатора.

Примечание. На самом деле он отлично работает для структур и перечислений, но не для классов.

Предлагаемый способ обработки сохраненных свойств, которые не могут быть инициализированы до сбоя инициализатора, - объявить их как неявно развернутые необязательные параметры.

Пример из документов:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

В приведенном выше примере свойство name класса Product определяется как имеющее неявно развернутый необязательный строковый тип (String!). Поскольку это необязательный тип, это означает, что свойство name имеет значение по умолчанию nil, прежде чем ему будет присвоено конкретное значение во время инициализации. Это значение по умолчанию, равное nil, в свою очередь, означает, что все свойства, представленные классом Product, имеют допустимое начальное значение. В результате сбойный инициализатор для продукта может вызвать сбой инициализации в начале инициализатора, если ему передается пустая строка, перед присвоением определенного значения свойству name в инициализаторе.

В вашем случае, однако, простое определение userNameкак a String!не устраняет ошибку компиляции, потому что вам все еще нужно беспокоиться об инициализации свойств в вашем базовом классе NSObject. К счастью, с userNameопределением как a String!, вы можете вызвать super.init()перед собой, return nilкоторый запустит ваш NSObjectбазовый класс и исправит ошибку компиляции.

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}
Майк С
источник
1
Большое спасибо, не только правильно, но и хорошо объяснено
Кай Хуппманн
9
в swift1.2, пример из документации делает ошибку «Все сохраненные свойства экземпляра класса должны быть инициализированы перед возвратом nil из инициализатора»
Джеффри
2
@jeffrey Это верно, пример из документации ( Productкласс) не может вызвать сбой инициализации перед присвоением определенного значения, даже если в документации это указано. Документы не синхронизированы с последней версией Swift. varВместо этого рекомендуется сделать это на данный момент let. Источник: Крис Латтнер .
Arjan
1
В документации этот фрагмент кода немного отличается: сначала вы устанавливаете свойство, а затем проверяете, присутствует ли оно. См. Разделы «Неудачные инициализаторы для классов», «Язык программирования Swift». `` class Product {let name: String! init? (name: String) {self.name = name if name.isEmpty {return nil}}} ``
Миша Карпенко
Я тоже читал это в документации Apple, но не понимаю, зачем это нужно. В любом случае неудача будет означать возврат nil, какое тогда имеет значение, инициализированы ли свойства?
Alper
132

В этом нет смысла. Зачем мне инициализировать эти свойства, если я планирую вернуть nil?

По словам Криса Латтнера, это ошибка. Вот что он говорит:

Это ограничение реализации в компиляторе swift 1.1, задокументированное в примечаниях к выпуску. В настоящее время компилятор не может уничтожить частично инициализированные классы во всех случаях, поэтому он не допускает формирования ситуации, в которой это было бы необходимо. Мы считаем, что это ошибка, которая будет исправлена ​​в будущих выпусках, а не функция.

Источник

РЕДАКТИРОВАТЬ:

Итак, swift теперь имеет открытый исходный код и, согласно этому списку изменений, теперь исправлен в снимках swift 2.2.

Инициализаторы назначенных классов, объявленные как сбойные или вызывающие, теперь могут возвращать nil или вызывать ошибку, соответственно, до полной инициализации объекта.

Мустафа
источник
2
Спасибо за ответ на мою точку зрения о том, что идея инициализации свойств, которые больше не будут нужны, кажется не очень разумной. И +1 за то, что поделился источником, что доказывает, что Крис Латтнер чувствует то же, что и я;).
Кай Хуппманн
22
К вашему сведению: «В самом деле. Мы все еще хотели бы улучшить это, но не сделали сокращения для Swift 1.2». - Крис Латтнер 10. Фев 2015
dreamlab
14
К вашему сведению: в Swift 2.0 beta 2 это все еще проблема, и это также проблема с инициализатором, который выдает.
араназавр
7

Я согласен с тем, что ответ Майка С - это рекомендация Apple, но я не думаю, что это лучшая практика. Весь смысл сильной системы типов состоит в том, чтобы переместить ошибки времени выполнения во время компиляции. Это «решение» противоречит этой цели. ИМХО, лучше было бы инициализировать имя пользователя на"" а затем проверить его после super.init (). Если пустые имена пользователей разрешены, установите флаг.

class User: NSObject {
    let userName: String = ""
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: [String: AnyObject]) {
        if let user_name = dictionary["user_name"] as? String {
            userName = user_name
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()

        if userName.isEmpty {
            return nil
        }
    }
}
Дэниел Т.
источник
Спасибо, но я не вижу, как идеи систем строгих типов искажаются ответом Майка. В целом вы представляете одно и то же решение с той разницей, что начальное значение установлено на "" вместо nil. Более того, ваш код требует использования "" в качестве имени пользователя (что может показаться вполне академичным, но, по крайней мере, это отличается от того, что не указано в json / словаре)
Кай Хуппманн
2
Проверив, я вижу, что вы правы, но только потому, что userName является константой. Если бы это была переменная, то принятый ответ был бы хуже моего, потому что userName позже можно было бы установить в ноль.
Дэниел Т.
Мне нравится этот ответ. @KaiHuppmann, если вы хотите разрешить пустые имена пользователей, у вас также может быть простой Bool needsReturnNil. Если значение не существует в словаре, установите для needsReturnNil значение true, а для userName - любое. После super.init () проверьте needReturnNil и при необходимости верните nil.
Ричард Венейбл,
6

Другой способ обойти это ограничение - работать с классом-функциями для инициализации. Возможно, вы даже захотите переместить эту функцию в расширение:

class User: NSObject {

    let username: String
    let isSuperUser: Bool
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {

         self.userName = userName
         self.isSuperUser = isSuperUser
         self.someDetails = someDetails

         super.init()
    }
}

extension User {

    class func fromDictionary(dictionary: NSDictionary) -> User? {

        if let username: String = dictionary["user_name"] as? String {

            let isSuperUser = (dictionary["super_user"] as? Bool) ?? false
            let someDetails = dictionary["some_details"] as? [String]

            return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails)
        }

        return nil
    }
}

Использование его станет:

if let user = User.fromDictionary(someDict) {

     // Party hard
}
Кевин Р
источник
1
Мне это нравится; Я предпочитаю, чтобы конструкторы были прозрачны в отношении того, что они хотят, а передача в словаре очень непрозрачна.
Ben Leggiero
3

Хотя был выпущен Swift 2.2, и вам больше не нужно полностью инициализировать объект перед ошибкой инициализатора, вам нужно подождать, пока https://bugs.swift.org/browse/SR-704 не будет исправлен.

серебро
источник
1

Я узнал, что это можно сделать в Swift 1.2.

Есть некоторые условия:

  • Обязательные свойства должны быть объявлены как неявно развернутые необязательные параметры
  • Присвойте значение требуемым свойствам ровно один раз. Это значение может быть нулевым.
  • Затем вызовите super.init (), если ваш класс наследуется от другого класса.
  • После того, как всем вашим обязательным свойствам присвоено значение, проверьте, соответствует ли их значение ожидаемому. Если нет, верните ноль.

Пример:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Пим
источник
0

Неисправный инициализатор для типа значения (то есть структуры или перечисления) может вызвать сбой инициализации в любой точке в своей реализации инициализатора.

Для классов, однако, отказавший инициализатор может вызвать сбой инициализации только после того, как все сохраненные свойства, введенные этим классом, были установлены на начальное значение и произошло любое делегирование инициализатора.

Выдержка из: Apple Inc. « Быстрый язык программирования. IBooks. https://itun.es/sg/jEUH0.l

user1046037
источник
0

Вы можете использовать удобный init :

class User: NSObject {
    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {
        self.userName = userName
        self.isSuperUser = isSuperUser
        self.someDetails = someDetails
    }     

    convenience init? (dict: NSDictionary) {            
       guard let userName = dictionary["user_name"] as? String else { return nil }
       guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil }
       guard let someDetails = dictionary["some_details"] as? [String] else { return nil }

       self.init(userName: userName, isSuperUser: isSuperUser, someDetails: someDetails)
    } 
}
Максим Петров
источник