Перейти к содержанию
Мобильная разработка

12 вопросов по теме «iOS: Swift и типы» на собеседовании

В этом материале — 12 вопросов из русской колоды RecallDeck по теме «iOS: Swift и типы». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

12 мин чтения12 подробных ответовПроверено 24 августа 2026
Главная мысль

Привязывайте платформенное решение к lifecycle, состоянию UI, обратной связи, доступности и цене поддержки для команды.

Вопросы и ответы

12 подробных ответов

01

В чём реальные различия между struct и class в Swift?

Короткий ответ: struct — значимый тип: при присваивании и передаче в функцию значение копируется. class — ссылочный: переменные разделяют один экземпляр. Наследование, deinit и идентичность (===) есть только у классов; мутация структуры требует var и mutating-методов.

Подробно:

struct class
Семантика копия значения общая ссылка
Наследование ✗ (только протоколы)
deinit / ===
Мутация mutating + var всегда, даже через let
ARC нет retain/release (если нет ссылочных полей) есть
Инициализатор memberwise бесплатно пишешь сам
  1. По умолчанию — struct — предсказуемое копирование, нет гонок на общем состоянии, дешевле для ARC.
  2. Класс — когда нужна общая идентичность (кэш, сервис), наследование от NSObject-мира или deinit для освобождения ресурса.

⚠️ Частая ошибка: «структуры всегда живут на стеке». Нет: структура — поле класса, захвачена escaping-замыканием или упакована в экзистенциал с большим payload — живёт в куче. Value type определяет семантику копирования, а не место в памяти.

02

Что напечатает этот код: массив структур против массива экземпляров класса?

Короткий ответ: Присваивание массива структур даёт независимую копию — мутация копии оригинал не трогает. Присваивание массива классов копирует только ссылки — объекты общие, мутация видна через обе переменные.

Подробно:

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 общий
  1. Массив — тоже структураb = a семантически копия; физически буфер общий до первой мутации (CoW), но наблюдаемое поведение — полная копия.
  2. Элементы-классы — копируются указатели, d[0] и c[0] — один и тот же объект в куче.
  3. let d не защищаетlet фиксирует ссылку, а не состояние объекта: поля класса мутировать можно.

⚠️ Частая ошибка: ответить «99 и 99», забыв, что value-семантика массива структур распространяется и на элементы: b[0].x = 99 мутирует элемент внутри копии, а не общий объект.

03

Как устроен Copy-on-Write в коллекциях Swift, и получит ли его твоя структура автоматически?

Короткий ответ: CoW — оптимизация value-семантики: при присваивании копируется только ссылка на общий буфер, а реальная копия делается лишь при мутации, если буфер разделяется (isKnownUniquelyReferenced). Array/String/Dictionary реализуют это вручную. Твоя структура CoW автоматически не получает.

Подробно:

  1. Присваивание — O(1): обе переменные указывают на один heap-буфер, счётчик ссылок = 2.
  2. Мутация — проверка уникальности; если буфер общий, сначала копия, потом запись.
  3. Своя структура — если она держит ссылку на 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 допустим?

Короткий ответ: Опционал — обычный дженерик-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

Лестница развёртки — от предпочтительного к крайнему:

  1. guard let — инвариант дальше по функции, ранний выход.
  2. if let / ?. / ?? — локальная работа со значением и дефолты.
  3. ! — только для гарантированных программистом инвариантов: IBOutlet после загрузки view, ресурс в бандле, фикстура в тестах. Падение здесь — сигнал сборки, а не пользовательский сценарий.

⚠️ Частая ошибка: «! нельзя никогда». Зрелый ответ — про инварианты: URL(string: "https://api.example.com")! с константным литералом честнее, чем молчаливый ?? с бессмысленным дефолтом, маскирующий баг.

05

Почему значения протокола с associatedtype нельзя просто сложить в массив, и какие есть альтернативы?

Короткий ответ: Протокол с 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 известен компилятору
}
  1. any P — гетерогенная коллекция возможна, но результат all() теряет конкретный Entity и приходит косвенно, через witness table.
  2. Дженерик <R: Repository> — полный доступ к R.Entity, статический диспатч; но один вызов — один конкретный тип.
  3. Type erasure — обёртки вида AnyPublisher, AnySequence: прячут конкретный тип, сохраняя ассоциированный (AnyPublisher<Int, Never>).

⚠️ Частая ошибка: лечить ошибку компилятора навешиванием any не думая. Если коллекция гомогенна — дженерик и быстрее, и сохраняет типы.

06

some P, any P и дженерик <T: P>: что компилятор делает по-разному?

Короткий ответ: some P — opaque type: за ним стоит ровно один конкретный тип, известный компилятору («обратные дженерики») — статический диспатч, без бокса. any P — экзистенциальный бокс: значение хранится с косвенностью через witness table, большой payload уезжает в кучу. <T: P> — дженерик, мономорфизируется под каждый вызов.

Подробно:

some P any P <T: P>
Конкретный тип один, скрыт от вызывающего любой, меняется в рантайме один на инстанциацию
Диспатч статический witness table статический
Память без бокса инлайн-буфер 3 слова, крупнее — куча без бокса
Когда возврат «какого-то» типа: some View гетерогенные коллекции, поля-плагины горячий код, переиспользуемые алгоритмы
  1. some — выбирает тип реализация, а не вызывающий; тип стабилен между вызовами.
  2. any — честный runtime-полиморфизм: [any Shape] с кругами и квадратами вперемешку.
  3. Дженерик — тип выбирает вызывающий; компилятор клонирует и оптимизирует код под каждый T.

