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 или общее изменяемое состояние.