Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
12 подробных ответов
Проверка по первоисточникам опубликована для 2 из 12 ответов. Дата и область проверки указаны у каждого проверенного ответа.
01В чём реальные различия между struct и class в Swift?
junior
Короткий ответ: struct — значимый тип, class — ссылочный тип с идентичностью. Обычная копируемая структура имеет семантику копирования; присваивание ссылки на класс разделяет экземпляр. Наследование и === относятся к классам. С Swift 5.9 существуют некопируемые структуры ~Copyable: они могут иметь deinit, но не допускают обычного копирования.
Подробно:
struct |
class |
|
|---|---|---|
| Семантика | значение; обычно Copyable |
общая ссылка |
| Наследование | нет, но есть протоколы | есть |
Идентичность === |
нет | есть |
deinit |
у некопируемых структур | есть |
| Мутация | для изменения хранимых полей нужны var и mutating-методы |
свойства var можно менять и через ссылку let |
| ARC | может применяться к ссылочным полям | управляет временем жизни экземпляра |
| Инициализатор | memberwise при выполнении условий синтеза | автоматический default/inherited — лишь при определённых условиях |
- Структура удобна для независимых значений. Копирование может быть оптимизировано, например через copy-on-write; ссылочные поля всё равно могут разделять изменяемое состояние.
- Класс нужен, когда важна общая идентичность или наследование. Безопасность потоков не следует автоматически ни из
struct, ни изclass. - Некопируемый ресурс можно моделировать
struct Resource: ~Copyable { deinit {} }: владение передают без копирования.
⚠️ Частая ошибка: «структуры всегда живут на стеке». Семантика значения не определяет размещение в памяти: структура может находиться внутри объекта в куче или быть упакована в экзистенциал.
02Что напечатает этот код: массив структур против массива экземпляров класса?
junior
Короткий ответ: Присваивание массива структур даёт независимую копию — мутация копии оригинал не трогает. Присваивание массива классов копирует только ссылки — объекты общие, мутация видна через обе переменные.
Подробно:
struct Point { var x = 0 }
final class Box { var x = 0 }
var a = [Point(), Point()]
var b = a
b[0].x = 99
print(a[0].x) // 0 — у b своя копия массива структур
let c = [Box(), Box()]
let d = c
d[0].x = 99
print(c[0].x) // 99 — скопировались только ссылки, Box общий
- Массив — тоже структура —
b = aсемантически копия; физически буфер общий до первой мутации (CoW), но наблюдаемое поведение — полная копия. - Элементы-классы — копируются указатели,
d[0]иc[0]— один и тот же объект в куче. let dне защищает —letфиксирует ссылку, а не состояние объекта: поля класса мутировать можно.
⚠️ Частая ошибка: ответить «99 и 99», забыв, что value-семантика массива структур распространяется и на элементы: b[0].x = 99 мутирует элемент внутри копии, а не общий объект.
03Как устроен Copy-on-Write в коллекциях Swift, и получит ли его твоя структура автоматически?
middle
Короткий ответ: CoW — оптимизация value-семантики: при присваивании копируется только ссылка на общий буфер, а реальная копия делается лишь при мутации, если буфер разделяется (isKnownUniquelyReferenced). Array/String/Dictionary реализуют это вручную. Твоя структура CoW автоматически не получает.
Подробно:
- Присваивание — O(1): обе переменные указывают на один heap-буфер, счётчик ссылок = 2.
- Мутация — проверка уникальности; если буфер общий, сначала копия, потом запись.
- Своя структура — если она держит ссылку на class-хранилище, копируется только указатель, и мутация «протечёт» во все копии. Проверку пишешь сам:
final class Storage { var values: [Int] = [] }
struct Buffer {
private var storage = Storage()
mutating func set(_ v: Int, at i: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy() // копия только при общем буфере
}
storage.values[i] = v
}
}
⚠️ Частая ошибка: «все value-типы в Swift копируются через CoW». Нет: обычная структура копируется побитово сразу; CoW — ручной паттерн, из коробки он есть только у стандартных коллекций.
04Что такое Optional под капотом, и когда force unwrap допустим?
junior
Короткий ответ: Опционал — обычный дженерик-enum с двумя кейсами: .some(Wrapped) и .none. T? — синтаксический сахар для Optional<T>, nil — литерал для .none. Force unwrap (!) терпим для инвариантов, которые гарантирует программист, но в потоке бизнес-логики — code smell.
Подробно:
// В стандартной библиотеке буквально:
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
guard let user = findUser(id) else { return } // ранний выход
if let name = user.nickname { greet(name) } // локальная развёртка
let count = user.nickname?.count ?? 0 // чейнинг + nil-coalescing
Лестница развёртки — от предпочтительного к крайнему:
guard let— инвариант дальше по функции, ранний выход.if let/?./??— локальная работа со значением и дефолты.!— только для гарантированных программистом инвариантов:IBOutletпосле загрузки view, ресурс в бандле, фикстура в тестах. Падение здесь — сигнал сборки, а не пользовательский сценарий.
⚠️ Частая ошибка: «! нельзя никогда». Зрелый ответ — про инварианты: URL(string: "https://api.example.com")! с константным литералом честнее, чем молчаливый ?? с бессмысленным дефолтом, маскирующий баг.
05Почему значения протокола с associatedtype нельзя просто сложить в массив, и какие есть альтернативы?
middle
Короткий ответ: Протокол с associatedtype — не конкретный тип, а шаблон требований: компилятор не знает, чем является T. До Swift 5.7 такой протокол вообще нельзя было использовать как тип; сейчас можно any P — экзистенциальный бокс с динамическим диспатчем, но ассоциированный тип наружу выходит стёртым. Альтернативы — дженерики или type erasure.
Подробно:
protocol Repository {
associatedtype Entity
func all() -> [Entity]
}
// let repos: [Repository] = [] // ✗ до 5.7: 'Repository' can only be used
// // as a generic constraint
let repos: [any Repository] = [] // ✓ бокс, но Entity снаружи стёрт
func sync<R: Repository>(_ repo: R) -> [R.Entity] {
repo.all() // ✓ дженерик: Entity известен компилятору
}
any P— гетерогенная коллекция возможна, но результатall()теряет конкретныйEntityи приходит косвенно, через witness table.- Дженерик
<R: Repository>— полный доступ кR.Entity, статический диспатч; но один вызов — один конкретный тип. - Type erasure — обёртки вида
AnyPublisher,AnySequence: прячут конкретный тип, сохраняя ассоциированный (AnyPublisher<Int, Never>).
⚠️ Частая ошибка: лечить ошибку компилятора навешиванием any не думая. Если коллекция гомогенна — дженерик и быстрее, и сохраняет типы.
06some P, any P и дженерик <T: P>: что компилятор делает по-разному?
senior
Короткий ответ: В результате функции some P скрывает один конкретный тип, выбранный реализацией; <T: P> принимает тип, выбранный вызывающим. any P хранит значение одного из подходящих типов через экзистенциал. Это различия модели типов, а не безусловные обещания статического диспатча или аллокаций.
Подробно:
| Форма | Кто выбирает конкретный тип | Типичное применение |
|---|---|---|
Результат some P |
реализация; фиксирован для конкретной подстановки generic-параметров | скрыть конкретный результат, сохранив его типовую идентичность |
Параметр some P |
вызывающий; сокращение generic-параметра | простой generic-аргумент |
<T: P> |
вызывающий | связать типы аргументов/результата, выразить ограничения |
any P |
конкретное значение может меняться во время выполнения | хранить разные реализации за одним интерфейсом |
- Дженерики — Swift поддерживает неспециализированный код с metadata и witness tables. Если тело и конкретный тип доступны оптимизатору, он может специализировать и встроить вызовы; копия кода на каждый call site не гарантируется.
- Opaque result —
some Pсохраняет конкретную типовую идентичность, хотя вызывающий не знает её имени. Это создаёт возможности оптимизации, но на границе модуля вызов не обязан стать прямым. - Экзистенциал — хранит значение и сведения о типе/соответствии. Небольшое значение часто помещается в inline-буфер, крупное может требовать heap box; layout зависит от ограничений протокола и ABI. Оптимизатор иногда устраняет косвенность.
- Associated types — у
any Pчасть информации стирается, но современные constrained existentials могут сохранять явно заданные ограничения. Не сводите различие к «associated types всегда теряются».
⚠️ Частая ошибка: обещать, что some/generics всегда статические и без аллокаций, а any всегда выделяет память. Выбирайте по API и сохранению связей типов; измеряйте горячий код в release-сборке.
07Зачем существует @escaping и что он меняет для замыкания?
junior
Короткий ответ: @escaping маркирует замыкание, которое переживает вызов функции — его сохраняют в свойство или передают в асинхронную работу. Это меняет две вещи: захваты переезжают в кучу (замыкание живёт дольше кадра стека), и компилятор требует явного self, делая семантику захвата видимой.
Подробно:
final class Loader {
var completion: (() -> Void)?
func load(_ done: @escaping () -> Void) {
completion = done // сохранили → escaping обязателен
DispatchQueue.global().async { done() } // уйдёт в async → тоже escaping
}
func forEachItem(_ body: () -> Void) {
body() // non-escaping: умирает до return
}
}
- Non-escaping (по умолчанию) — замыкание гарантированно вызовется до возврата функции: захваты можно держать на стеке,
selfпишется неявно. - Escaping — время жизни неизвестно: контекст захвата аллоцируется в куче, а явный
self.— сигнал «здесь возможен retain cycle, подумай про[weak self]». - Где встречается — completion-хендлеры сетевых вызовов, хранение колбэков,
DispatchQueue.async.
⚠️ Частая ошибка: говорить, что @escaping «делает замыкание асинхронным». Нет: это только о времени жизни. Escaping-замыкание может быть вызвано синхронно — или вообще никогда.
08Как enum с associated values помогает смоделировать состояние экрана?
junior
Короткий ответ: Каждое состояние — кейс, а его данные — associated values: .loaded([Item]) физически не существует без массива, .error(Error) — без ошибки. Невозможные состояния становятся непредставимыми, а switch заставляет обработать все кейсы.
Подробно:
enum ScreenState {
case loading
case loaded([Item])
case error(Error)
}
switch state {
case .loading: showSpinner()
case .loaded(let items): render(items)
case .error(let e): showRetry(e)
} // добавишь кейс — компилятор укажет все места
- Против bool-флагов —
isLoading+items: [Item]?+error: Error?дают 8 комбинаций, из которых валидны 3: «loading и одновременно error» приходится запрещать дисциплиной. Enum запрещает их типом. - Данные привязаны к кейсу — до
itemsне добраться, пока не доказалswitch-ем, что состояние.loaded. indirect— для рекурсивных enum (дерево, JSON):indirect case node(Tree, Tree)— Swift упакует рекурсию через указатель.
⚠️ Частая ошибка: ставить default: в switch по своему enum. Он выключает главное преимущество — исчерпывающую проверку: новый кейс молча провалится в default, и компилятор промолчит.
09Какие виды диспетчеризации методов есть в Swift, и что напечатает классический пример с protocol extension?
senior
Короткий ответ: Три механизма: прямой (static) — адрес известен на компиляции; табличный — vtable у классов и witness table у протоколов; message dispatch — objc_msgSend для @objc dynamic. Знаменитая ловушка: метод из protocol extension, не объявленный требованием протокола, диспатчится статически — по типу переменной.
Подробно:
- Прямой — методы структур,
final-методы, методы в extension. Самый быстрый, инлайнится. - Vtable / witness table — обычные методы классов; требования протокола при вызове через
any P. - Message —
@objc dynamic:objc_msgSend, нужен для KVO и swizzling.
protocol Greeter { func hello() }
extension Greeter {
func hello() { print("protocol hello") }
func bye() { print("protocol bye") } // НЕ требование протокола
}
struct Person: Greeter {
func hello() { print("Person hello") }
func bye() { print("Person bye") }
}
let g: any Greeter = Person()
g.hello() // "Person hello" — требование → witness table → реализация типа
g.bye() // "protocol bye" — не требование → статически по типу any Greeter
let p = Person()
p.bye() // "Person bye" — конкретный тип известен
⚠️ Частая ошибка: ждать «Person bye» от g.bye(). Метод не в списке требований — witness table про него не знает, компилятор зашивает вызов версии из extension. Вторая ловушка: метод класса из extension нельзя переопределить в наследнике без @objc dynamic — extension-методы диспатчатся напрямую.
10В чём разница между static и class членами типа?
middle
Короткий ответ: Оба — члены типа, а не экземпляра. static доступен везде (struct, enum, class) и запрещает переопределение — это буквально final class. class существует только в классах и разрешает override в наследниках. Хранимых class-свойств не бывает — только вычисляемые.
Подробно:
class Animal {
static func kind() -> String { "animal" } // = final class func
class func sound() -> String { "..." } // можно переопределить
// class var name = "x" // ✗ class stored properties not supported
class var legs: Int { 4 } // ✓ вычисляемое — можно
static let planet = "Earth" // ✓ хранимое — только static
}
class Dog: Animal {
override class func sound() -> String { "woof" }
// override static func kind() ... // ✗ cannot override static method
}
Dog.sound() // "woof" — диспатч по метатипу через vtable
static— статический диспатч, работает во всех типах; хранимые свойства типа — только так (и они лениво и потокобезопасно инициализируются).class— участвует в динамическом диспатче по метатипу:type(of: obj).sound()уважает переопределение.- Выбор — по умолчанию
static;class— когда полиморфизм на уровне типа реально нужен (фабрики,reuseIdentifier).
⚠️ Частая ошибка: «static — для структур, class — для классов». Нет: static отлично живёт в классах и означает там final class; различие — в возможности override, а не в месте использования.
11Когда инициализируется lazy-свойство, и потокобезопасно ли это?
junior
Короткий ответ: При первом обращении: вычислилось один раз — сохранилось, дальше отдаётся готовое значение. И нет, это не атомарно: два потока, одновременно читающие неинициализированный lazy var, могут запустить инициализацию дважды. В отличие от глобальных let и static let, у которых семантика dispatch_once.
Подробно:
final class ViewModel {
lazy var formatter: DateFormatter = {
let f = DateFormatter() // выполнится при первом обращении
f.dateFormat = "dd.MM.yyyy"
return f
}()
}
// static let shared = ViewModel() // а вот это лениво И потокобезопасно
- Зачем — отложить дорогую инициализацию (форматтеры, вычисления от других свойств); внутри замыкания уже доступен
self. - Всегда
var— значение записывается после init, поэтомуlazy letне существует. - Потоки — компилятор не ставит блокировку: гонка двух потоков может дать двойную инициализацию. Нужна ленивость + потокобезопасность — бери
static letили синхронизацию. - В структуре — первое чтение мутирует хранилище: нужен
var-экземпляр, а чтение изlet-структуры — ошибка компиляции.
⚠️ Частая ошибка: считать lazy var потокобезопасным «как синглтон». dispatch_once-семантика есть у глобальных и static констант, но не у lazy-свойств экземпляра.
12Property wrapper: что генерирует компилятор, и чем wrappedValue отличается от projectedValue?
middle
Короткий ответ: Для @Wrapper var x компилятор синтезирует скрытое хранилище private var _x: Wrapper, а обращения к x переписывает в _x.wrappedValue. projectedValue — второе, опциональное значение обёртки, доступное через $x: в SwiftUI $text у @State — это Binding.
Подробно:
@propertyWrapper
struct Clamped {
private var value: Int
let range: ClosedRange<Int>
var wrappedValue: Int {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
var projectedValue: Bool { value == range.upperBound } // это и есть $x
init(wrappedValue: Int, _ range: ClosedRange<Int>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct Player {
@Clamped(0...100) var health = 100
// компилятор сгенерировал: private var _health: Clamped
}
var p = Player()
p.health = 250
print(p.health, p.$health) // 100 true
- Зачем — переиспользуемая логика доступа: валидация,
UserDefaults, потокобезопасность — без копипасты в каждом свойстве. - Известные обёртки —
@Published($ — Publisher),@State/@Binding($ — Binding),@AppStorage.
⚠️ Частая ошибка: путать $x с _x. _x — само хранилище-обёртка (доступно внутри типа), $x — её projectedValue; если projectedValue не объявлен, синтаксиса $x просто нет.
Источники
Источники и редакционная политика
Ссылки на источники помогают изучить тему глубже. Если для ответа опубликована техническая проверка, рядом указаны её дата и область. Мы не связаны с упомянутыми работодателями и не продаём места в подборках. Источники, функции сервисов и формат найма могут меняться.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.