Зачем нужен ещё один тип, кроме структур
Когда разбирали структуры, вы видели, что каждая копия 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 у объекта класса фиксирует только саму ссылку, а не то, что лежит внутри объекта.