Najlepsza praktyka wdrażania dostępnego inicjatora w języku Swift

100

Za pomocą poniższego kodu próbuję zdefiniować prostą klasę modelu i jej dostępny inicjalizator, który jako parametr przyjmuje słownik (json-). Inicjator powinien zwrócić, niljeśli nazwa użytkownika nie jest zdefiniowana w oryginalnym pliku json.

1. Dlaczego kod się nie kompiluje? Komunikat o błędzie mówi:

Wszystkie przechowywane właściwości wystąpienia klasy muszą zostać zainicjowane przed zwróceniem wartości nil z inicjatora.

To nie ma sensu. Dlaczego powinienem inicjalizować te właściwości, kiedy planuję powrót nil?

2. Czy moje podejście jest właściwe, czy też byłyby inne pomysły lub wspólne wzorce, aby osiągnąć mój cel?

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()
    }
}
Kai Huppmann
źródło
Miałem podobny problem, z moim doszedłem do wniosku, że należy się spodziewać każdej wartości słownikowej, więc wymuszam rozpakowanie wartości. Jeśli nieruchomości nie ma, będę w stanie złapać błąd. Dodatkowo dodałem canSetCalculablePropertiesparametr boolowski, który umożliwia mojemu inicjatorowi obliczenie właściwości, które można lub nie można utworzyć w locie. Na przykład, jeśli dateCreatedbrakuje klucza i mogę ustawić właściwość w locie, ponieważ canSetCalculablePropertiesparametr ma wartość true, po prostu ustawiam go na bieżącą datę.
Adam Carter

Odpowiedzi:

71

Aktualizacja: z dziennika zmian Swift 2.2 (opublikowanego 21 marca 2016 r.):

Wyznaczone inicjatory klas zadeklarowane jako dostępne lub zgłaszające mogą teraz zwracać odpowiednio nil lub zgłaszać błąd, zanim obiekt zostanie w pełni zainicjowany.


Dla Swift 2.1 i starszych:

Zgodnie z dokumentacją Apple (i błędem kompilatora), klasa musi zainicjować wszystkie swoje przechowywane właściwości przed powrotem nilz dostępnego inicjatora:

Jednak w przypadku klas dostępny inicjator może wyzwolić błąd inicjalizacji tylko wtedy, gdy wszystkie przechowywane właściwości wprowadzone przez tę klasę zostały ustawione na wartość początkową i nastąpiło delegowanie inicjatora.

Uwaga: w rzeczywistości działa dobrze w przypadku struktur i wyliczeń, ale nie klas.

Sugerowanym sposobem obsługi przechowywanych właściwości, których nie można zainicjować przed niepowodzeniem inicjatora, jest zadeklarowanie ich jako niejawnie rozpakowanych opcji.

Przykład z dokumentów:

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

W powyższym przykładzie właściwość name klasy Product jest zdefiniowana jako posiadająca niejawnie rozpakowany opcjonalny typ ciągu (String!). Ponieważ jest to typ opcjonalny, oznacza to, że właściwość name ma domyślną wartość nil, zanim zostanie przypisana określona wartość podczas inicjalizacji. Ta domyślna wartość zero z kolei oznacza, że ​​wszystkie właściwości wprowadzone przez klasę Product mają prawidłową wartość początkową. W rezultacie dostępny inicjator dla produktu może wyzwolić błąd inicjalizacji na początku inicjatora, jeśli zostanie przekazany pusty ciąg, przed przypisaniem określonej wartości do właściwości name w inicjatorze.

W twoim przypadku, jednak po prostu definiując userNamejako String!nie naprawić błąd kompilacji, ponieważ wciąż trzeba się martwić o inicjowanie właściwości na swojej klasy bazowej NSObject. Na szczęście z userNamedefinicją a String!, możesz zadzwonić super.init()przed tobą, return nilco zainicjuje twoją NSObjectklasę bazową i naprawi błąd kompilacji.

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
    }
}
Mike S.
źródło
1
Dziękuję bardzo nie tylko słusznie, ale także dobrze wyjaśnione
Kai Huppmann
9
w swift1.2, Przykład z dokumentacji popełnia błąd „Wszystkie przechowywane właściwości instancji klasy muszą zostać zainicjowane przed zwróceniem wartości nil z inicjatora”
jeffrey
2
@jeffrey Zgadza się, przykład z dokumentacji ( Productklasy) nie może wywołać błędu inicjalizacji przed przypisaniem określonej wartości, nawet jeśli dokumentacja mówi, że może. Dokumenty nie są zsynchronizowane z najnowszą wersją Swift. varZamiast tego zaleca się zrobienie tego na razie let. źródło: Chris Lattner .
Arjan,
1
W dokumentacji ten fragment kodu jest nieco inny: najpierw ustawiasz właściwość, a następnie sprawdzasz, czy jest obecna. Zobacz „Dostępne inicjatory dla klas”, „Swift Programming Language”. `` `` klasa Produkt {niech nazwa: ciąg! init? (name: String) {self.name = name if name.isEmpty {return nil}}} `` ``
Misha Karpenko
Przeczytałem to również w dokumentach Apple, ale nie widzę, dlaczego byłoby to wymagane. Awaria oznaczałaby zwrócenie zera i tak, jakie to ma znaczenie, czy właściwości zostały zainicjowane?
Alper
132

