Błąd w klasie Swift: właściwość nie została zainicjowana przy wywołaniu super.init

218

Mam dwie klasy ShapeiSquare

class Shape {
    var numberOfSides = 0
    var name: String
    init(name:String) {
        self.name = name
    }
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Przy powyższej implementacji pojawia się błąd:

property 'self.sideLength' not initialized at super.init call
    super.init(name:name)

Dlaczego muszę ustawić self.sideLengthprzed połączeniem super.init?

JuJoDi
źródło
całkiem pewne, że ma to związek z dobrymi praktykami programowania, a nie z faktycznymi ograniczeniami technicznymi. Jeśli Shape miałby wywołać funkcję, którą Square zastąpił, Square może chcieć użyć sideLength, ale nie została jeszcze zainicjowana. Swift prawdopodobnie ogranicza cię przed zrobieniem tego przez przypadek, zmuszając Cię do zainicjowania instancji przed wywołaniem klasy podstawowej.
cwharris
3
Przykłady w książce są złe. Powinieneś zawsze wywoływać super.init () jako ostatnią, aby mieć pewność, że wszystkie właściwości zostały zainicjowane, wyjaśnione poniżej.
Pascal

Odpowiedzi:

173

Cytat z The Swift Programming Language, który odpowiada na twoje pytanie:

„Kompilator Swift wykonuje cztery pomocne kontrole bezpieczeństwa, aby upewnić się, że inicjalizacja dwufazowa przebiegła bez błędów:”

Kontrola bezpieczeństwa 1 „Wyznaczony inicjator musi zapewnić, że wszystkie„ właściwości wprowadzone przez jego klasę zostaną zainicjowane przed przekazaniem go do inicjatora nadklasy ”.

Fragment: Apple Inc. „Swift Programming Language”. iBooks. https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

Ruben
źródło
47
To niesamowita zmiana w porównaniu do C ++, C # lub Java.
MDJ
12
@MDJ Pewnie. Szczerze mówiąc, nie widzę też jego wartości dodanej.
Ruben
20
Właściwie wydaje się, że jest jeden. Powiedzmy w języku C #, konstruktor nadklasy nie powinien wywoływać żadnych nadrzędnych (wirtualnych) metod, ponieważ nikt nie wie, jak zareagowałyby na nie w pełni zainicjowaną podklasę. W Swift jest w porządku, ponieważ stan dodatkowej podklasy jest w porządku, gdy działa konstruktor nadklasy. Ponadto w Swift wszystkie metody oprócz ostatecznych są nadrzędne.
MDJ
17
Szczególnie denerwuje mnie to, ponieważ oznacza to, że jeśli chcę utworzyć podklasę UIView, która tworzy własne widoki podrzędne, muszę zainicjować te widoki podrzędne bez ramek na początku i dodać ramki później, ponieważ nie mogę odwoływać się do widoku graniczy do momentu PO wywołania super.init.
Ash
5
@Janos, jeśli ustawisz opcjonalną właściwość, nie musisz jej inicjować init.
JeremyP,
105

Swift ma bardzo wyraźną, określoną sekwencję operacji wykonywanych w inicjalizatorach. Zacznijmy od kilku podstawowych przykładów i przejdźmy do ogólnej sprawy.

Weźmy obiekt A. Zdefiniujemy go w następujący sposób.

class A {
    var x: Int
    init(x: Int) {
        self.x = x
    }
}

Zauważ, że A nie ma nadklasy, więc nie może wywołać funkcji super.init (), ponieważ ona nie istnieje.

OK, więc teraz podklasę A z nową klasą o nazwie B.

class B: A {
    var y: Int
    init(x: Int, y: Int) {
        self.y = y
        super.init(x: x)
    }
}

Jest to odejście od celu C, w którym [super init]zwykle nazywa się go najpierw przed czymkolwiek innym. Nie tak w Swift. Jesteś odpowiedzialny za upewnienie się, że zmienne instancji są w spójnym stanie, zanim zrobisz cokolwiek innego, w tym metody wywoływania (w tym inicjalizator nadklasy).

Hitendra Solanki
źródło
1
jest to niezwykle pomocne, wyraźny przykład. Dziękuję Ci!
FullMetalFist
Co jeśli potrzebuję wartości do obliczenia y, na przykład: init (y: Int) {self.y = y * self.x super.init ()}
6rod9
1
użyj czegoś takiego, init (y: Int, x: Int = 0) {self.y = y * x; self.x = x; super.init (x: x)}, również nie można bezpośrednio wywoływać pustego konstruktora dla superklasy w odniesieniu do powyższego przykładu, ponieważ nazwy
superklasy
43

Z dokumentów

Kontrola bezpieczeństwa 1

Wyznaczony inicjalizator musi zapewnić, że wszystkie właściwości wprowadzone przez jego klasę zostaną zainicjowane przed delegowaniem do inicjalizatora nadklasy.


Dlaczego potrzebujemy takiej kontroli bezpieczeństwa?

Aby odpowiedzieć na to, przejdźmy szybko przez proces inicjalizacji.

Inicjalizacja dwufazowa

Inicjalizacja klasy w Swift jest procesem dwufazowym. W pierwszej fazie każdej przechowywanej właściwości przypisywana jest klasa początkowa przez klasę, która ją wprowadziła. Po określeniu stanu początkowego dla każdej przechowywanej właściwości rozpoczyna się druga faza i każda klasa ma możliwość dalszego dostosowania swoich przechowywanych właściwości, zanim nowa instancja zostanie uznana za gotową do użycia.

Zastosowanie dwufazowego procesu inicjalizacji sprawia, że ​​inicjalizacja jest bezpieczna, a jednocześnie zapewnia pełną elastyczność każdej klasie w hierarchii klas. Inicjalizacja dwufazowa uniemożliwia dostęp do wartości właściwości przed ich zainicjowaniem i zapobiega nieoczekiwanemu ustawieniu wartości właściwości na inną wartość przez inny inicjator.

Tak więc, aby upewnić się, że dwuetapowy proces inicjalizacji przebiega zgodnie z powyższą definicją, istnieją cztery kontrole bezpieczeństwa, jednym z nich jest

Kontrola bezpieczeństwa 1

Wyznaczony inicjalizator musi zapewnić, że wszystkie właściwości wprowadzone przez jego klasę zostaną zainicjowane przed delegowaniem do inicjalizatora nadklasy.

Teraz dwufazowa inicjalizacja nigdy nie mówi o kolejności, ale ta kontrola bezpieczeństwa wprowadza się super.initpo zamówieniu, po inicjalizacji wszystkich właściwości.

Kontrola bezpieczeństwa 1 może wydawać się nieistotna, ponieważ inicjalizacja dwufazowa uniemożliwia dostęp do wartości właściwości przed ich zainicjowaniem , bez tej kontroli bezpieczeństwa 1.

Jak w tej próbce

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        super.init(sides: 3, named: "Triangle") 
        self.hypotenuse = hypotenuse
    }
}

