Site icon AppTractor

Понимаем indirect в Swift

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

Ключевое слово 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 на уровне ветки или всего 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)
    }
}

Это рекурсивная функция, которая работает с рекурсивной структурой данных:

Результат

let result = evaluate(expr)
print(result) // 20

Это чистый, выразительный и типобезопасный подход.

Нет синтаксического анализа строк. Нет неоднозначности во время выполнения.

Почему indirect важен

1. Позволяет создавать рекурсивные модели

Без indirect невозможно моделировать:

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. Сложнее отлаживать

Графы памяти становятся менее очевидными.

Но на практике

Если ты строишь:

лучшей альтернативы просто нет.

Частые ошибки

❌ Забыть 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 — это не функция для начинающих. Это инструмент моделирования. Большинству разработчиков он не нужен, потому что большинство разработчиков не моделируют сложные структуры данных.

Но если вы создаете:

Тогда indirect становится необходимым.

Ключ прост:

Используйте indirect только тогда, когда ваша структура данных требует рекурсии.

Источник

Exit mobile version