Модель инициализации Swift была разработана, чтобы устранить неопределенное поведение, распространенное в языках, таких как Objective-C, где доступ к методам или свойствам экземпляра до полной инициализации всей памяти мог привести к ошибкам сегментации или уязвимостям безопасности. Основная проблема заключается в иерархиях классов: объект подкласса содержит память для собственных хранимых свойств плюс все унаследованные свойства, и компилятор должен гарантировать, что никакой код не взаимодействует с этой памятью, пока каждый байт не станет действительным. Для решения этой проблемы Swift вводит инвариант определенной инициализации (DI) с помощью статического анализа, требуя, чтобы объект оставался в частично сконструированном, небезопасном состоянии до завершения Фазы 1 своего двухфазного инициализатора. В течение Фазы 1 инициализатор должен присвоить значения всем свойствам, представленным текущим классом, и делегировать вызов вверх к инициализаторам суперкласса; только после завершения этой фазы self можно безопасно использовать или передавать.
class Vehicle { let wheelCount: Int init(wheels: Int) { self.wheelCount = wheels // Фаза 1 завершена для Vehicle } } class Bicycle: Vehicle { let hasBell: Bool init(bell: Bool) { // Фаза 1: Сначала инициализируем собственные свойства self.hasBell = bell // Затем делегируем вызов суперклассу super.init(wheels: 2) // Фаза 1 завершена: полная определенная инициализация достигнута // Фаза 2: Можно безопасно использовать self self.checkSafety() } func checkSafety() { print("Велосипед с \(wheelCount) колесами \(hasBell ? "имеет" : "не имеет") звонок") } }
При разработке приложения для медицинских записей мы столкнулись со сложной ситуацией с суперклассом PatientRecord и подклассом ICUPatientRecord, который требовал вычислять показатель тяжести на основе возраста пациента (свойства суперкласса) во время инициализации. Первоначальная реализация попыталась вызвать вспомогательный метод calculateSeverity(), который обращался к self.age, до вызова super.init(age:), что привело к ошибке компиляции, так как инициализатор подкласса еще не гарантировал безопасность унаследованной памяти. Мы оценили три различных архитектурных подхода для решения этой проблемы.
Один из подходов заключался в том, чтобы объявить показатель тяжести как неявно разворачиваемую опциональную переменную (var severity: Int!) и отложить его присвоение до завершения инициализации суперкласса. Хотя это удовлетворяло компилятор, это привело к значительному риску во время выполнения: свойство могло быть доступно до присвоения, что вызывало бы сбой, и это предотвратило бы использование неизменяемого объявления let, нарушая целостность записи.
Вторая стратегия рассматривала использование статического фабричного метода, который создавал временный объект-заполнитель только для чтения возраста, вычисления тяжести в оффлайне, а затем создания реального экземпляра с предвычисленными значениями. Это сохраняло безопасность памяти, но добавляло значительный набор вспомогательного кода и затемняло поток инициализации, делая кодовую базу значительно сложнее для поддержки и отладки другими членами команды.
Выбранное решение включало в себя реорганизацию инициализатора, чтобы принимать возраст в качестве параметра, вычисляя тяжесть с использованием чистой статической функции, которая работала с входным параметром, а не со свойством экземпляра, и передавая предвычисленное значение в назначенный инициализатор. Этот подход поддерживал неизменяемость, позволяя severity быть константой let, строго соблюдая правила двухфазной инициализации и позволяя компилятору проверять безопасность во время компиляции, а не во время выполнения. Результатом стала последовательность инициализации без сбоев, которая ясно выражала зависимость данных между возрастом и тяжестью, при этом используя статический анализ Swift для предотвращения регрессии.
Почему компилятор не позволяет вызывать методы экземпляра на self, даже если эти методы определены в подклассе и кажутся не связанными со свойствами суперкласса?
Компилятор налагает это ограничение, потому что объект существует как выделенная память, но часть суперкласса остается неинициализированной сырой памятью. Любой вызов метода на self — независимо от того, где он определен — получает полный указатель на объект и может потенциально получить доступ к неинициализированным полям суперкласса косвенным образом, нарушая безопасность памяти. Swift осторожно рассматривает все использования self до завершения Фазы 1 как небезопасные, разрешая только прямые присвоения хранимым свойствам текущего класса.
Как анализ определенной инициализации обрабатывает свойства с weak ссылками по сравнению с свойствами с unowned ссылками?
Проверка определенной инициализации рассматривает опциональные типы, включая переменные weak, которые являются неявно опциональными, как имеющие действительное начальное значение nil, автоматически внедренное компилятором. Следовательно, свойства weak не требуют явной инициализации в инициализаторах. Напротив, unowned ссылки являются необязательными и предполагают немедленную не-nil семантику; следовательно, им должно быть присвоено значение до завершения инициализации, так же как и сильным ссылкам, иначе компилятор выдаст ошибку по определенной инициализации.
Что отличает правила делегирования для удобных инициализаторов от назначенных инициализаторов с точки зрения определенной инициализации?
Удобные инициализаторы действуют как вторичные точки входа, которые должны делегировать вызов назначенному инициализатору (через self.init) перед выполнением любых операций, специфичных для экземпляра. Им строго запрещено напрямую инициализировать хранимые свойства, потому что назначенный инициализатор, на который они ссылаются, несет ответственность за выполнение требований к определенной инициализации. Это контрастирует с назначенными инициализаторами, которые должны инициализировать все свойства, представленные их классом, перед делегированием вызова к инициализатору суперкласса, обеспечивая правильность объекта на каждом уровне иерархии.