Программирование
Понимаем indirect в Swift
На первый взгляд, оно кажется малопонятным. Вы редко встречаете его в повседневной разработке приложений. Но когда оно вам действительно нужно, ничто другое его не заменит.
Swift — это язык, который ценит ясность и безопасность. В большинстве случаев его возможности становятся очевидными, как только вы увидите их в действии.
Ключевое слово indirect не относится к таким возможностям. На первый взгляд, оно кажется малопонятным. Вы редко встречаете его в повседневной разработке приложений. Но когда оно вам действительно нужно, ничто другое его не заменит.
В этой статье рассматриваются следующие вопросы:
- Что такое indirect
- Зачем оно существует
- Где оно используется
- Как оно работает «под капотом»
- Когда его следует (и не следует) использовать
Что такое indirect?
Ключевое слово indirect в Swift используется с перечислениями, чтобы поддерживать рекурсивные структуры данных.
Оно сообщает компилятору:
Храни этот case косвенно (через ссылку), а не инлайн (не прямо в значении).
Это важно, когда перечисление ссылается само на себя, прямо или косвенно.
Это не просто ключевое слово.
Это обходной путь для решения очень реального ограничения типов значений.
Проблема, решаемая indirect
Давайте начнем с простой попытки:
enum Node {
case value(Int)
case branch(Node, Node)
}
Это не скомпилируется. Потому что перечисления в Swift — это типы значений, а типы значений требуют известного конечного размера во время компиляции.
Но здесь:
case branch(Node, Node)
Node содержит Node, который содержит Node, который содержит Node…
Это создает проблему бесконечного размера. Компилятор не может определить, сколько памяти выделить.
Вступает в игру indirect
Мы исправляем это, помечая рекурсивный случай как indirect:
enum Node {
case value(Int)
indirect case branch(Node, Node)
}
Теперь это компилируется. Что изменилось? Вариант branch больше не хранится непосредственно в памяти. Вместо этого Swift хранит его косвенно, обычно используя выделение памяти в куче и ссылку.
Это решает проблему бесконечной рекурсии.
Другими словами:
- Без
indirect→ всё работает как в стеке, встраивается в память, предсказуемо. - С
indirect→ вы вводите выделение памяти в куче и косвенный доступ.
Компромиссы:
✅ Поддержка бесконечных структур
❌ Ради абсолютной предсказуемости памяти
Эта жертва преднамеренна.
indirect на уровне ветки или всего enum
У вас есть два варианта:
Целевой (лучший вариант по умолчанию)
enum Node {
case value(Int)
indirect case branch(Node, Node)
}
Это точно. Вы платите только там, где это необходимо.
Глобальный (удобно, но лениво)
indirect enum Node {
case value(Int)
case branch(Node, Node)
}
Это работает, но часто это излишне.
Мнение
Если вы помечаете весь перечислимый тип как indirect, вы, вероятно, недостаточно тщательно продумываете свою модель данных.
Реальный пример: деревья выражений
Вот где indirect тип становится действительно полезным.
Давайте смоделируем математические выражения:
indirect enum Expression {
case number(Int)
case addition(Expression, Expression)
case multiplication(Expression, Expression)
}
Это не просто перечисление. Это дерево.
Создание реального выражения
let expr = Expression.multiplication(
.addition(.number(2), .number(3)),
.number(4)
)
Это означает:
(2 + 3) * 4
Вычисление выражения
Теперь давайте вычислим его:
func evaluate(_ expression: Expression) -> Int {
switch expression {
case .number(let value):
return value
case .addition(let left, let right):
return evaluate(left) + evaluate(right)
case .multiplication(let left, let right):
return evaluate(left) * evaluate(right)
}
}
Это рекурсивная функция, которая работает с рекурсивной структурой данных:
.number→ базовый кейс.addition→ вычисляем обе стороны, затем складываем.multiplication→ вычисляем обе стороны, затем умножаем
Результат
let result = evaluate(expr) print(result) // 20
Это чистый, выразительный и типобезопасный подход.
Нет синтаксического анализа строк. Нет неоднозначности во время выполнения.
Почему indirect важен
1. Позволяет создавать рекурсивные модели
Без indirect невозможно моделировать:
- Деревья
- Графы (с осторожностью)
- Вычислители выражений
- Абстрактные синтаксические деревья (AST)
2. Сохраняет семантику значений
Даже несмотря на то, что indirect использует ссылки, перечисление извне по-прежнему ведет себя как тип значения.
Это означает:
- Копирование по-прежнему работает как ожидается
- По умолчанию нет общего изменяемого состояния
- Безопаснее, чем альтернативы на основе классов
3. Избегает ручной упаковки
Без indirectвам понадобилось бы что-то вроде:
final class Box<T> {
let value: T
init(_ value: T) {
self.value = value
}
}
А затем:
enum Node {
case value(Int)
case branch(Box<Node>, Box<Node>)
}
Это шумный, менее выразительный вариант, который легко неправильно использовать. indirect полностью устраняет этот шаблонный код.
Под капотом
Когда вы помечаете case как indirect, Swift:
- Сохраняет связанное значение в куче
- Сохраняет ссылку в перечислении
Таким образом, вместо:
Enum содержит всю вложенную структуру инлайн
мы получаем:
Enum → указатель → реальные данные
Иными словами, значение больше не хранится целиком «внутри» enum, а лежит отдельно (в куче), а enum держит лишь ссылку на него.
Именно это разрывает рекурсию бесконечного размера.
Компромиссы
1. Выделение памяти в куче
Вы теряете гарантию, что всё хранится только в стеке.
2. Цена косвенности
Появляются дополнительные обращения по ссылке. Обычно это незначительно, но overhead всё же есть.
3. Сложнее отлаживать
Графы памяти становятся менее очевидными.
Но на практике
Если ты строишь:
- вычислители выражений;
- синтаксические деревья;
- вложенные UI/state-модели;
- файловые иерархии;
лучшей альтернативы просто нет.
Частые ошибки
❌ Забыть indirect в рекурсивном enum
enum Tree {
case leaf(Int)
case node(Tree, Tree) // ❌ compiler error
}
Исправление:
enum Tree {
case leaf(Int)
indirect case node(Tree, Tree)
}
❌ Чрезмерное использование indirect
Не каждому перечислению это необходимо.
enum Result {
case success(Int)
case failure(Error)
}
Добавление indirect здесь излишне и вредно.
❌ Cчитать, что появляется ссылочная семантика
Даже с indirect это всё ещё тип данных:
var a = expr var b = a
Изменение b не влияет на a.
Практический пример: файловая система
indirect enum FileSystemItem {
case file(name: String)
case folder(name: String, contents: [FileSystemItem])
}
let system = FileSystemItem.folder(
name: "root",
contents: [
.file(name: "README.md"),
.folder(
name: "src",
contents: [
.file(name: "main.swift")
]
)
]
)
Именно так следует моделировать иерархические данные.
Не с помощью словарей. Не с помощью массивов со слабой типизацией. Не с помощью классов по умолчанию.
Когда следует использовать indirect
Используйте его, когда:
- Ваши данные по своей природе рекурсивны
- Вам нужны древовидные структуры
- Вам нужна семантика значений со структурой
Когда не следует использовать indirect
Избегайте его, когда:
- Ваша модель плоская
- Вы не знаете
- Вы пытаетесь «защитить» код без необходимости
Заключительные мысли
indirect — это не функция для начинающих. Это инструмент моделирования. Большинству разработчиков он не нужен, потому что большинство разработчиков не моделируют сложные структуры данных.
Но если вы создаете:
- Компиляторы
- DSL
- Продвинутые системы состояний
- Или что-либо древовидное
Тогда indirect становится необходимым.
Ключ прост:
Используйте
indirectтолько тогда, когда ваша структура данных требует рекурсии.
-
Интегрированные среды разработки3 недели назадРасширение поддержки Swift в разных IDE
-
GitHub3 недели назадRoxum IDE — среда разработки для Android
-
Разработка4 недели назадБудущее Android-приложений с AppFunctions
-
Разработка4 недели назадЯ сократил время разработки Android вдвое с помощью ИИ — вот как это сделать
