$ sudo teach IT

Зачем создавать свой тип

Представьте, что вы описываете книгу в коде: название, автора и количество страниц храните в трёх отдельных переменных — title, author, pages. Пока книга одна, это работает. Но как только книг становится десять, а функция printCard должна принимать все три значения по отдельности, начинаются проблемы: порядок параметров легко перепутать, а компилятор не подскажет, что вы случайно передали название туда, где ждали автора — обе переменные ведь строки.

Кортеж, который вы уже умеете использовать, решает часть проблемы — упаковывает значения вместе. Но у кортежа нет собственного имени типа: если функция принимает (String, String, Int), из сигнатуры не видно, что это именно книга, а не что-то другое с теми же типами полей. К тому же кортеж неудобно передавать между разными частями программы как устойчивое понятие — у него нет имени, на которое можно сослаться в другой функции или структуре.

Структура решает обе проблемы: даёт связанным данным одно общее имя типа и превращает набор разрозненных переменных в единое понятие, с которым дальше работает вся программа.

Структура: свойства и создание экземпляра

В официальной терминологии Swift такой тип называется struct (structure), а переменные внутри него — свойствами (properties). Объявление выглядит так:

struct Book {
    var title: String
    var author: String
    var pages: Int
}

Здесь Book — новый тип, а title, author, pages — его свойства. Само по себе объявление ничего не создаёт: оно лишь описывает форму будущих значений, как чертёж комнаты не является самой комнатой.

Чтобы получить конкретную книгу, нужно создать экземпляр (instance) — конкретное значение этого типа:

let book = Book(title: "Мартин Иден", author: "Джек Лондон", pages: 352)
print(book.title)
print(book.pages)

Обратиться к свойству экземпляра можно через точку: book.title. Такой синтаксис вы уже видели у строк и массивов — text.count, array.first устроены точно так же, просто определены Apple, а не вами.

Автоматический memberwise-инициализатор

Обратите внимание: в примере выше нигде не объявлен способ создания Book(...) — он появился сам. Swift автоматически генерирует так называемый memberwise-инициализатор: по одному параметру на каждое свойство, с меткой, совпадающей с именем свойства, в том порядке, в котором свойства объявлены в структуре.

struct Point {
    var x: Double
    var y: Double
}

let origin = Point(x: 0, y: 0)
let target = Point(x: 3, y: 4)

Метки аргументов обязательны и должны идти в порядке объявления свойств — так же, как у обычных функций с метками, которые вы уже писали. Свой собственный инициализатор, который меняет это поведение, вы напишете в одном из следующих уроков; пока пользуйтесь только тем, что Swift даёт бесплатно.

Семантика значения: структура — это копия

Самое важное отличие структуры от того, с чем вы работали раньше: когда вы присваиваете экземпляр структуры другой переменной или передаёте его в функцию, Swift создаёт независимую копию значения, а не общую ссылку на одни и те же данные.

struct Counter {
    var value: Int
}

var first = Counter(value: 1)
var second = first
second.value = 100

print(first.value)
print(second.value)

Программа напечатает 1, а затем 100. Строка second = first не создала два имени для одного значения — она скопировала Counter целиком. Изменение second никак не задевает first. Это называется семантикой значения (value semantics), и вы уже встречали такое поведение у чисел, строк и массивов (все они в Swift тоже реализованы через структуры) и у кортежей, которые устроены похожим образом. Теперь вы знаете, откуда берётся это поведение.

Изменение свойств: значение живёт в переменной, а не в типе

Возможность менять свойства зависит не от структуры, а от того, в var или в let лежит экземпляр:

var mutableBook = Book(title: "Черновик", author: "Аноним", pages: 10)
mutableBook.pages = 120

let fixedBook = Book(title: "Издано", author: "Аноним", pages: 200)
// fixedBook.pages = 210 — не скомпилируется

let у структуры делает неизменяемым весь экземпляр целиком, включая все его свойства, даже если сами свойства объявлены через var. Это логично: let означает «это значение не поменяется», а значение структуры — это все её поля сразу.

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

  • Пытаться изменить свойство у экземпляра, объявленного через let — компилятор сразу укажет на ошибку, это не баг, а следствие того, что let фиксирует весь экземпляр целиком.
  • Перепутать порядок аргументов в memberwise-инициализаторе — Book(author: ..., title: ...) в другом порядке не скомпилируется: метки обязаны стоять в том же порядке, в котором свойства объявлены в структуре.
  • Ожидать, что изменение копии повлияет на оригинал, как было бы со ссылочными типами в некоторых других языках. У структур это не так: каждая переменная хранит собственную, независимую копию значения.
  • Забыть метки вообще: Book("Название", "Автор", 100) без меток не компилируется — структура не похожа на позиционный кортеж, метки обязательны.

Резюме

  • Структура (struct) объединяет связанные свойства в один именованный тип вместо набора отдельных переменных или безымянного кортежа.
  • Swift сам генерирует memberwise-инициализатор: по параметру на каждое свойство, с метками в порядке объявления.
  • Экземпляры структур копируются при присваивании и передаче в функцию — это семантика значения, изменение копии не затрагивает оригинал.
  • Изменяемость экземпляра определяет var или let у переменной, которая его хранит, а не сама структура.

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

4 вопроса

Точка и манхэттенское расстояние

Даны координаты точки на плоскости. Допишите структуру Point со свойствами x и y типа Double — она уже объявлена в заготовке, трогать её не нужно. Реализуйте функцию manhattanDistance(_:), которая принимает значение типа Point и возвращает манхэттенское расстояние от этой точки до начала координат — сумму модулей координат: abs(x) + abs(y).

Кошелёк: списание без изменения оригинала

Допишите структуру Wallet со свойством balance типа Int — она уже объявлена в заготовке. Реализуйте функцию spend(_:amount:), которая принимает кошелёк и сумму списания, а возвращает новый кошелёк с уменьшенным на эту сумму балансом. Кошелёк, переданный в функцию, изменяться не должен: функция обязана вернуть новое значение, а не менять исходное.