Помилка класу Swift: Властивість не ініціалізується при виклику super.init


219

У мене два класи, ShapeіSquare

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

З реалізацією вище я отримую помилку:

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

Чому я повинен встановити, self.sideLengthперш ніж дзвонити super.init?


майже впевнений, що це стосується належної практики програмування, а не фактичного технічного обмеження. Якщо Shape мав би викликати функцію, яку Square замінив, Square може захотіти використовувати sideLength, але вона ще не була ініціалізована. Swift, ймовірно, просто обмежує вас робити це випадково, змушуючи вас ініціалізувати свої екземпляри спочатку перед викликом базового класу.
cwharris

3
Приклади в книзі погані. Ви завжди повинні викликати super.init () останнім, щоб переконатися, що всі властивості були ініціалізовані, пояснено нижче.
Паскаль

Відповіді:


173

Цитата з мови програмування Swift, яка відповідає на ваше запитання:

"Компілятор Swift виконує чотири корисні перевірки безпеки, щоб переконатися, що двофазна ініціалізація завершена без помилок:"

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

Уривок: Apple Inc. "Мова швидкого програмування". iBooks. https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11


47
Тепер це приголомшлива зміна порівняно з C ++, C # або Java.
MDJ

12
@MDJ Звичайно, річ. Я, чесно кажучи, не бачу додаткової вартості цього.
Рубен

20
Насправді, схоже, є один. Скажімо, у C #, конструктор надкласового класу не повинен викликати перезабірних (віртуальних) методів, тому що ніхто не знає, як би вони реагували з не повністю ініціалізованим підкласом. У Swift нормально, так як підклас-додатковий стан добре, коли працює конструктор надкласового класу. Більше того, у Swift всі методи, крім остаточних, переборливі.
MDJ

17
Мені це особливо прикро, тому що це означає, що якщо я хочу створити підклас UIView, який створює власні підпогляди, я повинен ініціалізувати ці підперегляди без фреймів для початку та додати кадри пізніше, оскільки я не можу посилатися на перегляд межі, поки ПІСЛЯ виклику super.init.
Еш

5
@Janos Якщо ви зробите властивість необов'язковою, вам не доведеться ініціалізувати її init.
JeremyP

105

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

Візьмемо об’єкт А. Визначимо його наступним чином.

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

Зауважте, що A не має надкласового класу, тому він не може викликати функцію super.init (), оскільки його не існує.

Гаразд, тепер давайте підклас А з новим класом на ім'я Б.

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

Це відхід від Objective-C, де [super init]зазвичай називають першим, перш ніж будь-що інше. Не так у Свіфта. Ви несете відповідальність за те, щоб змінні вашого примірника знаходились у послідовному стані, перш ніж робити щось інше, включаючи методи виклику (що включає ініціалізатор вашого суперкласу).


1
це надзвичайно корисно, наочний приклад. спасибі!
FullMetalFist

Що робити, якщо мені потрібно значення для обчислення y, наприклад: init (y: Int) {self.y = y * self.x super.init ()}
6rod9

1
використовувати щось на зразок, init (y: Int, x: Int = 0) {self.y = y * x; self.x = x; super.init (x: x)}, також ви не можете безпосередньо викликати порожній конструктор для суперкласу з посиланням на приклад вище, оскільки в іменах суперкласу A немає порожнього конструктора
Hitendra Solanki

43

Від док

Перевірка безпеки 1

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


Навіщо нам потрібна така перевірка безпеки?

Щоб відповісти на це, ми можемо пройти процес швидкої ініціалізації.

Двофазна ініціалізація

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

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

Отже, щоб переконатися, що процес ініціалізації в два етапи виконується, як визначено вище, є чотири перевірки безпеки, одна з них:

Перевірка безпеки 1

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

Тепер двофазова ініціалізація ніколи не говорить про порядок, але ця перевірка безпеки вводить super.init впорядковано після ініціалізації всіх властивостей.

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

