Connect with us

Программирование

Понимаем 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 только тогда, когда ваша структура данных требует рекурсии.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: