Зачем нужен свой инициализатор
Раньше для структур вы получали инициализатор бесплатно: Swift сам собирает memberwise-инициализатор из всех хранимых свойств. Но что, если значение нужно проверить перед сохранением, вычислить одно свойство через другое или дать пользователю несколько удобных способов создать значение — по двум числам, по одному числу, вообще без аргументов? Готового инициализатора для этого не хватит, и его пишут вручную.
У классов автоматического инициализатора вообще нет (кроме тривиального случая, когда у класса нет хранимых свойств) — там свой init нужен почти всегда.
Свой init
Инициализатор объявляют ключевым словом init — без func и без имени. У него могут быть параметры, но не может быть возвращаемого значения: тип создаваемого экземпляра и так известен заранее.
struct Temperature {
var celsius: Double
init(celsius: Double) {
self.celsius = celsius
}
}
let t = Temperature(celsius: 20)
Как только вы объявляете хотя бы один свой init, автоматический memberwise-инициализатор структуры исчезает — компилятор считает, что вы уже взяли инициализацию под свой контроль.
self. для устранения неоднозначности
Параметр инициализатора часто называют так же, как свойство, которому он присваивается, — имена совпадают чаще, чем нет. Внутри тела инициализатора такое имя ссылается на параметр, а не на свойство: параметр находится ближе по области видимости и «затеняет» свойство. Чтобы обратиться именно к свойству, перед ним пишут self.:
struct User {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
Запись self.name = name читается так: «свойству name у self присвоить значение параметра name». Если убрать self., получится name = name — присваивание параметра самому себе, а свойство так и останется без значения. Компилятор такое не пропустит.
Несколько инициализаторов
У типа может быть сколько угодно инициализаторов с разными списками параметров — как и с обычными функциями, это работает через разные метки и типы аргументов:
struct Rectangle {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
init(side: Double) {
self.init(width: side, height: side)
}
}
let square = Rectangle(side: 5)
Второй инициализатор не дублирует логику первого — он вызывает его через self.init(...), передавая одно и то же значение в оба параметра. Такой вызов называют делегированием: один инициализатор поручает основную работу другому. Делегирование через self.init работает и у структур, и у классов, пока речь идёт об инициализаторах одного и того же типа, без участия наследования.
Порядок инициализации свойств
Правило простое и жёсткое: к моменту, когда инициализатор завершает работу, у каждого хранимого свойства должно быть значение — либо заданное при объявлении, либо присвоенное в теле инициализатора. Пока это условие не выполнено, экземпляр считается не полностью готовым, и Swift не разрешит ни прочитать любое его свойство, ни вызвать метод, ни передать self куда-либо ещё.
struct Point3D {
var x: Double
var y: Double
var z: Double = 0 // значение по умолчанию — можно не трогать в init
init(x: Double, y: Double) {
self.x = x
self.y = y
// z уже получил значение при объявлении, трогать необязательно
}
}
Свойству со значением по умолчанию (z) не обязательно присваивать что-то заново внутри инициализатора — но можно, если конкретный вызов должен его переопределить.
Failable init: инициализатор, который может не создать значение
Иногда входные данные в принципе могут быть некорректными: отрицательный возраст, число вне допустимого диапазона. Вместо того чтобы создавать «сломанный» экземпляр или прерывать программу ошибкой, для таких случаев есть failable-инициализатор — init? со знаком вопроса:
struct Age {
let value: Int
init?(value: Int) {
guard value >= 0 && value <= 150 else {
return nil
}
self.value = value
}
}
let real = Age(value: 30) // Optional(Age(value: 30))
let broken = Age(value: -5) // nil
Внутри init? в любой момент можно написать return nil — это прерывает инициализацию и означает «создать значение не удалось». Результат вызова такого инициализатора — не сам тип, а Optional от него, и разворачивают его точно так же, как любой другой опционал: через if let, guard let или ??.
deinit у классов
У классов, в отличие от структур, есть ещё один особый метод — deinit. Он вызывается автоматически, ровно один раз, в момент, когда на экземпляр не остаётся ни одной сильной ссылки и он освобождается из памяти. У deinit нет параметров и круглых скобок с аргументами, в одном классе не может быть больше одного deinit, и вызвать его вручную нельзя — только сам Swift решает, когда экземпляр действительно больше не нужен.
class FileHandle {
let name: String
init(name: String) {
self.name = name
print("Открыт файл \(name)")
}
deinit {
print("Закрыт файл \(name)")
}
}
var handle: FileHandle? = FileHandle(name: "log.txt")
handle = nil // здесь вызовется deinit
У структур deinit нет вообще: у значения, в отличие от ссылки на объект в куче, нет отдельного момента «последняя ссылка исчезла» — копия просто перестаёт существовать вместе со своей областью видимости, и освобождать отдельный ресурс не нужно. Подробно, как именно и когда работает подсчёт ссылок, разберём отдельно, когда дойдём до темы автоматического управления памятью.
Частые ошибки
- Забыть
self., когда параметр называется так же, как свойство — присваивание видаname = nameничего не меняет в самом объекте, а компилятор укажет, что свойство осталось неинициализированным. - Оставить хотя бы одно хранимое свойство без значения к концу инициализатора — Swift не даст это скомпилировать, даже если кажется, что значение появится позже.
- Написать собственный
initу структуры и удивиться, что бесплатный memberwise-инициализатор пропал: как только появляется хоть один свойinit, автоматический исчезает. - Перепутать обычный
initиinit?: обычный обязан создать значение всегда, аinit?возвращаетOptional, который нужно разворачивать. - Пытаться добавить
deinitструктуре или вызватьdeinitвручную — ни то, ни другое не работает:deinitесть только у классов и запускается только самим Swift.
Резюме
- Свой
initобъявляют безfuncи без возвращаемого типа; он обязан присвоить значение каждому хранимому свойству, у которого его ещё нет. self.свойство = параметрнужен, когда имя параметра совпадает с именем свойства.- У типа может быть несколько инициализаторов с разными параметрами; один инициализатор может делегировать работу другому через
self.init(...). - Свойство со значением по умолчанию можно не трогать в инициализаторе, если это значение подходит.
init?— failable-инициализатор, может завершиться черезreturn nil; результат его вызова — всегдаOptional.deinitесть только у классов, не принимает параметров, вызывается автоматически при освобождении последней сильной ссылки и не может быть вызван вручную.- У структур нет
deinit— копии значений освобождаются вместе с областью видимости, без отдельного события.
Проверьте себя
4 вопроса
Диапазон с несколькими инициализаторами
Реализуйте структуру SafeRange с хранимыми свойствами lower и upper типа Int и двумя инициализаторами.
Первый — init(from lower: Int, to upper: Int): если lower больше upper, значения нужно поменять местами (то есть границы всегда сохраняются в правильном порядке).
Второй — init(around center: Int): должен делегировать работу первому через self.init(...), создавая диапазон от center - 1 до center + 1.
Также реализуйте функции rangeLength(from lowerRaw: Int, to upperRaw: Int) -> Int и rangeLengthAroundCenter(_ center: Int) -> Int. Каждая должна создать SafeRange подходящим инициализатором и вернуть количество целых чисел в диапазоне, то есть upper - lower + 1.
Проверка возраста через failable init
Реализуйте структуру Age с хранимым свойством value типа Int и failable-инициализатором init?(value: Int): если value меньше 0 или больше 150, инициализация должна провалиться (return nil), иначе сохранить значение.
Реализуйте функцию validAge(_ raw: Int) -> Int?, которая создаёт Age из raw и возвращает его value, либо nil, если возраст некорректен.
Реализуйте функцию ageCategory(_ raw: Int) -> String: если Age(value: raw) не удалось создать, верните "некорректный возраст"; иначе для значения меньше 18 верните "ребёнок", для значения от 18 до 64 включительно — "взрослый", для 65 и старше — "пожилой".