$ sudo teach IT

Зачем вообще выбирать

Вы уже умеете объявлять и 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, «копирование при записи»: до первой попытки что-то поменять вы платите за копию буквально ничего, а платите только тогда, когда действительно нужны две разные версии данных.

Сравнение

Признакstructclass
Что копируетсяЗначение (независимая копия)Ссылка на объект
После присваиванияДве независимые переменныеДве переменные на один объект
Можно ли проверить «это один и тот же объект»Вопрос не имеет смысла — копии равноправныДа, через оператор идентичности
НаследованиеНе поддерживаетПоддерживает
Типичное применениеКоординаты, деньги, настройки, элементы коллекцийОбщее состояние, менеджеры, объекты интерфейса

Практическое правило выбора

По умолчанию берите 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.

Сумма может быть и отрицательной — это означает списание.