$ sudo teach IT

Зачем нужен ещё один тип, кроме структур

Когда разбирали структуры, вы видели, что каждая копия struct — это отдельная, независимая коробка со своим содержимым: скопировали значение — получили две коробки, которые дальше живут сами по себе. Но иногда это неудобно. Например, если один и тот же банковский счёт должны видеть сразу несколько частей программы, и изменение баланса в одном месте обязано быть заметно всюду, где на этот счёт есть ссылка. Для таких ситуаций в Swift есть тип со ссылочной семантикой — class.

Представьте квартиру и ключ от неё. Если отдать другу копию самой квартиры (структура), у него появится отдельное, независимое жильё — что бы он в нём ни поменял, вашей квартиры это не коснётся. А если отдать ключ от одной и той же квартиры (класс), оба будете заходить в одно и то же помещение: любые изменения увидят оба, потому что помещение одно, а ключей — просто несколько.

Класс и ссылочная семантика

Объявляется класс почти так же, как структура — только словом class вместо struct. Разница не в синтаксисе объявления, а в том, что происходит при копировании переменной.

class Counter {
    var value = 0
}

Обратите внимание: свойству value сразу задано значение по умолчанию. У классов, в отличие от структур, нет автоматического memberwise-инициализатора — Swift не станет сам генерировать init(value:) по списку свойств. Но если у всех хранимых свойств есть значение по умолчанию, класс всё равно получает пустой инициализатор без параметров, и создать объект можно так: Counter(). Писать собственные инициализаторы с параметрами — тема следующего урока, пока обойдёмся значениями по умолчанию.

Теперь ключевой момент — что происходит при присваивании:

let a = Counter()
let b = a
b.value = 99
print(a.value)

b — это не копия a, а второе имя для того же самого объекта. Строка let b = a скопировала не содержимое счётчика, а ссылку на него — «адрес», по которому лежит объект в памяти. Поэтому изменение через b видно и через a: программа напечатает 99. Если бы Counter был структурой, b получила бы независимую копию, и a.value так и осталась бы равна 0.

Идентичность: оператор ===

Раз переменные класса хранят не значение, а ссылку, возникает вопрос: как проверить, что две переменные указывают на один и тот же объект, а не просто на два разных объекта с одинаковым содержимым? Для этого есть оператор идентичности === (и его отрицание !==).

let first = Counter()
let second = Counter()
let alias = first

print(first === second) // false: разные объекты
print(first === alias)  // true: одно и то же значение value не делает объекты равными

first и second — это два разных объекта: у них может случайно совпасть значение value, но живут они в разных местах памяти, поэтому === вернёт false. А alias — это ещё одно имя для того же объекта, что и first, поэтому здесь === вернёт true. Обычный оператор == здесь использовать нельзя: сравнение по значению для собственных классов Swift не создаёт автоматически, об этом позже расскажут в теме про стандартные протоколы.

let фиксирует ссылку, а не содержимое объекта

Здесь чаще всего ошибаются новички: let у структуры делает неизменяемым весь набор значений целиком, а let у класса запрещает менять только саму ссылку — то есть нельзя присвоить переменной другой объект, но менять свойства объекта, на который она указывает, можно совершенно свободно.

let account = Counter()
account.value += 10   // разрешено: меняем свойство объекта
// account = Counter() — а вот так нельзя: нельзя переставить ссылку на другой объект

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

Частые ошибки

  • Ждать, что let у класса защитит объект от изменений так же, как это было бы у структуры. На самом деле защищена только сама ссылка, а не состояние объекта.
  • Сравнивать объекты классов оператором ==, рассчитывая на сравнение по значению — компилятор откажется собирать такой код, пока тип не научили сравниваться самостоятельно.
  • Путать === с ==: первый спрашивает «это буквально один и тот же объект в памяти», второй — «равны ли значения» (и его ещё нужно реализовать отдельно).
  • Забывать, что у класса без значений по умолчанию не получится вызвать Тип() без аргументов — в отличие от структуры, класс не получает memberwise-инициализатор автоматически.
  • Не замечать, что передача объекта класса в функцию не копирует его: функция может незаметно изменить состояние объекта, которым пользуется остальной код.

Резюме

  • class — тип со ссылочной семантикой: переменная хранит не значение, а ссылку на объект в памяти.
  • Присваивание объекта класса другой переменной создаёт alias — второе имя для того же объекта, а не независимую копию.
  • === и !== проверяют идентичность: это буквально один и тот же объект, а не просто совпадение значений внутри.
  • let у переменной класса запрещает менять саму ссылку, но никак не ограничивает изменение свойств объекта.
  • У классов нет автоматического memberwise-инициализатора: без значений по умолчанию у свойств понадобится свой init — об этом в следующем уроке.

Проверьте себя

4 вопроса

Общий счётчик

Ниже дан класс Counter с одним свойством value (по умолчанию равно 0). Реализуйте функцию bump, которая увеличивает value переданного счётчика на amount и возвращает новое значение.

Проверки создадут один объект Counter, вызовут вашу функцию несколько раз подряд, а затем передадут дополнительную переменную-псевдоним того же объекта — убедитесь, что изменения видны через любую из ссылок на этот объект.

Перевод между счетами

Дан класс Account со свойством balance (по умолчанию 0). Реализуйте функцию transfer(from:to:amount:): если на счёте source хватает средств, спишите amount с него и зачислите на destination, верните true. Если средств не хватает, не меняйте балансы и верните false.

В тестах оба счёта объявлены через let: это не помешает менять их свойства, потому что let у объекта класса фиксирует только саму ссылку, а не то, что лежит внутри объекта.