Value type против reference type

Первый вопрос почти любого iOS-собеседования: зачем в Swift два способа объявить тип и какой брать по умолчанию.

Value type (struct, enum, кортеж) копируется при присваивании и передаче: у каждого владельца свой экземпляр. Reference type (class, actor, замыкание) передаёт ссылку: владельцев много, объект один.

Как звучит вопрос

«В чём разница между struct и class?» и следом: «Что вы выберете и почему?»

Что на самом деле проверяют

Не знание определений из документации. Интервьюер хочет понять, умеете ли вы предсказать поведение кода: кто увидит изменение, когда произойдёт копирование, где возможен цикл сильных ссылок и гонка данных. Junior, который отвечает «struct на стеке, class в куче», обычно валится на следующем же уточняющем вопросе.

Семантика значения на пальцах

struct Point {
    var x: Int
    var y: Int
}

var a = Point(x: 0, y: 0)
var b = a          // копия
b.x = 10
print(a.x, b.x)

final class Box {
    var value: Int
    init(value: Int) { self.value = value }
}

let one = Box(value: 0)
let two = one      // та же ссылка
two.value = 10
print(one.value, two.value)

Вывод:

0 10
10 10

Обратите внимание на let one: константа запрещает менять саму ссылку, но не запрещает менять объект по этой ссылке. С let у struct всё иначе — экземпляр целиком становится неизменяемым.

Стек и куча: без мифов

Заученная фраза «struct живёт на стеке» верна только для простых локальных случаев. Экземпляр struct физически лежит там, где лежит его хранилище: захваченный замыканием — в куче, лежащий полем класса — внутри объекта в куче, в массиве — в буфере массива в куче. Правильная формулировка на собеседовании: «размещение — деталь реализации компилятора, а гарантируется семантика: копия при присваивании».

Copy-on-write

Если бы Array, String и Dictionary копировали буфер на каждое присваивание, Swift был бы непригоден. Поэтому у них copy-on-write: буфер общий, пока никто не пишет.

var first = [1, 2, 3]
var second = first   // буфер общий, копирования нет
second.append(4)     // ссылок две -> буфер копируется здесь

Механизм строится на isKnownUniquelyReferenced — проверке, что на ссылочное хранилище указывает ровно один владелец. Свой COW-тип пишут так:

final class Storage {
    var bytes: [UInt8]
    init(bytes: [UInt8]) { self.bytes = bytes }
}

struct Buffer {
    private var storage: Storage

    init(_ bytes: [UInt8]) { storage = Storage(bytes: bytes) }

    mutating func append(_ byte: UInt8) {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(bytes: storage.bytes)   // копируем только при записи
        }
        storage.bytes.append(byte)
    }
}

mutating и наследование

У struct метод, меняющий свойства, помечается mutating: он на самом деле переприсваивает self, поэтому вызвать его на константе нельзя. У class такого ключевого слова нет — метод меняет объект по ссылке, и компилятору незачем что-то переприсваивать.

Наследования у struct тоже нет. Замена — протоколы с реализацией по умолчанию: композиция вместо иерархии, никакого хрупкого базового класса.

Identity против равенства

let left = Box(value: 1)
let right = Box(value: 1)
print(left === right)         // false: разные объекты

struct Money: Equatable {
    let amount: Decimal
}
print(Money(amount: 10) == Money(amount: 10))   // true

Оператор === работает только со ссылочными типами: у значения нет «личности», две одинаковые купюры по 10 — это просто 10 и 10. Для значений применяют Equatable, синтезируемый компилятором.

Ловушка про массивы

struct Task { var done: Bool }
var tasks = [Task(done: false)]
for var task in tasks { task.done = true }   // меняются копии
print(tasks[0].done)                          // false
tasks[0].done = true                          // а так — по-настоящему

final class RefTask { var done = false }
let shared = RefTask()
let listA = [shared]
let listB = [shared]
listA[0].done = true
print(listB[0].done)                          // true — объект один

Правило выбора

Гайдлайн Apple простой: по умолчанию берём struct. Class нужен, когда требуется хотя бы одно из: identity (важно «тот же самый объект», а не «равный»), наследование или Objective-C-интероп (UIViewController, NSObject), deinit для освобождения ресурса, общее изменяемое состояние, которое видят все владельцы.

Бонус значений — потокобезопасность: у каждого потока своя копия, гонки на ровном месте не возникает. С class приходится думать про синхронизацию, actor или очередь.

Типичные ошибки кандидатов

  • «struct всегда на стеке, class всегда в куче» — грубое упрощение, ломается на захвате в замыкании и на полях класса.
  • Считать, что let у class запрещает менять свойства объекта.
  • Утверждать, что копирование массива всегда дорогое, не зная про copy-on-write.
  • Пытаться сравнить два struct через ===.
  • Брать class «на всякий случай, вдруг понадобится наследование» вместо протокола.
  • Не назвать ни одного признака, по которому выбирают class — ответ звучит как заученное определение.

Как ответить кратко

struct — семантика значения: при присваивании получается копия, изменения видит только владелец, есть mutating-методы и нет наследования. class — семантика ссылки: объект один, изменения видят все, есть identity через ===, наследование и deinit. У Array, String и Dictionary копирование ленивое — copy-on-write через isKnownUniquelyReferenced. По умолчанию беру struct, потому что значения проще и безопаснее в многопоточности; class беру осознанно — когда нужна identity, наследование, deinit, интероп с Objective-C или общее изменяемое состояние.

Проверьте себя
1. Что напечатает код: let box = Box(value: 0); let alias = box; alias.value = 5; print(box.value) — где Box это final class?
A0, потому что box объявлен через let
B5, потому что box и alias ссылаются на один объект
CОшибка компиляции: нельзя менять свойство у let-константы
D0, потому что при присваивании объект копируется
2. Зачем в реализации copy-on-write нужен isKnownUniquelyReferenced?
AЧтобы проверить, что тип соответствует Equatable
BЧтобы узнать, лежит ли значение на стеке или в куче
CЧтобы копировать внутренний буфер только тогда, когда на него есть больше одной ссылки
DЧтобы атомарно увеличить счётчик ссылок перед записью
3. Какой из признаков НЕ является поводом выбрать class вместо struct?
AНужна identity: важно, что это тот же самый объект
BНужен deinit для освобождения ресурса
CНужно передать значение в несколько потоков без синхронизации
DНужен интероп с Objective-C, например наследник UIViewController