При инициализации класса свойство по умолчанию обращается к self: почему Swift запрещает это до завершения инициализации?
Swift запрещает обращение к self в инициализаторе обычного хранимого свойства, потому что это свойство вычисляется до выполнения тела инициализатора и до полной инициализации экземпляра. В этот момент объект может находиться в частично инициализированном состоянии. Обращаться к self можно после инициализации всех хранимых свойств, например в теле инициализатора или при первом доступе к lazy-свойству.
Двухфазная инициализация в Swift предназначена для безопасного создания объектов, особенно в иерархиях классов. Сначала должны получить корректные значения все хранимые свойства, затем объект можно считать полностью сформированным и передавать его методам или замыканиям.
Такой подход предотвращает использование экземпляра, в котором часть состояния ещё не установлена. Он также задаёт предсказуемый порядок инициализации свойств текущего класса и суперкласса.
Значение обычного хранимого свойства с инициализатором по умолчанию вычисляется до выполнения тела init. Если это вычисление обращается к другому свойству через self, Swift не может гарантировать, что нужное свойство уже инициализировано.
Разрешение такого обращения могло бы привести к чтению неопределённого состояния, вызову метода на неполностью созданном объекте или публикации self до завершения инициализации. Поэтому компилятор отклоняет обращение к self на этом этапе, даже если разработчик фактически ожидает безопасный порядок.
В первой фазе инициализации класс должен установить значения всех собственных хранимых свойств. До завершения этой фазы self нельзя использовать для чтения свойств, вызова экземплярных методов или передачи объекта наружу.
После присваивания всех обязательных свойств собственными инициализаторами и завершения необходимых шагов иерархии self становится доступен. Поэтому вычисление свойства можно перенести в тело инициализатора после нужных присваиваний.
lazy-свойство является исключением по времени вычисления: его значение не создаётся при инициализации экземпляра. Оно вычисляется при первом обращении, когда self уже полностью инициализирован. Компромисс состоит в том, что первое чтение может выполнять работу и изменять состояние; кроме того, lazy применим только к переменным свойствам.
Здесь greeting вычисляется безопасно в инициализаторе из уже доступного параметра, а upperName обращается к состоянию экземпляра только при первом чтении. Если свойство должно вычисляться один раз во время создания объекта, его следует явно присвоить в init, а не делать lazy.
В объекте экрана нужно сформировать форматированное имя пользователя. Вариант с обычным значением по умолчанию, обращающимся к другому свойству через self, не компилируется. Вариант с lazy откладывает вычисление и удобен, если значение нужно не всегда, но добавляет отложенную работу и потенциальную проблему многопоточного первого доступа.
Можно вычислить строку непосредственно в init из его параметра. Это наиболее предсказуемый вариант: значение готово сразу, порядок очевиден, а доступ к self до завершения инициализации не требуется. Если вычисление дорогое и результат может не понадобиться, обоснованнее выбрать lazy.
Нет, сам факт присваивания одного свойства ещё не означает, что экземпляр полностью инициализирован. До использования self должны быть инициализированы все хранимые свойства текущего класса, а для класса также должны быть выполнены требования инициализации суперкласса. Безопасный вариант — вычислять второе значение из параметров или присваивать его после завершения необходимой фазы инициализации.
lazy-инициализатор запускается при обращении после создания экземпляра. Замыкание, присвоенное свойству в init после допустимого момента, также может захватывать self, но оно может создать цикл сильных ссылок, если объект хранит это замыкание, а замыкание удерживает объект. Для разрыва цикла обычно применяют слабый или небезопасно неконтролируемый захват, выбирая между безопасностью weak и гарантией времени жизни unowned.
Статический член принадлежит типу, а не конкретному экземпляру, поэтому для его вычисления не требуется partially initialized self. Он не зависит от состояния создаваемого объекта. Однако если вычисление статического значения косвенно требует экземплярного состояния, это состояние всё равно нельзя получить до завершения инициализации.