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

В следующем коде я пытаюсь определить простой класс модели, и это неудачный инициализатор, который принимает (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()
    }
}

Ответ 1

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

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


Для Swift 2.1 и более ранних версий:

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

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

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

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

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

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

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

В вашем случае, однако, просто определение userName как String! не исправляет ошибку компиляции, потому что вам все равно нужно беспокоиться об инициализации свойств в базовом классе NSObject. К счастью, с userName, определяемым как 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
    }
}

Ответ 2

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

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

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

Источник

EDIT:

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

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

Ответ 3

Я согласен с тем, что ответ Майка S - рекомендация 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
        }
    }
}

Ответ 4

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

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
}

Ответ 5

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

Ответ 6

Я узнал, что это может сделать в 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
        }
    }
}

Ответ 7

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

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

Отрывок из: Apple Inc. " Язык быстрого программирования." iBooks. https://itun.es/sg/jEUH0.l

Ответ 8

Вы можете использовать удобство 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)
    } 
}