Як і в цьому зразку

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.initініціалізується, кожне властивість перед використанням. Тому перевірка безпеки 1 видається нерелевантною,

Але тоді може бути інший сценарій, трохи складний,

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)

Вихід:

Shape Name :Triangle
Sides :3
Hypotenuse :12

Тут, якби ми зателефонували super.initдо встановлення цього hypotenuse, super.initвиклик тоді закликав би те, printShapeDescription()і оскільки це було відмінено, він би вперше відступив до реалізації класу Triangle printShapeDescription(). Клас printShapeDescription()Triangle має доступ до hypotenuseнеобов'язкового властивості, яке ще не було ініціалізовано. І це не дозволено як двофазна ініціалізація перешкоджає доступу до значень властивостей перед їх ініціалізацією

Тому переконайтеся, що двофазова ініціалізація виконана так, як визначено, має бути певний порядок виклику super.init, тобто після ініціалізації всіх властивостей, введених selfкласом, таким чином нам потрібна перевірка безпеки 1


1
Прекрасне пояснення, чому напевно слід додати верхню відповідь.
Гай Дахер

Отже, ви в основному говорите, оскільки надклас init може викликати функцію (переосмислену) ..., в якій ця функція отримує доступ до властивості підкласів, щоб уникнути не встановлення значень, виклик superповинен відбуватися після встановлення всіх значень. ОК має сенс. Цікаво, як це зробив Objective-C тоді і чому вам довелося зателефонувати superспочатку?
Мед

За суті , що ви вказуючи на це схоже на: розміщення printShapeDescription() до self.sides = sides; self.name = named; того, який буде генерувати цю помилку: use of 'self' in method call 'printShapeDescription' before all stored properties are initialized. Помилка ОП задається для зменшення "можливості" помилки виконання.
Мед

