Ошибка в классе Swift: свойство не инициализируется при вызове super.init

220

У меня есть два класса, 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?

JuJoDi
источник
Я уверен, что это связано с хорошими практиками программирования, а не с техническими ограничениями. Если бы Shape должен был вызвать функцию, которую Square переопределил, Square может захотеть использовать sideLength, но она еще не инициализирована. Swift, вероятно, просто запрещает вам делать это случайно, заставляя вас сначала инициализировать ваши экземпляры перед вызовом базового класса.
cwharris
3
Примеры в книге плохие. Вы должны всегда вызывать super.init () last, чтобы убедиться, что все свойства были инициализированы, объяснено ниже.
Паскаль

Ответы:

173

Цитата из языка программирования Swift, который отвечает на ваш вопрос:

«Компилятор Swift выполняет четыре полезные проверки безопасности, чтобы убедиться, что двухфазная инициализация завершена без ошибок:»

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

Выдержка из: Apple Inc. «Язык программирования Swift». интерактивные книги. https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

Ruben
источник
47
Это потрясающее изменение по сравнению с C ++, C # или Java.
MDJ
12
@MDJ Конечно. Я, честно говоря, тоже не вижу добавленной стоимости.
Рубен
20
На самом деле, кажется, есть один. Скажем, в C # конструктор суперкласса не должен вызывать какие-либо переопределенные (виртуальные) методы, потому что никто не знает, как они будут реагировать с не полностью инициализированным подклассом. В Swift это нормально, так как состояние subclass-extra хорошо, когда работает конструктор суперкласса. Более того, в Swift все методы, кроме final, могут быть переопределены.
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 (), поскольку он не существует.

Хорошо, теперь давайте подкласс A с новым классом с именем B.

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

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

Хитендра Соланки
источник
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.

Двухфазная инициализация

Инициализация класса в 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

BangOperator
источник
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. Ошибка OP дана для уменьшения «возможности» ошибки времени выполнения.
Мед
Я специально использовал слово «возможность», потому что, если бы это printShapeDescriptionбыла функция, которая не имела отношения к, selfт. Е. Это было что-то вроде «печать (« ничего »)», тогда не было бы проблем. (Тем не менее, даже для этого компилятор выдаст ошибку, потому что он не очень умный)
Honey
Ну, объект просто не был в безопасности. Swift безопасен от типов, поэтому объекты, которые не являются необязательными, должны быть ненулевыми!
Дай-Джан
36

«Super.init ()» следует вызывать после инициализации всех переменных вашего экземпляра.

В видео Apple «Intermediate Swift» (вы можете найти его на странице видеоматериала 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
    }
}
Shuyang
источник
1
Никогда бы не догадался. Я бы подумал, что инициализация с супер должна быть первой. !! гектометр Сильное изменение со Swift.
Мифический
Почему это должно прийти после? Пожалуйста, предоставьте техническую причину
Масих
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
    }
}

Edit1:

Есть лучший способ пропустить эту ошибку. Согласно комментарию 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:

После получения двух минусов на этот ответ я нашел еще лучший способ. Если вы хотите, чтобы член класса был инициализирован в вашем конструкторе, вы должны присвоить ему начальное значение внутри contructor и перед вызовом 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
    }
}

Удачи в изучении Swift.

fnc12
источник
Просто переключитесь super.init(name:name)и self.sideLength = sideLength. Объявление sideLengthкак необязательное вводит в заблуждение и создает дополнительные хлопоты позже, когда вам придется принудительно развернуть его.
Йоханнес Луонг
Да, это вариант. Спасибо
fnc12
Вы можете просто иметь var sideLength: Double, не нужно присваивать ему начальное значение
Jarsen
Что если у меня действительно есть необязательная константа? Что мне с этим делать? Нужно ли инициализировать в конструкторе? Я не понимаю, почему вы должны это делать, но компилятор жалуется на Swift 1.2
Ван Ду Тран
1
Отлично ! все 3 решения работали «?», «String ()», но проблема для меня заключалась в том, что я не «назначил» одно из свойств, и когда я это сделал, это сработало! Спасибо друг
Найша
9

swift заставляет вас инициализировать каждый элемент var до того, как он когда-либо будет использоваться. Поскольку он не может быть уверен, что произойдет, когда наступит супер-поворот, он выдаст ошибку: лучше, чем потом сожалеть

DAij-Джан
источник
1
Это не имеет смысла IMO, потому что родительский класс не должен видеть свойства, объявленные в его дочерних классах!
Энди Хин
1
Это не так, но вы можете переопределить материал и начать использовать self «до того, как это
сделает
Можете ли вы увидеть здесь и комментарии, которые следуют за этим? Я думаю, что я говорю именно то, что вы говорите, т.е. мы оба говорим, что компилятор хочет быть безопасным, а не сожалеть, мой единственный вопрос, так как же target-c решил эту проблему? Или это не так? Если этого не произошло, то почему он все еще требует от вас писать super.initв первой строке?
Мед
7

Эдвард,

Вы можете изменить код в вашем примере следующим образом:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

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

Это использует неявно развернутый необязательный.

В документации мы можем прочитать:

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

Павел Губарев
источник
Я думаю, что это самый чистый вариант. В моей самой первой попытке 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
        }
    }
ylgwhyh
источник
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 }
}
Renetik
источник
-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
Это не ошибка, это новый фундамент программирования ООП.
Хитендра Соланки