⚠️ Частая ошибка: писать any везде «для удобства». Это молчаливая цена: бокс, косвенный вызов, потеря ассоциированных типов. Правило: начинай с дженерика или some, any — когда действительно нужна гетерогенность.

07

Зачем существует @escaping и что он меняет для замыкания?

Короткий ответ: @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
    }
}
  1. Non-escaping (по умолчанию) — замыкание гарантированно вызовется до возврата функции: захваты можно держать на стеке, self пишется неявно.
  2. Escaping — время жизни неизвестно: контекст захвата аллоцируется в куче, а явный self. — сигнал «здесь возможен retain cycle, подумай про [weak self]».
  3. Где встречается — completion-хендлеры сетевых вызовов, хранение колбэков, DispatchQueue.async.

⚠️ Частая ошибка: говорить, что @escaping «делает замыкание асинхронным». Нет: это только о времени жизни. Escaping-замыкание может быть вызвано синхронно — или вообще никогда.

08

Как enum с associated values помогает смоделировать состояние экрана?

Короткий ответ: Каждое состояние — кейс, а его данные — 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)
}   // добавишь кейс — компилятор укажет все места
  1. Против bool-флаговisLoading + items: [Item]? + error: Error? дают 8 комбинаций, из которых валидны 3: «loading и одновременно error» приходится запрещать дисциплиной. Enum запрещает их типом.
  2. Данные привязаны к кейсу — до items не добраться, пока не доказал switch-ем, что состояние .loaded.
  3. indirect — для рекурсивных enum (дерево, JSON): indirect case node(Tree, Tree) — Swift упакует рекурсию через указатель.

⚠️ Частая ошибка: ставить default: в switch по своему enum. Он выключает главное преимущество — исчерпывающую проверку: новый кейс молча провалится в default, и компилятор промолчит.

09

Какие виды диспетчеризации методов есть в Swift, и что напечатает классический пример с protocol extension?

Короткий ответ: Три механизма: прямой (static) — адрес известен на компиляции; табличный — vtable у классов и witness table у протоколов; message dispatch — objc_msgSend для @objc dynamic. Знаменитая ловушка: метод из protocol extension, не объявленный требованием протокола, диспатчится статически — по типу переменной.

Подробно:

  1. Прямой — методы структур, final-методы, методы в extension. Самый быстрый, инлайнится.
  2. Vtable / witness table — обычные методы классов; требования протокола при вызове через any P.
  3. 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 членами типа?

Короткий ответ: Оба — члены типа, а не экземпляра. 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
  1. static — статический диспатч, работает во всех типах; хранимые свойства типа — только так (и они лениво и потокобезопасно инициализируются).
  2. class — участвует в динамическом диспатче по метатипу: type(of: obj).sound() уважает переопределение.
  3. Выбор — по умолчанию static; class — когда полиморфизм на уровне типа реально нужен (фабрики, reuseIdentifier).

⚠️ Частая ошибка: «static — для структур, class — для классов». Нет: static отлично живёт в классах и означает там final class; различие — в возможности override, а не в месте использования.

11

Когда инициализируется lazy-свойство, и потокобезопасно ли это?

Короткий ответ: При первом обращении: вычислилось один раз — сохранилось, дальше отдаётся готовое значение. И нет, это не атомарно: два потока, одновременно читающие неинициализированный 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()  // а вот это лениво И потокобезопасно
  1. Зачем — отложить дорогую инициализацию (форматтеры, вычисления от других свойств); внутри замыкания уже доступен self.
  2. Всегда var — значение записывается после init, поэтому lazy let не существует.
  3. Потоки — компилятор не ставит блокировку: гонка двух потоков может дать двойную инициализацию. Нужна ленивость + потокобезопасность — бери static let или синхронизацию.
  4. В структуре — первое чтение мутирует хранилище: нужен var-экземпляр, а чтение из let-структуры — ошибка компиляции.

⚠️ Частая ошибка: считать lazy var потокобезопасным «как синглтон». dispatch_once-семантика есть у глобальных и static констант, но не у lazy-свойств экземпляра.

12

Property wrapper: что генерирует компилятор, и чем wrappedValue отличается от projectedValue?

Короткий ответ: Для @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
  1. Зачем — переиспользуемая логика доступа: валидация, UserDefaults, потокобезопасность — без копипасты в каждом свойстве.
  2. Известные обёртки@Published ($ — Publisher), @State/@Binding ($ — Binding), @AppStorage.

⚠️ Частая ошибка: путать $x с _x. _x — само хранилище-обёртка (доступно внутри типа), $x — её projectedValue; если projectedValue не объявлен, синтаксиса $x просто нет.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Мобильная разработка8 мин

10 вопросов по теме «Android: Компоненты и жизненный цикл» на собеседовании

10 вопросов по теме «Android: Компоненты и жизненный цикл» с подробными ответами, примерами, компромиссами и частыми ошибками кандидатов на техническом интервью.

10 подробных ответов
Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS