Зачем нужны 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 так, чтобы он возвращал все переданные ему строки в том же порядке, в котором они перечислены в блоке.