Обёртки свойств и макросы
Во что компилируется обёртка свойства, наблюдатели свойств и `lazy`, KeyPath и чем макрос Swift вроде `@Observable` отличается от обёртки.
10 вопросов
JuniorТеорияОчень частоЧто такое newValue и oldValue в willSet/didSet и могут ли быть наблюдатели у вычисляемого свойства?
Что такое newValue и oldValue в willSet/didSet и могут ли быть наблюдатели у вычисляемого свойства?
willSet выполняется перед записью и даёт входящее значение как newValue; didSet — после и даёт прежнее значение как oldValue. У вычисляемого свойства наблюдателей быть не может — логику кладут в его get/set.
Типичные ошибки
- ✗Путают, какой наблюдатель видит входящее значение, а какой — прежнее
- ✗Пытаются повесить
willSet/didSetна вычисляемое свойство - ✗Ждут срабатывания наблюдателей при чтении, а не только при записи
Уточняющие вопросы
- →Можно ли переименовать
newValue/oldValueи какого они типа? - →Почему вычисляемое свойство кладёт логику изменения в
set, а не вdidSet?
JuniorТеорияОчень частоЧем хранимое свойство отличается от вычисляемого и что меняет для него static?
Чем хранимое свойство отличается от вычисляемого и что меняет для него static?
Хранимое свойство держит значение в памяти самого экземпляра; вычисляемое не имеет хранилища и выполняет get/set при каждом обращении. static привязывает свойство к типу, а не к экземпляру — одно значение, хранимое раз на тип.
Типичные ошибки
- ✗Думают, что вычисляемое свойство резервирует хранилище, как хранимое
- ✗Считают, что
static-свойство живёт по разу на экземпляр, а не на тип - ✗Полагают, что
static— это лишь про контроль доступа или read-only
Уточняющие вопросы
- →Чем
staticотличается отclassпри объявлении свойства типа? - →Когда на самом деле инициализируется хранимое
static-свойство?
JuniorТеорияЧастоКогда инициализируется lazy var, почему это не может быть let и потокобезопасно ли оно?
Когда инициализируется lazy var, почему это не может быть let и потокобезопасно ли оно?
lazy var выполняет инициализатор при первом обращении, а не при создании объекта, поэтому может обращаться к self. Он остаётся var, ведь первое обращение записывает его слот, и он не потокобезопасен при конкурентных первых чтениях.
Типичные ошибки
- ✗Думают, что
lazy-свойство инициализируется сразу вместе с другими хранимыми свойствами - ✗Считают ленивую инициализацию автоматически потокобезопасной
- ✗Думают, что
lazy letразрешён
Уточняющие вопросы
- →Почему инициализатор
lazy varможет обращаться кself, а у обычного хранимого свойства — нет? - →Как сделать
lazy-свойство безопасным при конкурентном первом обращении?
MiddleДебаггингЧастоПочему willSet/didSet не срабатывают при присваивании внутри init и как это обойти?
Почему willSet/didSet не срабатывают при присваивании внутри init и как это обойти?
Внутри init объект ещё не полностью инициализирован, поэтому первое присваивание ставит значение, и Swift пропускает наблюдатели. Обходят это, запуская эффект после init вручную, либо через обёртку свойства, чей setter выполняется всегда.
Типичные ошибки
- ✗Ждут, что побочный эффект
didSetотработает для значения, присвоенного вinit - ✗Винят «вырезание» наблюдателей из-за наличия собственного инициализатора
- ✗Берут
lazyвместо явного запуска эффекта после инициализации
Уточняющие вопросы
- →Срабатывают ли наблюдатели, если свойство меняет метод, вызванный из
init? - →Почему setter обёртки свойства срабатывает при инициализирующем присваивании?
MiddleТеорияЧастоЧто из static let, class var и глобальной переменной инициализируется лениво и потокобезопасно?
Что из static let, class var и глобальной переменной инициализируется лениво и потокобезопасно?
static let и глобальная инициализируются лениво при первом обращении, ровно один раз, и рантайм делает эту инициализацию потокобезопасной. class var обязана быть вычисляемой — у неё нет слота для ленивой инициализации.
Типичные ошибки
- ✗Считают, что
static letинициализируется жадно при запуске, а не при первом обращении - ✗Ждут хранимую
class var, хотя для переопределения нужна вычисляемаяclass/static - ✗Думают, что локальный
lazy varполучает ту же потокобезопасность «ровно раз», что иstatic
Уточняющие вопросы
- →Почему
static letпотокобезопасен, аlazy varэкземпляра — нет? - →Какой механизм гарантирует, что глобальная инициализируется ровно один раз?
SeniorДизайнЧастоВы переносите SwiftUI-экран, чья view model — это class, соответствующий протоколу ObservableObject, с несколькими свойствами @Published. Коллега просит перевести её на макрос @Observable из iOS 17. Объясните, что макрос @Observable генерирует на классе, чем попроперти-отслеживание изменений отличается от сигнала objectWillChange на весь объект, который шлёт @Published, и почему это делает обновления представления дешевле. Скажите, как меняется владение во view — какая SwiftUI-обёртка заменяет @StateObject — и назовите одно поведенческое отличие, которое небрежная миграция может сломать, например вычисляемое свойство, читающее отслеживаемое хранимое. Ограничьтесь последствиями для наблюдения и отрисовки, без посторонних рефакторингов.
Вы переносите SwiftUI-экран, чья view model — это class, соответствующий протоколу ObservableObject, с несколькими свойствами @Published. Коллега просит перевести её на макрос @Observable из iOS 17. Объясните, что макрос @Observable генерирует на классе, чем попроперти-отслеживание изменений отличается от сигнала objectWillChange на весь объект, который шлёт @Published, и почему это делает обновления представления дешевле. Скажите, как меняется владение во view — какая SwiftUI-обёртка заменяет @StateObject — и назовите одно поведенческое отличие, которое небрежная миграция может сломать, например вычисляемое свойство, читающее отслеживаемое хранимое. Ограничьтесь последствиями для наблюдения и отрисовки, без посторонних рефакторингов.
Макрос @Observable переписывает класс, отслеживая каждое хранимое свойство отдельно через Observation framework, поэтому изменение перерисовывает лишь читателей. ObservableObject же шлёт один objectWillChange на весь объект, пересчитывая наблюдателей. Модель держат через @State, а не @StateObject.
Типичные ошибки
- ✗Считают, что
@Observableпо-прежнему шлёт изменение на весь объект, как@Published - ✗Оставляют
@StateObject/@ObservedObjectвместо перехода на@State - ✗Думают, что отслеживание делает рантайм-KVO, а не разворачивание макроса при компиляции
Уточняющие вопросы
- →Почему view должно читать свойство в
body, чтобы@Observableего отслеживал? - →Что происходит с вычисляемым свойством, производным от отслеживаемого хранимого?
MiddleКодИногдаНапишите обёртку свойства @Clamped, удерживающую значение в диапазоне
Напишите обёртку свойства @Clamped, удерживающую значение в диапазоне
Структура с @propertyWrapper хранит ClosedRange и backing-значение; setter её wrappedValue зажимает через min(max(...)), и init зажимает начальное значение. Любая запись загоняется обратно в диапазон.
Типичные ошибки
- ✗Забывают зажать начальное значение, переданное через
init(wrappedValue:) - ✗Зажимают в
didSet, который не срабатывает при присваивании вinit - ✗Хранят сырое значение и зажимают только при чтении свойства
Уточняющие вопросы
- →Как
@Clampedможет выдатьprojectedValue, доступное через$? - →Почему
Valueдолжен бытьComparable, чтобы зажим скомпилировался?
MiddleТеорияИногдаЧто такое key path вроде \User.name, чем отличается WritableKeyPath и зачем передавать его в дженерик-функцию?
Что такое key path вроде \User.name, чем отличается WritableKeyPath и зачем передавать его в дженерик-функцию?
Key path — хранимая типизированная ссылка на свойство \User.name типа KeyPath<User, String>; WritableKeyPath вдобавок разрешает запись. Дженерик принимает KeyPath<Root, Value> вместо замыкания (Root) -> Value, поэтому users.map(\.name).
Типичные ошибки
- ✗Принимают key path за замыкание, а не за хранимое сравнимое значение
- ✗Путают доступный только на чтение
KeyPathсWritableKeyPath - ✗Думают, что key path — нетипизированные строки, разрешаемые рефлексией в рантайме
Уточняющие вопросы
- →Как сокращение
\.nameвыводит свой типRoot? - →Когда вместо этого нужен
ReferenceWritableKeyPath?
MiddleТеорияИногдаВо что компилируется обёртка свойства и чем на самом деле является префикс $?
Во что компилируется обёртка свойства и чем на самом деле является префикс $?
Компилятор превращает @Wrapper var x в скрытое _x типа обёртки, и любое чтение или запись x идёт через wrappedValue. Префикс $x открывает необязательное projectedValue обёртки, а не встроенный binding.
Типичные ошибки
- ✗Считают, что
$всегда даёт SwiftUIBinding, а неprojectedValueобёртки - ✗Думают, что обёртка применяется в рантайме, а не синтезируется компилятором
- ✗Забывают, что
_x— это и есть экземпляр обёртки за свойством
Уточняющие вопросы
- →Когда у обёртки нет доступной проекции
$? - →Как код может напрямую обратиться к самому экземпляру обёртки?
SeniorТеорияРедкоЧто у хранимого, вычисляемого свойства, обёртки свойства и макроса Swift разрешается при компиляции, а что в рантайме?
Что у хранимого, вычисляемого свойства, обёртки свойства и макроса Swift разрешается при компиляции, а что в рантайме?
Раскладка хранения, структура-обёртка и код макроса фиксируются при компиляции. В рантайме есть лишь аксессоры — get/set вычисляемого и wrappedValue обёртки. Макрос не добавляет рантайм-механики — это чистая генерация исходника.
Типичные ошибки
- ✗Считают, что макрос оставляет рантайм-механику, а не разворачивается в обычный код
- ✗Думают, что обёртки свойств применяются рантайм-рефлексией
- ✗Полагают, что аксессоры вычисляемого разрешаются полностью при компиляции без вызова в рантайме
Уточняющие вопросы
- →Что оставляет в бинарнике макрос по сравнению с обёрткой свойства?
- →Где вычисляемое свойство несёт свою единственную рантайм-стоимость?