Зачем вообще выбирать
Вы уже умеете объявлять и struct, и class — у обоих есть свойства и автоматически появляющийся способ создать экземпляр. Соблазн решить, что это просто два синонима с разным ключевым словом. На практике они ведут себя по-разному в одной конкретной ситуации: когда значение копируют и передают дальше. От этого выбора зависит, будет ли у вас в программе один общий объект, который видят все, или у каждого — своя независимая копия. Ошибиться легко: код скомпилируется в любом случае, а поведение окажется не тем, что вы ожидали.
Официальные термины
struct — это value type, тип значения. class — это reference type, ссылочный тип. Когда вы копируете значение типа значения (присваиваете переменной, передаёте в функцию, кладёте в массив), вы получаете независимую копию. Когда вы копируете значение ссылочного типа, вы получаете вторую переменную, указывающую на тот же самый объект в памяти — измените его через одну переменную, увидите изменение через другую.
Копия остаётся копией
Посмотрите на структуру с одним свойством:
struct Point {
var x: Int
}
var a = Point(x: 1)
var b = a
b.x = 5
print(a.x, b.x)
Программа напечатает 1 5. Строка var b = a создаёт независимую копию значения a. Дальше b.x = 5 меняет только копию — a остаётся прежней. Это и есть value-семантика: присваивание отрезает копию от оригинала, и дальше они живут отдельно, как будто вы отксерокопировали лист бумаги.
Ссылка ведёт к одному и тому же объекту
Теперь то же самое, но с классом:
class Box {
var value = 1
}
let box1 = Box()
box1.value = 10
let box2 = box1
box2.value = 99
print(box1.value)
Здесь напечатается 99. Строка let box2 = box1 не создаёт новый объект — она просто копирует адрес: теперь box1 и box2 смотрят на один и тот же экземпляр Box. Изменение через box2 видно и через box1, потому что это буквально одна и та же коробка, а не две одинаковые. Обратите внимание: обе переменные объявлены через let — константным здесь остаётся сам адрес (нельзя переприсвоить box1 = Box()), а вот свойства объекта, на который он указывает, менять можно свободно.
Copy-on-write: копия почти даром
Если бы каждое присваивание массива честно копировало все элементы, работа с большими коллекциями была бы медленной. На практике массивы, множества и словари в Swift — тоже структуры, но копирование выглядит подозрительно дешёвым:
var numbersA = [1, 2, 3]
var numbersB = numbersA
numbersB.append(4)
print(numbersA)
print(numbersB)
Вывод: [1, 2, 3] и [1, 2, 3, 4] — то есть оригинал не пострадал, как и положено значению. Но физического копирования элементов в строке var numbersB = numbersA не происходит: обе переменные сначала указывают на один и тот же внутренний буфер данных. Копия создаётся только в момент первого изменения — как раз на строке numbersB.append(4), и только для той переменной, которую меняют. Это называется copy-on-write, «копирование при записи»: до первой попытки что-то поменять вы платите за копию буквально ничего, а платите только тогда, когда действительно нужны две разные версии данных.
Сравнение
| Признак | struct | class |
|---|---|---|
| Что копируется | Значение (независимая копия) | Ссылка на объект |
| После присваивания | Две независимые переменные | Две переменные на один объект |
| Можно ли проверить «это один и тот же объект» | Вопрос не имеет смысла — копии равноправны | Да, через оператор идентичности |
| Наследование | Не поддерживает | Поддерживает |
| Типичное применение | Координаты, деньги, настройки, элементы коллекций | Общее состояние, менеджеры, объекты интерфейса |
Практическое правило выбора
По умолчанию берите struct. Она проще в рассуждении: если у вас есть значение, никто исподтишка не изменит его у вас за спиной, пока вы сами не присвоите новое. Переходите на class, когда вам ДЕЙСТВИТЕЛЬНО нужно, чтобы разные части программы работали с одним и тем же изменяемым объектом и видели чужие изменения — например, общий счётчик посещений или менеджер, на который ссылаются сразу несколько экранов приложения. По той же причине на классах строятся элементы пользовательского интерфейса в UIKit и AppKit: кнопка на экране — это один конкретный объект, а не независимая копия для каждого, кто на неё ссылается.
Частые ошибки
- Ждать, что структура «поделится» изменением с копией, потому что она когда-то была получена из той же переменной — структуры так не работают, у каждой копии своя жизнь.
- Заводить класс просто по привычке, хотя данные никогда не меняются после создания и никакого общего состояния не нужно — тогда более простая структура подходит лучше и меньше удивляет.
- Путать
letу переменной класса с неизменяемостью самого объекта:letзапрещает переприсвоить переменную, но не запрещает менятьvar-свойства объекта, на который она указывает. - Считать copy-on-write поводом не думать о производительности вообще — как только вы меняете один из двух совместно используемых массивов, копия всё же происходит, просто в удобный момент, а не заранее.
Резюме
struct— тип значения: присваивание создаёт независимую копию.class— тип ссылки: присваивание даёт вторую переменную на тот же объект.- Массивы, множества и словари — структуры с оптимизацией copy-on-write: копия физически происходит только при первом изменении.
- По умолчанию выбирайте
struct; переходите наclass, когда нужно намеренно общее изменяемое состояние. letу ссылки на класс фиксирует саму ссылку, а не содержимое объекта.
Проверьте себя
4 вопроса
Кошелёк, который не портится от копирования
Ниже уже объявлена структура кошелька — её менять не нужно:
struct Wallet {
var balance: Int
}
Реализуйте функцию deposit(_:amount:), которая принимает кошелёк и сумму пополнения и возвращает НОВЫЙ кошелёк с балансом, увеличенным на эту сумму. Кошелёк, переданный в параметр, должен остаться без изменений — не пытайтесь менять его свойства напрямую, просто создайте и верните новое значение Wallet.
Сумма может быть и отрицательной — это означает списание.