Найкраща практика впровадження недоступного ініціалізатора в Swift


100

За допомогою наступного коду я намагаюся визначити простий клас моделі та його недоступний ініціалізатор, який бере в якості параметра словник (json-). Ініціалізатор повинен повернутися, nilякщо ім'я користувача не визначено у вихідному json.

1. Чому код не складається? Повідомлення про помилку говорить:

Усі збережені властивості екземпляра класу повинні бути ініціалізовані перед поверненням нуля з ініціалізатора.

Це не має сенсу. Чому я повинен ініціалізувати ці властивості, коли планую повернутися 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 р.):

Позначені ініціалізатори класу, оголошені як недоступні або кинуті, тепер можуть повернути нуль або випустити помилку відповідно до того, як об'єкт буде повністю ініціалізований.


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

Відповідно до документації Apple (та вашої помилки компілятора), клас повинен ініціалізувати всі збережені властивості перед поверненням nilіз недоступного ініціалізатора:

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

Примітка. Це насправді добре працює для структур та перерахувань, тільки не для класів.

Запропонований спосіб обробки збережених властивостей, які неможливо ініціалізувати до відмови ініціалізатора, - це оголосити їх як неявно розгорнуті необов'язкові.

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

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

У наведеному вище прикладі властивість імені класу Product визначається як неявно розпакований необов'язковий тип рядка (String!). Оскільки це необов'язковий тип, це означає, що властивість імені має значення за замовчуванням нуль, перш ніж йому буде присвоєно конкретне значення під час ініціалізації. Це значення за замовчуванням нуля в свою чергу означає, що всі властивості, введені класом Product, мають дійсне початкове значення. Як результат, недоступний ініціалізатор для Продукту може викликати збій ініціалізації на початку ініціалізатора, якщо він передається порожнім рядком, перш ніж присвоїти певне значення властивості імені в ініціалізаторі.

У вашому випадку, однак, просто визначаючи , userNameяк 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, Приклад з docs робить помилку "Усі збережені властивості екземпляра класу повинні бути ініціалізовані перед поверненням нуля з ініціалізатора"
jeffrey

2
@jeffrey Правильно, приклад з документації ( Productкласу) не може викликати помилку ініціалізації до призначення конкретного значення, навіть якщо документи говорять, що це може. Документи не синхронізовані з останньою версією Swift. Радимо зробити це varзамість цього let. джерело: Кріс Леттнер .
Ар'ян

1
У документації цей фрагмент коду дещо інший: ви спочатку встановлюєте властивість, а потім перевіряєте, чи він присутній. Див. "Недоступні ініціалізатори для класів", "Швидка мова програмування". `` Класовий продукт {нехай назва: Рядок! init? (name: String) {self.name = name if name.isEmpty {return nil}}}} `` `
Міша Карпенко

Я читав це також у документах Apple, але не розумію, чому це потрібно. Невдача означатиме повернення нуля в будь-якому випадку, що має значення тоді, чи були ініціалізовані властивості?
Альпер

132

Це не має сенсу. Навіщо мені ініціалізувати ці властивості, коли я планую повернути нуль?

За словами Кріса Леттнера, це помилка. Ось що він говорить:

Це обмеження на реалізацію у компіляторі swift 1.1, задокументованому в примітках до випуску. Наразі компілятор не в змозі знищити частково ініціалізовані класи у всіх випадках, тому він виключає формування ситуації, коли це доведеться. Ми вважаємо цю помилку виправленою у майбутніх випусках, а не особливістю.

Джерело

Редагувати:

Отже, swift тепер є відкритим кодом, і відповідно до цього журналу змін він фіксується зараз у знімках швидкого 2.2

Позначені ініціалізатори класу, оголошені як недоступні або кинуті, тепер можуть повернути нуль або випустити помилку відповідно до того, як об'єкт буде повністю ініціалізований.


2
Дякую за те, що я звернувся до моєї точки зору, що ідея ініціалізації властивостей, яка більше не буде потрібною, здається не дуже розумною. І +1 за обмін джерелом, що доводить, що Кріс Леттнер відчуває себе так, як і я;).
Кай Хуппман

22
FYI: "Дійсно. Це все-таки, що ми хотіли б вдосконалити, але не зробили скорочення для Swift 1.2". - Chris Lattner 10. Feb 2015
dreamlab

14
FYI: У Swift 2.0 beta 2 це все ще проблема, а також проблема з ініціалізатором, який кидає.
араназавр

7

Я приймаю, що відповідь Майка S - це рекомендація Apple, але я не думаю, що це найкраща практика. Вся суть системи сильного типу полягає в переміщенні помилок виконання для збирання часу. Це "рішення" перемагає цю мету. IMHO, краще буде продовжити ініціалізувати ім'я користувача, ""а потім перевірити його після 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
        }
    }
}

Дякую, але я не бачу, як ідеї систем сильного типу пошкоджуються відповіддю Майка. Загалом ви представляєте одне і те ж рішення з тією різницею, що початкове значення встановлено на "" замість нуля. Більше того, ви забираєте код, щоб використовувати "" ім'я користувача (що може здатися досить академічним, але принаймні, це відрізняється від того, що він не встановлений у json / словнику)
Kai Huppmann,

2
Після перегляду я бачу, що ви маєте рацію, але тільки тому, що userName - це постійна. Якби це була змінна, то прийнята відповідь була б гіршою, ніж моя, оскільки пізніше userName може бути встановлений на нуль.
Даніель Т.

Мені подобається ця відповідь. @KaiHuppmann, якщо ви хочете дозволити порожні імена користувачів, ви також можете просто мати Bool potrebamaReturnNil. Якщо значення в словнику не існує, встановіть needReturnNil на true та встановіть userName на будь-що. Після super.init (), перевірте NeedReturnNil та поверніть нуль, якщо потрібно.
Річард Венебл

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
Мені подобається це; Я вважаю за краще, щоб конструктори були прозорими щодо того, чого вони хочуть, і передача у словник дуже непрозора.
Бен Леджієро

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


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)
    } 
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.