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

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

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

12 мин чтения12 подробных ответовОбновлено
Главная мысль

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

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

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

Проверка по первоисточникам опубликована для 2 из 12 ответов. Дата и область проверки указаны у каждого проверенного ответа.

01

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

Короткий ответ: struct — значимый тип, class — ссылочный тип с идентичностью. Обычная копируемая структура имеет семантику копирования; присваивание ссылки на класс разделяет экземпляр. Наследование и === относятся к классам. С Swift 5.9 существуют некопируемые структуры ~Copyable: они могут иметь deinit, но не допускают обычного копирования.

Подробно:

struct class
Семантика значение; обычно Copyable общая ссылка
Наследование нет, но есть протоколы есть
Идентичность === нет есть
deinit у некопируемых структур есть
Мутация для изменения хранимых полей нужны var и mutating-методы свойства var можно менять и через ссылку let
ARC может применяться к ссылочным полям управляет временем жизни экземпляра
Инициализатор memberwise при выполнении условий синтеза автоматический default/inherited — лишь при определённых условиях
  1. Структура удобна для независимых значений. Копирование может быть оптимизировано, например через copy-on-write; ссылочные поля всё равно могут разделять изменяемое состояние.
  2. Класс нужен, когда важна общая идентичность или наследование. Безопасность потоков не следует автоматически ни из struct, ни из class.
  3. Некопируемый ресурс можно моделировать struct Resource: ~Copyable { deinit {} }: владение передают без копирования.

⚠️ Частая ошибка: «структуры всегда живут на стеке». Семантика значения не определяет размещение в памяти: структура может находиться внутри объекта в куче или быть упакована в экзистенциал.

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 скрывает один конкретный тип, выбранный реализацией; <T: P> принимает тип, выбранный вызывающим. any P хранит значение одного из подходящих типов через экзистенциал. Это различия модели типов, а не безусловные обещания статического диспатча или аллокаций.

Подробно:

Форма Кто выбирает конкретный тип Типичное применение
Результат some P реализация; фиксирован для конкретной подстановки generic-параметров скрыть конкретный результат, сохранив его типовую идентичность
Параметр some P вызывающий; сокращение generic-параметра простой generic-аргумент
<T: P> вызывающий связать типы аргументов/результата, выразить ограничения
any P конкретное значение может меняться во время выполнения хранить разные реализации за одним интерфейсом
  1. Дженерики — Swift поддерживает неспециализированный код с metadata и witness tables. Если тело и конкретный тип доступны оптимизатору, он может специализировать и встроить вызовы; копия кода на каждый call site не гарантируется.
  2. Opaque result — some P сохраняет конкретную типовую идентичность, хотя вызывающий не знает её имени. Это создаёт возможности оптимизации, но на границе модуля вызов не обязан стать прямым.
  3. Экзистенциал — хранит значение и сведения о типе/соответствии. Небольшое значение часто помещается в inline-буфер, крупное может требовать heap box; layout зависит от ограничений протокола и ABI. Оптимизатор иногда устраняет косвенность.
  4. Associated types — у any P часть информации стирается, но современные constrained existentials могут сохранять явно заданные ограничения. Не сводите различие к «associated types всегда теряются».

⚠️ Частая ошибка: обещать, что some/generics всегда статические и без аллокаций, а any всегда выделяет память. Выбирайте по API и сохранению связей типов; измеряйте горячий код в release-сборке.

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 возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

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

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

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

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

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

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