JavaRush /Курсы /Swift SELF /Value semantics: struct

Value semantics: struct/ enum как “копируемое значение”

Swift SELF
35 уровень , 0 лекция
Открыта

1. Value semantics: основная идея

Если вы только начинаете, вам легко думать так: «переменная — это коробка, а внутри лежит штука». И это… удивительно хорошая модель, пока мы говорим про value‑типы. Value semantics — это про наблюдаемое поведение программы: когда вы присваиваете значение другой переменной или передаёте в функцию, у вас получается независимая копия “штуки”, и изменения одной коробки не должны магически менять содержимое другой.

Почему это важно в реальном коде? Потому что так проще рассуждать о состоянии. Особенно в CLI‑приложении вроде LibraryCLI, где мы постоянно «берём текущее состояние библиотеки → считаем новое состояние → печатаем → сохраняем/возвращаем». Value semantics помогает делать это предсказуемо: меньше сюрпризов, меньше “призраков в машине”.

Небольшая схема‑интуиция:

flowchart LR
    A["var a = Value(...)"] --> B["var b = a"]
    B --> C["b изменили"]
    A --> D["a не изменился"]

И это не философия. Это прям то, что вы увидите в print.

struct и enum как value‑типы

Сейчас мы сделаем главный «вау‑эксперимент дня»: присвоим одно значение другому и поменяем копию. У value‑типов это должно быть скучно и предсказуемо: поменяли копию — оригинал не трогали. И да, “скучно” здесь — комплимент: скучный код обычно надёжнее (а весёлый код обычно весело падает).

Начнём с максимально простого примера — счётчика:

import Foundation

struct Counter {
    var value: Int
}

var a = Counter(value: 0)
var b = a
b.value += 1

print(a.value) // 0
print(b.value) // 1

Здесь важна не математика, а поведение. b = a означает: «создай значение b, равное значению a». После этого b живёт своей жизнью.

То же самое работает и с enum. Часто кажется, что enum — это что-то “особое”, но для value semantics он обычный value‑тип.

import Foundation

enum Command {
    case add(title: String)
    case list
}

var c1 = Command.add(title: "Swift 6.2 для людей")
var c2 = c1

c2 = .list

print(c1) // add(title: "Swift 6.2 для людей")
print(c2) // list

С enum даже легче заметить копирование: мы не “подкручиваем поле”, мы целиком заменяем значение.

2. Мутации: let, var и mutating

Почему let “замораживает” значение целиком

На этом месте многие спотыкаются об очень человеческое ожидание: «ну у структуры же внутри var, почему я не могу поменять одно поле, если сама переменная — let?». Потому что для value‑типа let означает: вся коробка неизменяемая. Не “поле неизменяемое”, а именно всё значение.

То есть, если экземпляр struct лежит в let, вы не можете менять его stored properties и вы не можете вызывать mutating‑методы. Swift заставляет вас быть честными: либо значение меняется (тогда var), либо нет (тогда let). И это очень полезная строгость: она экономит часы отладки.

Пример:

import Foundation

struct BookDraft {
    var title: String
}

let draft = BookDraft(title: "Черновик")
// draft.title = "Не черновик" // ❌ ошибка компиляции

print(draft.title) // Черновик

Зачем нужен mutating

А теперь добавим mutating‑метод. Ключевое слово mutating — это как табличка «Осторожно, меняю себя!». Если метод меняет self (а значит меняет value целиком), Swift требует написать это явно.

import Foundation

struct Counter {
    var value: Int

    mutating func increment() {
        value += 1
    }
}

var c = Counter(value: 10)
c.increment()
print(c.value) // 11

Если вы попробуете сделать let c = Counter(...) и вызвать increment(), компилятор справедливо скажет: «Вы обещали не менять значение».

Важно понимать одну тонкость. Value semantics — это не «про поля», а про значение целиком. Поэтому mutating меняет весь self, даже если вы поменяли только value += 1. Это всё равно считается изменением значения.

Небольшая полезная таблица для запоминания:

Ситуация struct в let struct в var
Изменить stored property нельзя можно
Вызвать mutating метод нельзя можно
Присвоить в новую переменную можно (копия) можно (копия)

3. Функции: где проходят границы изменений

Почему изменения “не выходят наружу”

Дальше начинается магия, которая не магия, а просто контракт функций. Новички часто думают: «я передал переменную в функцию — значит функция может её поменять». В Swift по умолчанию не может. Параметры функции (без inout) — это локальные значения, и любые изменения внутри функции остаются внутри.

Это снова value semantics в действии: вы дали функции копию значения (по смыслу), и функция работает с ней.

Пример на Int (чтобы вообще не отвлекаться):

import Foundation

func bump(_ x: Int) {
    var x = x
    x += 1
    print("inside:", x) // inside: 11
}

let n = 10
bump(n)
print("outside:", n) // outside: 10

Даже если вы внутри функции создаёте var x = x, вы меняете локальную переменную.

Теперь сделаем то же самое на нашей модели библиотеки, потому что Int слишком “идеальный”, и кажется, что это только про числа.

import Foundation

struct LibraryState {
    var titles: [String]

    mutating func add(_ title: String) {
        titles.append(title)
    }
}

func addInsideFunction(_ state: LibraryState) {
    var copy = state
    copy.add("Книга из функции")
    print(copy.titles.count) // например, 2
}

var state = LibraryState(titles: ["Первая"])
addInsideFunction(state)
print(state.titles.count) // 1

Снаружи state не поменялся. Потому что функция не получала права менять ваш аргумент.

Если вы хотите, чтобы функция меняла аргумент, вы используете inout. Но это уже отдельная тема дня (эксклюзивный доступ и почему inout не разрешает «одновременно везде менять всё сразу»). Сегодня нам важно увидеть границу: обычные параметры — это локальные значения.

4. Value semantics в LibraryCLI: состояние как значение

Теперь давайте приземлим всё это на наш учебный проект. Представим, что у нас есть простейшее состояние приложения: список книг. Мы хотим уметь брать состояние, делать новый вариант состояния и выбирать, какой дальше использовать. Value semantics идеально подходит под такой стиль: “данные = значение”.

Начнём с модели Book и Library:

import Foundation

struct Book {
    let id: Int
    var title: String
}

struct Library {
    var books: [Book]

    mutating func addBook(title: String) {
        let nextId = (books.last?.id ?? 0) + 1
        books.append(Book(id: nextId, title: title))
    }
}

Теперь ключевой эксперимент: мы “сохранили снимок” состояния, потом изменили текущее.

import Foundation

var lib = Library(books: [])
lib.addBook(title: "Swift для начинающих")

let snapshot = lib  // снимок (копия значения по смыслу)
lib.addBook(title: "Swift для продолжающих")

print(snapshot.books.count) // 1
print(lib.books.count)      // 2

Это очень мощная идея. Вы можете безопасно хранить “снимки” состояния, передавать их в функции, сравнивать, логировать, откатывать — и всё это без ощущения, что где-то внутри живёт «общий объект, который все тайно крутят».

В CLI‑приложении это особенно приятно: вы парсите команду, создаёте новое состояние, печатаете результат. Когда вы мыслите value‑типами, вы прямо чувствуете, где у вас “до” и где “после”.

Можно даже представить это как конвейер:

flowchart TD
    A["Текущее Library"] --> B["Команда пользователя"]
    B --> C["Новая Library (копия + изменения)"]
    C --> D["Печать/сохранение"]

Именно так часто и проектируют приложения, где состояние важно держать аккуратно.

5. Поведение и производительность value‑типов

Value semantics — это про поведение, а не про “копировать память всегда”

Сейчас будет момент, где я аккуратно сниму с вас розовые очки и тут же выдам взамен нормальные рабочие. Value semantics означает: «по поведению это копия». Но это не обещание, что компьютер каждый раз физически копирует все байты в памяти немедленно. Я специально говорю об этом сейчас, чтобы вы не попали в ловушку «value‑типы всегда медленные».

Swift умеет оптимизировать value‑типы так, чтобы они вели себя как копии, но при этом не копировали лишнее. В стандартной библиотеке большинство базовых типов — value‑семантические (включая коллекции), и язык построен вокруг этого подхода.

Но здесь есть тонкая опасность: если вы сделаете struct, который внутри хранит ссылочный изменяемый объект, вы можете случайно сломать эту предсказуемость. И тогда у вас получится «struct, который ведёт себя как class», что обычно заканчивается тем, что вы разговариваете с компилятором на повышенных тонах.

В Swift‑экосистеме это известная проблема: value‑тип, который внутри держит изменяемый reference‑тип, может неожиданно “протекать” изменениями, если не реализовать правильную стратегию копирования (часто через Copy-on-Write).

Ещё один похожий источник неожиданной стоимости — property observers. Например, didSet может заставлять систему извлекать oldValue, и если внутри большой массив, это потенциально создаёт лишнюю работу. В Swift это даже отдельно уточняли и улучшали на уровне семантики, чтобы избежать ненужных копий там, где oldValue не используется.

Сегодня мы не будем углубляться в оптимизации (это отдельная лекция дня), но мораль простая: value semantics — ваш главный “модельный контракт”, а оптимизации — приятный бонус.

Как “увидеть” границу копии в коде

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

У value‑типов это обычно выглядит так:

  1. Создали значение (инициализация, литерал, возврат функции).
  2. Скопировали значение (присваивание, передача в функцию).
  3. Изменили значение (только если переменная var и только через mutating‑операции).

Вот короткий пример, где все три пункта рядом:

import Foundation

struct Year {
    var value: Int

    mutating func addOne() {
        value += 1
    }
}

var y1 = Year(value: 2026) // 1) создали
var y2 = y1                // 2) скопировали (по смыслу)
y2.addOne()                // 3) изменили копию

print(y1.value) // 2026
print(y2.value) // 2027

Когда вы научитесь “видеть” эти шаги глазами, многие вещи перестанут быть загадкой.

6. Типичные ошибки при работе с value semantics

Ошибка №1: ожидать, что после b = a изменения “протекут” из b в a.
Это частая привычка после языков, где всё вокруг “объекты”. В Swift для struct/enum такое ожидание неверно: b — это независимое значение. Если вы хотите “общую сущность”, это уже другой тип семантики (и другая лекция дня). На практике помогает простое правило: увидели struct — по умолчанию думайте «копия по смыслу».

Ошибка №2: объявить let, а потом пытаться менять поля и ругаться на компилятор.
Компилятор здесь не вредничает, а спасает. let для value‑типа означает «всё значение неизменяемо», даже если внутри поля объявлены как var. Если вам нужна мутация, делайте переменную var. А если мутация нужна только “иногда”, это хороший повод задуматься, где именно должно жить изменяемое состояние.

Ошибка №3: забыть mutating у метода структуры и долго искать, “почему нельзя менять поле”.
Если метод меняет stored properties, он обязан быть mutating. Это не бюрократия: это часть контракта value‑типа. Вы как автор типа прямо говорите вызывающему коду: «этот метод меняет значение». И дальше let‑экземпляры защищены от случайных изменений.

Ошибка №4: думать, что функция “обязана” менять аргумент, раз вы его туда передали.
Без inout аргументы функции — это локальные значения. Изменения внутри функции не обязаны отражаться снаружи, и чаще всего не отражаются. Если вы хотите изменить внешний аргумент, это должно быть явно: отдельный дизайн, отдельная сигнатура, отдельный вызов (в Swift это делается очень заметно).

Ошибка №5: случайно сломать value semantics, спрятав внутрь struct ссылочный изменяемый объект.
Это уже более продвинутая ловушка, но о ней важно знать заранее. Value‑тип должен вести себя как значение. Если внутри сидит изменяемый reference‑тип и вы не контролируете копирование, вы можете получить поведение, где “копия” меняется вместе с оригиналом — и это как раз тот случай, когда let снаружи не спасает от внутренней мутации. В Swift‑экосистеме это давно известный класс проблем, и для него обычно применяют подход Copy-on-Write.

1
Задача
Swift SELF, 35 уровень, 0 лекция
Недоступна
Счётчик квеста
Счётчик квеста
1
Задача
Swift SELF, 35 уровень, 0 лекция
Недоступна
Команда помощника
Команда помощника
1
Задача
Swift SELF, 35 уровень, 0 лекция
Недоступна
Замороженный профиль
Замороженный профиль
1
Задача
Swift SELF, 35 уровень, 0 лекция
Недоступна
Заметки без утечек
Заметки без утечек
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