Зачем создавать свой тип
Представьте, что вы описываете книгу в коде: название, автора и количество страниц храните в трёх отдельных переменных — 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:), которая принимает кошелёк и сумму списания, а возвращает новый кошелёк с уменьшенным на эту сумму балансом. Кошелёк, переданный в функцию, изменяться не должен: функция обязана вернуть новое значение, а не менять исходное.