Я спеціально використовував слово "можливість", тому що якби printShapeDescriptionфункція, на яку не зверталися, selfтобто це було щось на зразок `print (" нічого "), то проблем не було б. (Тим НЕ менше , навіть для цього компілятор видасть помилку, тому що це не супер розумний)
Мед

Що ж, objc просто не був безпечним. Swift є безпечним для типу, тому об'єкти, які не є необов'язковими, дійсно повинні бути ненініловими!
Daij-Djan

36

"Super.init ()" слід викликати після ініціалізації всіх змінних вашого примірника.

У відео "Intermediate Swift" від Apple (ви можете знайти його на сторінці відеоресурсів Apple Developer https://developer.apple.com/videos/wwdc/2014/ ), приблизно о 28:40, прямо сказано, що всі ініціалізатори в суперклас повинен бути викликаний ПІСЛЯ ви ініціалізуєте змінні вашого примірника.

У Objective-C це було зворотним. У Swift, оскільки всі властивості потрібно ініціалізувати перед його використанням, нам потрібно спочатку ініціалізувати властивості. Це призначено для запобігання виклику перекритої функції методом "init ()" суперкласу, без попереднього ініціалізації властивостей.

Тож реалізація "Площі" повинна бути:

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

1
Ніколи б не здогадувався. Ініціалізація з супер має бути першою заявою - це те, що я міг би подумати. !! хм. досить швидка зміна.
mythicalcoder

Чому це потрібно робити після? Надайте технічну причину
Масіх

14

Вибачте за некрасиве форматування. Просто поставте персонаж питання після декларування і все буде гаразд. Питання повідомляє компілятору, що значення необов’язкове.

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

Редагувати1:

Є кращий спосіб пропустити цю помилку. Відповідно до коментаря jmaschad, немає ніяких причин використовувати необов'язкові у вашому випадку, тому що додаткові опції не зручні у використанні, і ви завжди повинні перевірити, чи необов'язковий не нульовий, перш ніж отримати доступ до нього. Отже, все, що вам потрібно зробити, це ініціалізувати члена після декларування:

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

Edit2:

Після двох мінусів на цю відповідь я знайшов ще кращий шлях. Якщо ви хочете, щоб член класу був ініціалізований у вашому конструкторі, ви повинні призначити йому початкове значення всередині контролера та перед викликом super.init (). Подобається це:

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

Успіхів у навчанні Швидкому.


Просто перемкніть super.init(name:name)і self.sideLength = sideLength. Оголошення sideLengthяк необов'язкового є помилковим і вводить додаткові клопоти пізніше, коли вам доведеться змусити його розгортати.
Йоганнес Луонг

Так, це варіант. Дякую
fnc12

Ви насправді можете просто мати var sideLength: Double, не потрібно присвоювати йому початкове значення
Jarsen

Що робити, якщо у мене справді є факультативна константа. Що я з цим роблю? Чи потрібно мені ініціалізувати в конструкторі? Я не бачу, для чого це потрібно робити, але компілятор скаржиться на Swift 1.2
Van Du Tran

1
Ідеально! всі 3 рішення спрацювали "?", "String ()", але для мене питання було в тому, що я не "призначив" жодне майно, і коли я це зробив, воно працювало! Спасибі товариш
Naishta

9

swift змушує вас ініціалізувати кожен var var, перш ніж він коли-небудь може бути використаний. Оскільки він не може бути впевнений, що станеться, коли повернеться супер, він помиляється: краще безпечно, ніж шкода


1
Це не має сенсу IMO, оскільки батьківський клас не повинен мати видимість властивостей, заявлених у ньому дітей!
Енді Хін

1
Це не так, але ви можете перекрити речі та почати використовувати самості "до того, як супер зробив це"
Daij-Djan

Ви можете бачити тут і коментарі, які слідують за ним? Я думаю, я кажу саме те, що ви говорите, тобто ми обидва кажемо, що компілятор хоче бути безпечним, а не вибачте, моє єдине питання - тож як цілі-c вирішили цю проблему? Або не вийшло? Якщо це не так, чому він все ще вимагає від вас писати super.initв першому рядку?
Мед

7

Едвард,

Ви можете змінити код у своєму прикладі так:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

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

Для цього використовується неявно розгорнута опція.

У документації ми можемо прочитати:

"Як і у випадку необов'язкових даних, якщо ви не вказуєте початкове значення, коли ви оголошуєте неявно розгорнуту необов'язкову змінну чи властивість, значення автоматично встановлюється за замовчуванням."


Я думаю, що це найчистіший варіант. У моїй першій спробі Swift у мене була змінна члена типу AVCaptureDevice, яку неможливо встановити безпосередньо, тому потрібен код init (). Однак ViewController вимагає декількох ініціалізаторів, і ви не можете викликати загальний метод ініціалізації з init (), тому ця відповідь, здається, є єдиним варіантом, який дозволяє уникнути копіювання / вставлення дублікату коду у кожен ініціалізатор.
sunetos

6

Swift не дозволить вам ініціалізувати суперкласс з ініціалізацією властивостей, зворотною до Obj C. Тому вам доведеться ініціалізувати всі властивості перед тим, як викликати "super.init".

Перейдіть на сторінку http://blog.scottlogic.com/2014/11/20/swift-initialisation.html . Це дає гарне пояснення вашій проблемі.


6

Додайте нуль до кінця декларації.


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

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

Це працювало для моєї справи, але може не працювати для вашого


Хороший, використовуючи UIView з UIViewController з протоколом
abdul sathar

1

Ви просто запрошуєте в неправильному порядку.

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

0

Напевно, я отримаю кілька голосів, але якщо чесно, життя простіше таким чином:

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

-4

Це приголомшливо дурний дизайн.

Поміркуйте приблизно так:

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

Це недійсно, як зазначалося вище. Але так і є:

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

Тому що "Я" не було ініціалізовано.

Я щиро сподіваюся, що ця помилка буде виправлена ​​найближчим часом.

(Так, я знаю, що я міг створити порожній об'єкт, а потім встановити розмір, але це просто дурно).


2
Це не помилка, її нова основна програма програмування OOP.
Hitendra Solanki
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.