Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.
Вопросы и ответы
12 подробных ответов
01В чём реальные различия между struct и class в Swift?
junior
Короткий ответ: struct — значимый тип: при присваивании и передаче в функцию значение копируется. class — ссылочный: переменные разделяют один экземпляр. Наследование, deinit и идентичность (===) есть только у классов; мутация структуры требует var и mutating-методов.
Подробно:
struct |
class |
|
|---|---|---|
| Семантика | копия значения | общая ссылка |
| Наследование | ✗ (только протоколы) | ✓ |
deinit / === |
✗ | ✓ |
| Мутация | mutating + var |
всегда, даже через let |
| ARC | нет retain/release (если нет ссылочных полей) | есть |
| Инициализатор | memberwise бесплатно | пишешь сам |
- По умолчанию — struct — предсказуемое копирование, нет гонок на общем состоянии, дешевле для ARC.
- Класс — когда нужна общая идентичность (кэш, сервис), наследование от NSObject-мира или
deinitдля освобождения ресурса.
⚠️ Частая ошибка: «структуры всегда живут на стеке». Нет: структура — поле класса, захвачена escaping-замыканием или упакована в экзистенциал с большим payload — живёт в куче. Value type определяет семантику копирования, а не место в памяти.
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 — opaque type: за ним стоит ровно один конкретный тип, известный компилятору («обратные дженерики») — статический диспатч, без бокса. any P — экзистенциальный бокс: значение хранится с косвенностью через witness table, большой payload уезжает в кучу. <T: P> — дженерик, мономорфизируется под каждый вызов.
Подробно:
some P |
any P |
<T: P> |
|
|---|---|---|---|
| Конкретный тип | один, скрыт от вызывающего | любой, меняется в рантайме | один на инстанциацию |
| Диспатч | статический | witness table | статический |
| Память | без бокса | инлайн-буфер 3 слова, крупнее — куча | без бокса |
| Когда | возврат «какого-то» типа: some View |
гетерогенные коллекции, поля-плагины | горячий код, переиспользуемые алгоритмы |
some— выбирает тип реализация, а не вызывающий; тип стабилен между вызовами.any— честный runtime-полиморфизм:[any Shape]с кругами и квадратами вперемешку.- Дженерик — тип выбирает вызывающий; компилятор клонирует и оптимизирует код под каждый
T.
⚠️ Частая ошибка: писать any везде «для удобства». Это молчаливая цена: бокс, косвенный вызов, потеря ассоциированных типов. Правило: начинай с дженерика или some, any — когда действительно нужна гетерогенность.
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 сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.