Triangle.initzainicjował każdą właściwość przed użyciem. Kontrola bezpieczeństwa 1 wydaje się nieistotna,

Ale potem może być inny scenariusz, trochę skomplikowany,

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
        printShapeDescription()
    }
    func printShapeDescription() {
        print("Shape Name :\(self.name)")
        print("Sides :\(self.sides)")
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        self.hypotenuse = hypotenuse
        super.init(sides: 3, named: "Triangle")
    }

    override func printShapeDescription() {
        super.printShapeDescription()
        print("Hypotenuse :\(self.hypotenuse)")
    }
}

let triangle = Triangle(hypotenuse: 12)

Wynik :

Shape Name :Triangle
Sides :3
Hypotenuse :12

Tutaj, gdybyśmy wywołali super.initprzed ustawieniem hypotenuse, super.initwywołanie wywołałoby wtedy printShapeDescription()i, ponieważ zostało to nadpisane, najpierw powróciłoby do implementacji klasy Trójkąta printShapeDescription(). printShapeDescription()Triangle dostępu klasowym hypotenuseniebędącego własnością opcjonalne, które wciąż nie zostały zainicjowane zostały. Nie jest to dozwolone, ponieważ inicjalizacja dwufazowa uniemożliwia dostęp do wartości właściwości przed ich zainicjowaniem

Upewnij się więc, że inicjalizacja dwufazowa przebiega zgodnie z definicją, musi istnieć konkretna kolejność wywoływania super.init, to znaczy po zainicjowaniu wszystkich właściwości wprowadzonych przez selfklasę, dlatego potrzebujemy kontroli bezpieczeństwa 1

BangOperator
źródło
1
Świetne wyjaśnienie, dlaczego zdecydowanie należy dodać do najwyższej odpowiedzi.
Guy Daher
Mówisz więc w zasadzie, ponieważ nadklasa init może wywoływać funkcję (przesłaniającą) ... w której ta funkcja uzyskuje dostęp do właściwości podklas, a następnie, aby uniknąć braku ustawiania wartości, wywołanie supermusi nastąpić po ustawieniu wszystkich wartości. OK ma sens. Zastanawiacie się, jak to zrobiło Objective C i dlaczego musieliście supernajpierw zadzwonić ?
Honey
Zasadniczo, co masz zwrócić uwagę na to podobny do: wprowadzania printShapeDescription() przed self.sides = sides; self.name = named; który generuje ten błąd: use of 'self' in method call 'printShapeDescription' before all stored properties are initialized. Błąd PO jest podawany w celu zmniejszenia „możliwości” błędu czasu wykonywania.
Honey
W szczególności użyłem słowa „możliwość”, ponieważ jeśli byłaby printShapeDescriptionto funkcja, do której się nie odwoływało, selftzn. Byłaby czymś w rodzaju „print („ nic ”), to nie byłoby problemów. (Jednak nawet z tego powodu kompilator zgłasza błąd, ponieważ nie jest super inteligentny)
Honey
Cóż objc po prostu nie było bezpieczne. Swift jest bezpieczny dla typów, więc obiekty, które nie są opcjonalne, naprawdę muszą być niepuste!
Daij-Djan,
36

„Super.init ()” należy wywołać po zainicjowaniu wszystkich zmiennych instancji.

W filmie Apple „Intermediate Swift” (można go znaleźć na stronie zasobów wideo Apple Developer https://developer.apple.com/videos/wwdc/2014/ ), około 28:40, wyraźnie zaznaczono, że wszystkie inicjatory w superklasa musi być nazwana PO zainicjowaniu zmiennych instancji.

W Objective-C było odwrotnie. W Swift, ponieważ wszystkie właściwości muszą zostać zainicjowane przed ich użyciem, musimy najpierw zainicjować właściwości. Ma to na celu zapobieganie wywołaniu funkcji przesłoniętej z metody „init ()” superklasy, bez uprzedniego inicjowania właściwości.

Tak więc implementacja „Square” powinna być:

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength
        numberOfSides = 4
        super.init(name:name) // Correct position for "super.init()"
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}
Shuyang
źródło
1
Nigdy bym nie zgadł. Inicjalizacja za pomocą super powinna być pierwszym stwierdzeniem, co bym pomyślał. !! hm. dość szybka zmiana.
mythicalcoder
Dlaczego to musi nastąpić? Podaj powód techniczny
Masih
14

Przepraszamy za brzydkie formatowanie. Wystarczy umieścić znak zapytania po deklaracji i wszystko będzie dobrze. Pytanie mówi kompilatorowi, że wartość jest opcjonalna.

class Square: Shape {
    var sideLength: Double?   // <=== like this ..

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Edycja1:

Jest lepszy sposób na pominięcie tego błędu. Zgodnie z komentarzem jmaschada nie ma powodu, aby używać opcjonalnego w twoim przypadku, ponieważ opcjonalne nie są wygodne w użyciu i zawsze musisz sprawdzić, czy opcjonalne nie jest zerowe przed uzyskaniem dostępu. Musisz tylko zainicjować członka po deklaracji:

class Square: Shape {
    var sideLength: Double=Double()   

    init(sideLength:Double, name:String) {
        super.init(name:name)
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Edycja2:

Po dwóch minusach tej odpowiedzi znalazłem jeszcze lepszy sposób. Jeśli chcesz, aby członek klasy został zainicjowany w konstruktorze, musisz przypisać mu wartość początkową wewnątrz contructor i przed wywołaniem super.init (). Lubię to:

class Square: Shape {
    var sideLength: Double  

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength   // <= before super.init call..
        super.init(name:name)
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Powodzenia w nauce Swift.

fnc12
źródło
Wystarczy przełączyć super.init(name:name)i self.sideLength = sideLength. Deklarowanie sideLengthjako opcja jest misguiding i wprowadza dodatkowe kłopotów później, kiedy trzeba wymusić unwrap niego.
Johannes Luong
Tak, to jest opcja. Dzięki
fnc12,
Możesz po prostu mieć var sideLength: Double, bez potrzeby przypisywania jej wartości początkowej
Jarsen
Co jeśli naprawdę mam opcjonalną stałą. Co mam z tym zrobić? Czy muszę zainicjować w konstruktorze? Nie rozumiem, dlaczego musisz to robić, ale kompilator narzeka w Swift 1.2
Van Du Tran
1
Idealny ! wszystkie 3 rozwiązania działały „?”, „String ()”, ale dla mnie problemem było to, że nie „przypisałem” jednej z właściwości, a kiedy to zrobiłem, działało! Dzięki kolego
Naishta
9

swift wymusza inicjalizację każdego członka var, zanim będzie on kiedykolwiek / mógł zostać użyty. Ponieważ nie można mieć pewności, co się stanie, gdy zostanie włączony supers, błędnie się sprawdza: lepiej zabezpieczyć niż przepraszać

Daij-Djan
źródło
1
To nie ma sensu IMO, ponieważ klasa nadrzędna nie powinna mieć wglądu we właściwości zadeklarowane w jej dzieciach!
Andy Hin,
1
Nie, ale możesz przesłonić rzeczy i zacząć używać self 'zanim super to zrobi'
Daij-Djan,
Czy widzisz tutaj i następujące po nim komentarze? Myślę, że mówię dokładnie to, co mówisz, tzn. Oboje mówimy, że kompilator chce być bezpieczny, a nie przepraszam, moje jedyne pytanie brzmi: w jaki sposób cel-c rozwiązał ten problem? A może nie? Jeśli nie, to dlaczego nadal wymaga pisania super.initw pierwszej linii?
Honey
7

Edward,

Możesz zmodyfikować kod w swoim przykładzie w następujący sposób:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}

Wykorzystuje to domyślnie nieopakowane opcjonalne.

W dokumentacji możemy przeczytać:

„Podobnie jak w przypadku opcji, jeśli nie podasz wartości początkowej podczas deklarowania domyślnie nieopakowanej opcjonalnej zmiennej lub właściwości, jej wartość automatycznie przyjmuje domyślnie zero”.

Pavel Gubarev
źródło
Myślę, że to najczystsza opcja. Podczas mojej pierwszej próby Swift miałem zmienną składową typu AVCaptureDevice, której nie można bezpośrednio utworzyć, dlatego wymagany był kod init (). Jednak ViewController wymaga wielu inicjatorów i nie można wywołać wspólnej metody inicjalizacji z init (), więc ta odpowiedź wydaje się być jedyną opcją, która pozwala uniknąć kopiowania / wklejania zduplikowanego kodu w każdym inicjalizatorze.
sunetos
6

Swift nie pozwoli ci zainicjować superklasy bez inicjowania właściwości, odwrotnie niż w Obj C. Więc musisz zainicjować wszystkie właściwości przed wywołaniem „super.init”.

Przejdź do http://blog.scottlogic.com/2014/11/20/swift-initialisation.html . To daje dobre wyjaśnienie twojego problemu.

Jishnu Bala
źródło
6

Dodaj zero na końcu deklaracji.


// Must be nil or swift complains
var someProtocol:SomeProtocol? = nil

// Init the view
override init(frame: CGRect)
    super.init(frame: frame)
    ...

To zadziałało w moim przypadku, ale może nie działać w twoim przypadku

Michael
źródło
Dobry, podczas korzystania z UIView z UIViewController z protokołem
abdul sathar
1

Po prostu inicjujesz w niewłaściwej kolejności.

     class Shape2 {
        var numberOfSides = 0
        var name: String
        init(name:String) {
            self.name = name
        }
        func simpleDescription() -> String {
            return "A shape with \(numberOfSides) sides."
        }
    }

    class Square2: Shape2 {
        var sideLength: Double

        init(sideLength:Double, name:String) {

            self.sideLength = sideLength
            super.init(name:name) // It should be behind "self.sideLength = sideLength"
            numberOfSides = 4
        }
        func area () -> Double {
            return sideLength * sideLength
        }
    }
ylgwhyh
źródło
0

Prawdopodobnie otrzymam kilka głosów negatywnych, ale szczerze mówiąc, życie jest łatwiejsze w ten sposób:

class CSListServerData<ListItem: CSJsonData>: CSServerData {
    var key: String!
    var type: ListItem.Type!
    var property: CSJsonDataList<ListItem>!

    func construct(_ key: String, _ type: ListItem.Type) -> Self {
        self.key = key
        self.type = type
        property = CSJsonDataList(self, type, key)
        return self
    }

    func construct(_ type: ListItem.Type) { construct("list", type) }

    var list: [ListItem] { property.list }
}
Renetik
źródło
-4

To zadziwiająco głupi projekt.

Rozważ coś takiego:

.
.
.
var playerShip:PlayerShip
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
    playerLayerNode.addChild(playerShip)        
}
.
.
.

Jest to nieprawidłowe, jak wspomniano powyżej. Ale tak jest:

.
.
.
var playerShip:PlayerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}
.
.
.

Ponieważ „ja” nie zostało zainicjowane.

Mam szczerą nadzieję, że ten błąd zostanie wkrótce naprawiony.

(Tak, wiem, że mógłbym stworzyć pusty obiekt, a następnie ustawić rozmiar, ale to po prostu głupie).

Edward Kenworthy
źródło
2
To nie jest błąd, jego nowy programowy fundament OOP.
Hitendra Solanki