$ sudo teach IT

Зачем нужны result builders

Представьте, что вы собираете список покупок: хочется просто перечислить пункты один за другим, каждый на своей строке, без запятых и без квадратных скобок массива — как будто пишете обычный текст, а не код. Обычная функция так не умеет: ей нужно явно передать готовый массив ["Хлеб", "Молоко"]. Result builder — это способ научить компилятор самому собирать значение (массив, строку, что угодно) из последовательности выражений внутри блока { }, как будто каждая строка блока — отдельный пункт списка, а не отдельная инструкция.

Именно так устроен код SwiftUI, который вы могли видеть в чужих примерах: внутри VStack элементы экрана просто перечислены друг за другом, без запятых и без return. Это не особая магия SwiftUI, а обычный механизм языка Swift, который вы сейчас разберёте на своём примере.

Как это называется в документации Apple

Тип, помеченный атрибутом @resultBuilder, называется result builder («сборщик результата»). Внутри него объявляется статический метод buildBlock, который принимает все выражения из блока кода как аргументы и возвращает из них одно готовое значение. Чтобы функция приняла блок в таком виде, параметр-замыкание нужно пометить именем сборщика: @ИмяСборщика _ items: () -> Тип. Тогда компилятор берёт каждую строку внутри переданного блока и передаёт их все одним вызовом в buildBlock.

Простой сборщик списка

@resultBuilder
struct ListBuilder {
    static func buildBlock(_ items: String...) -> [String] {
        items
    }
}

func shoppingList(@ListBuilder _ items: () -> [String]) -> [String] {
    items()
}

let list = shoppingList {
    "Хлеб"
    "Молоко"
    "Яйца"
}
print(list) // ["Хлеб", "Молоко", "Яйца"]

buildBlock объявлен с вариативным параметром String..., поэтому он умеет принять и одну строку, и пять — компилятор просто передаёт туда все строки-выражения из блока по порядку, а метод складывает их в массив. Атрибут @ListBuilder перед параметром items в shoppingList — это и есть подключение сборщика: без него параметр остался бы обычным замыканием, и три строки подряд внутри { } компилятор бы не понял.

Что происходит внутри

Компилятор не делает ничего сверхъестественного: он буквально переписывает тело блока в один вызов. Блок из примера выше превращается в нечто равносильное ListBuilder.buildBlock("Хлеб", "Молоко", "Яйца"). Поэтому у buildBlock должна быть сигнатура, способная принять ровно столько аргументов и того типа, сколько выражений в блоке. Вариативный параметр решает это для однотипных выражений в любом количестве; если бы вместо него был параметр без ..., сборщик работал бы только с блоками ровно из одного выражения.

Сборка строки вместо массива

@resultBuilder
struct LineBuilder {
    static func buildBlock(_ lines: String...) -> String {
        lines.joined(separator: "\n")
    }
}

func note(@LineBuilder _ lines: () -> String) -> String {
    lines()
}

print(note {
    "Первая строка"
    "Вторая строка"
})

Result builder не обязан возвращать массив — он возвращает то, что укажете в сигнатуре buildBlock. Здесь те же выражения-строки склеиваются в одну многострочную String через joined(separator:). Именно эта гибкость и используется в SwiftUI: там сборщик ViewBuilder из перечисленных элементов экрана собирает не массив и не строку, а описание интерфейса, но идея та же самая — берём выражения из блока и превращаем их в одно значение.

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

  • Забыть пометить параметр-замыкание атрибутом сборщика (@ListBuilder _ items: () -> [String]). Без этой пометки блок с несколькими строками подряд — это уже не одно выражение, и компилятор откажется собирать функцию: он не знает, что делать со второй и следующими строками.
  • Объявить buildBlock без static — методы сборщика вызываются на самом типе, а не на его экземпляре, экземпляр ListBuilder вообще никогда не создаётся.
  • Сделать параметр buildBlock невариативным, когда блок может содержать разное число выражений — тогда блоки с другим количеством строк просто не найдут подходящую перегрузку и код не соберётся с ошибкой про лишний или недостающий аргумент.
  • Путать @resultBuilder с чем-то специфичным для SwiftUI — это обычная возможность языка Swift, SwiftUI лишь один из потребителей этого механизма.

Резюме

  • Result builder — тип с атрибутом @resultBuilder и статическим методом buildBlock, который собирает одно значение из выражений внутри блока кода.
  • Параметр-замыкание подключается к сборщику атрибутом @ИмяСборщика перед его типом.
  • Каждая строка внутри блока превращается в отдельный аргумент вызова buildBlock.
  • Вариативный параметр в buildBlock позволяет принимать блоки с разным числом строк.
  • buildBlock может возвращать что угодно — массив, строку или собственный тип; на этом устроен декларативный синтаксис SwiftUI.

Проверьте себя

3 вопроса

Сборщик списка покупок

Перед вами result builder ListBuilder и функция shoppingList, которая должна собирать список покупок из блока строк — по одной строке-пункту на строку кода, без запятых и без квадратных скобок. Сейчас buildBlock отбрасывает все переданные строки и всегда возвращает пустой массив. Допишите метод buildBlock так, чтобы он возвращал все переданные ему строки в том же порядке, в котором они перечислены в блоке.