$ sudo teach IT

Зачем нужен свой инициализатор

Раньше для структур вы получали инициализатор бесплатно: 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 и старше — "пожилой".