Лекция 2. Структуры и классы: семантика значений и ссылок
Цели лекции
После занятия студент должен уметь:
- объяснить различие между
structиclassв Swift; - предсказывать результат копирования значимых и ссылочных типов;
- использовать
mutatingдля изменения структуры; - понимать влияние
letна структуры и классы; - различать равенство значений и тождественность объектов;
- выбирать подходящее представление для модели предметной области;
- сочетать структуры и классы в одной модели.
1. В Swift ООП не ограничивается классами
Во многих языках сущность с данными и методами автоматически становится классом. Swift предлагает два основных составных типа:
struct— структура, тип со семантикой значения;class— класс, тип со ссылочной семантикой.
Оба типа могут содержать свойства, методы, инициализаторы, вычисляемые свойства, реализовывать протоколы и иметь расширения. Главное различие состоит не в синтаксисе, а в ответе на вопрос: что происходит при присваивании и передаче в функцию?
struct Coordinates { var x: Double var y: Double}
final class Marker { var title: String
init(title: String) { self.title = title }}Структуры обычно моделируют самостоятельные значения. Классы — объекты с идентичностью, общим изменяемым состоянием или жизненным циклом.
2. Семантика значения
При присваивании структуры логически создаётся независимое значение.
struct Point { var x: Int var y: Int}
var first = Point(x: 2, y: 3)var second = first
second.x = 100
print(first.x) // 2print(second.x) // 100Изменение second не влияет на first. Для пользователя языка это выглядит как
копирование, даже если компилятор внутри применяет оптимизации.
Семантика значения полезна, когда:
- значение должно быть локальным и независимым;
- равенство определяется данными;
- объект не нуждается в наблюдаемой идентичности;
- безопаснее получить изменённую копию, чем разделять состояние.
Примеры стандартных значимых типов: Int, Double, Bool, String, Array,
Dictionary, Set, кортежи, перечисления и пользовательские структуры.
3. Ссылочная семантика
При присваивании экземпляра класса копируется ссылка, а не сам объект.
final class Player { var score: Int
init(score: Int) { self.score = score }}
let firstPlayer = Player(score: 10)let secondReference = firstPlayer
secondReference.score = 50
print(firstPlayer.score) // 50print(secondReference.score) // 50print(firstPlayer === secondReference) // trueОбе константы указывают на один экземпляр. Такое разделение состояния бывает необходимо, но увеличивает число связей: изменение из одной части программы становится видно в другой.
Класс оправдан, когда важны:
- уникальная идентичность;
- совместно используемое изменяемое состояние;
- наследование;
- управление временем жизни и
deinit; - взаимодействие с API, требующим ссылочного типа.
4. let и изменяемость
Для структуры let делает неизменяемым всё значение:
struct Size { var width: Double var height: Double}
let size = Size(width: 10, height: 20)// size.width = 30 // ошибкаДля класса let запрещает заменить ссылку, но не запрещает менять var-свойства
экземпляра:
final class WindowState { var isOpen = false}
let state = WindowState()state.isOpen = true // разрешено// state = WindowState() // ошибкаПоэтому выражение «объявлено через let» имеет разный практический эффект для
значимого и ссылочного типа.
5. Изменяющие методы структур
Метод структуры по умолчанию не может менять её свойства. Для изменения нужен
модификатор mutating.
struct StepCounter { private(set) var steps = 0
mutating func add(_ amount: Int) -> Bool { guard amount > 0 else { return false } steps += amount return true }
mutating func reset() { self = StepCounter() }}
var counter = StepCounter()counter.add(2_000)print(counter.steps)mutating явно показывает, что вызов способен изменить значение или заменить
self. У методов класса такой метки нет: класс изначально имеет ссылочную
семантику.
6. Автоматический инициализатор структуры
Если структура не объявляет собственный инициализатор, Swift создаёт memberwise initializer:
struct UserProfile { var name: String var age: Int}
let profile = UserProfile(name: "Амина", age: 20)У класса аналогичный инициализатор автоматически не появляется:
final class UserProfileClass { var name: String var age: Int
init(name: String, age: Int) { self.name = name self.age = age }}Если структура получила пользовательский init внутри основного объявления,
автоматический memberwise initializer перестаёт быть доступным. Инициализатор в
extension позволяет сохранить автоматически созданный вариант.
7. Равенство и идентичность
Для значимых типов обычно важно равенство данных. Структура может получить ==
автоматически, если все её свойства поддерживают Equatable:
struct EmailAddress: Equatable { let value: String}
let firstEmail = EmailAddress(value: "student@example.com")let secondEmail = EmailAddress(value: "student@example.com")print(firstEmail == secondEmail) // trueДля классов есть два разных вопроса:
a === b— один ли это экземпляр;a == b— равны ли объекты по правилам модели, если класс реализуетEquatable.
final class Customer: Equatable { let id: Int var name: String
init(id: Int, name: String) { self.id = id self.name = name }
static func == (lhs: Customer, rhs: Customer) -> Bool { lhs.id == rhs.id }}
let a = Customer(id: 1, name: "Лейла")let b = Customer(id: 1, name: "Лейла")print(a == b) // true: модель считает идентификаторы равнымиprint(a === b) // false: экземпляры разные8. Copy-on-write в стандартных коллекциях
Array, Dictionary и String являются значимыми типами, но немедленно
копировать большой буфер при каждом присваивании дорого. Swift применяет
copy-on-write: копии временно используют общее внутреннее хранилище, а реальное
копирование происходит перед первым изменением.
var original = Array(1...1_000_000)var copy = original // буфер может пока быть общимcopy.append(1_000_001) // перед изменением copy отделяется
print(original.count) // 1_000_000print(copy.count) // 1_000_001Copy-on-write — оптимизация реализации. Наблюдаемое поведение остаётся семантикой значения: изменение копии не меняет оригинал.
9. Структура может содержать класс
Копирование структуры не клонирует объекты, на которые ссылаются её свойства.
final class Address { var city: String
init(city: String) { self.city = city }}
struct Delivery { var orderNumber: Int var address: Address}
let sharedAddress = Address(city: "Нальчик")var firstDelivery = Delivery(orderNumber: 1, address: sharedAddress)var secondDelivery = firstDelivery
secondDelivery.orderNumber = 2secondDelivery.address.city = "Москва"
print(firstDelivery.orderNumber) // 1print(firstDelivery.address.city) // МоскваСама Delivery скопировалась, но обе копии содержат ссылку на один Address.
Это не ошибка языка, а свойство выбранной модели. Если адрес должен быть
независимым значением, его тоже следует сделать структурой.
10. Функции и параметры
Передача структуры в функцию сохраняет семантику значения. Обычный параметр нельзя изменить так, чтобы изменение стало видно вызывающему коду.
struct Wallet { var amount: Int}
func addingBonus(to wallet: Wallet) -> Wallet { var result = wallet result.amount += 100 return result}
let initial = Wallet(amount: 500)let withBonus = addingBonus(to: initial)print(initial.amount) // 500print(withBonus.amount) // 600inout разрешает функции изменить переданное значение на месте:
func addBonus(to wallet: inout Wallet) { wallet.amount += 100}
var wallet = Wallet(amount: 500)addBonus(to: &wallet)Для класса специальный inout не нужен, чтобы менять свойства самого объекта.
Он понадобился бы для замены ссылки, хранящейся в переменной вызывающего кода.
11. Как выбрать между struct и class
Начинайте со структуры, если сущность:
- является значением, а не уникальным объектом;
- полностью описывается своими данными;
- должна безопасно копироваться;
- не требует наследования или
deinit; - невелика либо использует copy-on-write.
Выбирайте класс, если:
- у сущности есть устойчивая идентичность;
- изменения должны быть видны через несколько ссылок;
- нужен контролируемый жизненный цикл;
- требуется наследование;
- ссылочную семантику требует внешний фреймворк.
Размер структуры сам по себе не является главным критерием. Важнее семантика: должно ли присваивание создавать независимое значение или ещё одну ссылку на тот же объект?
12. Пример смешанной модели
import Foundation
struct Money: Equatable { let amount: Decimal let currency: String}
struct Product: Equatable { let id: UUID let name: String let price: Money}
final class ShoppingCart { let id: UUID private(set) var products: [Product] = []
init(id: UUID = UUID()) { self.id = id }
func add(_ product: Product) { products.append(product) }}Money и Product — значения. Корзина — объект с идентичностью и изменяемым
содержимым. Такая модель использует сильные стороны обоих подходов.
13. Типичные ошибки
Выбор класса по привычке
Класс для каждой маленькой модели создаёт ненужное совместное состояние и усложняет рассуждение об изменениях.
Ожидание глубокого копирования
Копирование структуры копирует её значение, но ссылочные свойства по-прежнему могут указывать на общие объекты.
Использование === для структур
Тождественность применима только к экземплярам классов. Значения структур
сравнивают через == после реализации Equatable.
Большой изменяемый класс как «контейнер данных»
Если объект лишь переносит набор полей и не имеет идентичности, структура обычно яснее и безопаснее.
14. Вопросы для самопроверки
- Что копируется при присваивании структуры?
- Что копируется при присваивании экземпляра класса?
- Почему
letпо-разному влияет на структуру и класс? - Зачем методу структуры нужен
mutating? - Что такое copy-on-write и меняет ли он внешнюю семантику
Array? - Чем
==отличается от===? - Что произойдёт, если структура содержит свойство ссылочного типа?
- Когда для функции нужен параметр
inout? - Почему идентичность сущности является аргументом в пользу класса?
- Почему «структуры для маленьких данных, классы для больших» — слабое правило?
Итоги
- Структуры имеют семантику значения, классы — ссылочную семантику.
letполностью фиксирует структуру, но фиксирует только ссылку на класс.- Изменяющие методы структур помечаются
mutating. - Стандартные коллекции сохраняют семантику значения с помощью copy-on-write.
- Выбор между
structиclassдолжен отражать смысл модели, а не привычку.