To nie ma sensu. Dlaczego powinienem inicjalizować te właściwości, kiedy planuję zwrócić zero?

Według Chrisa Lattnera jest to błąd. Oto, co mówi:

Jest to ograniczenie implementacyjne w kompilatorze Swift 1.1, udokumentowane w informacjach o wydaniu. Kompilator obecnie nie jest w stanie zniszczyć częściowo zainicjowanych klas we wszystkich przypadkach, więc nie pozwala na utworzenie sytuacji, w której musiałby to zrobić. Uważamy, że jest to błąd, który należy naprawić w przyszłych wydaniach, a nie funkcja.

Źródło

EDYTOWAĆ:

Tak więc Swift jest teraz open source i zgodnie z tym dziennikiem zmian jest teraz naprawiony w migawkach swift 2.2

Wyznaczone inicjatory klas zadeklarowane jako dostępne lub zgłaszające mogą teraz zwracać odpowiednio nil lub zgłaszać błąd, zanim obiekt zostanie w pełni zainicjowany.

mustafa
źródło
2
Dziękuję za zajęcie się moją uwagą, że pomysł inicjalizacji właściwości, które nie będą już potrzebne, wydaje się niezbyt rozsądny. I +1 za udostępnienie źródła, co udowadnia, że ​​Chris Lattner czuje się tak, jak ja;).
Kai Huppmann
22
FYI: „Rzeczywiście. To jest wciąż coś, co chcielibyśmy ulepszyć, ale nie udało się to w wersji Swift 1.2”. - Chris Lattner 10 lutego 2015
dreamlab
14
FYI: W Swift 2.0 beta 2 jest to nadal problem, a także problem z inicjatorem, który wyrzuca.
aranasaurus
7

Akceptuję, że odpowiedź Mike'a S jest rekomendacją Apple, ale nie sądzę, że jest to najlepsza praktyka. Istotą silnego systemu typów jest przeniesienie błędów uruchomieniowych do czasu kompilacji. To „rozwiązanie” przeczy temu celowi. IMHO, lepiej byłoby przejść dalej i zainicjować nazwę użytkownika, ""a następnie sprawdzić ją po super.init (). Jeśli puste nazwy użytkowników są dozwolone, ustaw flagę.

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
        }
    }
}
Daniel T.
źródło
Dziękuję, ale nie widzę, jak odpowiedź Mike'a psuje idee systemów typu silnego. W sumie przedstawiasz to samo rozwiązanie, z tą różnicą, że wartość początkowa jest ustawiona na „” zamiast na zero. Co więcej, Twój kod zabiera używanie „” jako nazwy użytkownika (co może wydawać się dość akademickie, ale przynajmniej różni się od tego, że nie ma jej w json / słowniku)
Kai Huppmann
2
Po przejrzeniu widzę, że masz rację, ale tylko dlatego, że nazwa_użytkownika jest stała. Gdyby to była zmienna, zaakceptowana odpowiedź byłaby gorsza niż moja, ponieważ nazwa_użytkownika mogłaby zostać później ustawiona na zero.
Daniel T.
Podoba mi się ta odpowiedź. @KaiHuppmann, jeśli chcesz zezwolić na puste nazwy użytkowników, możesz również mieć prostą wartość Bool needReturnNil. Jeśli wartość nie istnieje w słowniku, ustaw needReturnNil na true i ustaw userName na cokolwiek. Po super.init (), sprawdź needReturnNil iw razie potrzeby zwróć nil.
Richard Venable
6

Innym sposobem obejścia tego ograniczenia jest praca z funkcjami klas w celu wykonania inicjalizacji. Możesz nawet chcieć przenieść tę funkcję do rozszerzenia:

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

Używanie go stałoby się:

if let user = User.fromDictionary(someDict) {

     // Party hard
}
Kevin R.
źródło
1
Lubię to; Wolę, aby konstruktorzy byli przejrzysty w kwestii tego, czego chcą, a przekazywanie do słownika jest bardzo niejasne.
Ben Leggiero
1

Dowiedziałem się, że można to zrobić w Swift 1.2

Jest kilka warunków:

  • Wymagane właściwości powinny być zadeklarowane jako niejawnie rozpakowane opcje opcjonalne
  • Przypisz wartość do wymaganych właściwości dokładnie raz. Ta wartość może wynosić zero.
  • Następnie wywołaj super.init (), jeśli twoja klasa dziedziczy po innej klasie.
  • Po przypisaniu wartości wszystkim wymaganym właściwościom sprawdź, czy ich wartość jest zgodna z oczekiwaniami. Jeśli nie, zwróć zero.

Przykład:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Pim
źródło
0

Dostępny inicjator dla typu wartości (czyli struktury lub wyliczenia) może wyzwolić błąd inicjalizacji w dowolnym momencie w ramach jego implementacji inicjatora

Jednak w przypadku klas dostępny inicjator może wyzwolić błąd inicjalizacji tylko wtedy, gdy wszystkie przechowywane właściwości wprowadzone przez tę klasę zostały ustawione na wartość początkową i nastąpiło delegowanie inicjatora.

Fragment z: Apple Inc. „ Swift Programming Language. ”IBooks. https://itun.es/sg/jEUH0.l

user1046037
źródło
0

Możesz skorzystać z wygody w inicjalizacji :

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)
    } 
}
Максим Петров
źródło