Перейти к содержимому

Лекция 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) // 2
print(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) // 50
print(secondReference.score) // 50
print(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_000
print(copy.count) // 1_000_001

Copy-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 = 2
secondDelivery.address.city = "Москва"
print(firstDelivery.orderNumber) // 1
print(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) // 500
print(withBonus.amount) // 600

inout разрешает функции изменить переданное значение на месте:

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. Вопросы для самопроверки

  1. Что копируется при присваивании структуры?
  2. Что копируется при присваивании экземпляра класса?
  3. Почему let по-разному влияет на структуру и класс?
  4. Зачем методу структуры нужен mutating?
  5. Что такое copy-on-write и меняет ли он внешнюю семантику Array?
  6. Чем == отличается от ===?
  7. Что произойдёт, если структура содержит свойство ссылочного типа?
  8. Когда для функции нужен параметр inout?
  9. Почему идентичность сущности является аргументом в пользу класса?
  10. Почему «структуры для маленьких данных, классы для больших» — слабое правило?

Итоги

  • Структуры имеют семантику значения, классы — ссылочную семантику.
  • let полностью фиксирует структуру, но фиксирует только ссылку на класс.
  • Изменяющие методы структур помечаются mutating.
  • Стандартные коллекции сохраняют семантику значения с помощью copy-on-write.
  • Выбор между struct и class должен отражать смысл модели, а не привычку.