Зачем нужны замыкания
Когда вы сортировали массивы через sorted(by:), вы уже писали короткие блоки кода в фигурных скобках вроде { $0 > $1 } — просто не называли их отдельным термином. Это была не особая часть синтаксиса sorted, а самостоятельная возможность языка: кусок кода без имени, который можно передать как значение, сохранить в переменную или вызвать позже. Такой кусок кода называется замыканием.
Замыкания нужны там, где заводить отдельную именованную функцию было бы избыточно: разовое правило сравнения, маленькое преобразование числа, действие, которое передают внутрь другой функции и хотят описать прямо в месте вызова, а не искать глазами по файлу, что делает функция с каким-то отдельным именем.
Замыкание — это тоже блок кода
В документации Apple замыкание (closure) определяется как самодостаточный блок функциональности, который можно передавать по коду. Функция, объявленная через func, — это тоже замыкание, просто именованное. Замыкание в привычном смысле — версия без имени, записанная как выражение прямо там, где она нужна.
Полный синтаксис
В самом развёрнутом виде замыкание выглядит так:
let compare: (Int, Int) -> Bool = { (a: Int, b: Int) -> Bool in
return a < b
}
print(compare(3, 5))
Всё, что стоит до слова in, — список параметров и тип возврата, один в один как в объявлении функции. Слово in отделяет заголовок от тела: после него идёт обычный код, который выполняется при вызове. Тип переменной compare — (Int, Int) -> Bool, тот же тип функции, что встречался при передаче функций параметром.
Вывод типов сокращает запись
Раз тип переменной compare уже объявлен как (Int, Int) -> Bool, компилятор и так знает, что параметры — Int, а результат — Bool. Повторять эти типы внутри фигурных скобок необязательно:
let compare2: (Int, Int) -> Bool = { a, b in
a < b
}
Заодно пропало слово return: тело замыкания состоит из одного выражения, и Swift сам понимает, что его значение и есть результат. Это называется неявным возвратом — он работает только тогда, когда всё тело замыкания умещается в одно выражение. Как только строк с действиями становится больше одной, return снова обязателен.
Сокращённые имена $0, $1
Когда параметры замыкания используются один раз и по порядку, можно вообще не давать им имена — Swift позволяет обращаться к ним как $0, $1 и так далее, по позиции:
let compare3: (Int, Int) -> Bool = { $0 < $1 }
print(compare3(10, 4))
$0 — первый параметр, $1 — второй. Список параметров и in в этом случае исчезают целиком: раз вы используете $0, Swift сам выводит, сколько параметров нужно замыканию и как их зовут. Это самая короткая форма, но она удобна только для простых однострочных правил — если тело длиннее пары действий, именованные параметры читаются понятнее.
Замыкание как параметр функции
Замыкания чаще всего встречаются не сами по себе, а как параметр другой функции — так же, как раньше параметром можно было передать функцию, только теперь без предварительного объявления с именем:
func transform(_ numbers: [Int], using operation: (Int) -> Int) -> [Int] {
var result: [Int] = []
for number in numbers {
result.append(operation(number))
}
return result
}
let doubled = transform([1, 2, 3], using: { number in number * 2 })
print(doubled)
transform заранее не знает, как именно преобразовать число, — она просто вызывает operation для каждого элемента и собирает результаты. Какое конкретно правило применить, решает тот, кто вызывает transform, передавая своё замыкание.
Trailing closure: замыкание после скобок
Если замыкание — последний (или единственный) параметр функции, его можно вынести за круглые скобки вызова — это называется trailing closure, «замыкание в хвосте»:
let doubled2 = transform([1, 2, 3]) { number in
number * 2
}
print(doubled2)
Обратите внимание: метка параметра using в этой записи не пишется вовсе — при trailing closure она опускается для того параметра, что вынесен наружу. Если у функции только один параметр и это замыкание, круглые скобки можно убрать целиком — как в уже знакомом sorted { $0 > $1 }, где как раз используется trailing closure без единого параметра в скобках.
Частые ошибки
- Забыть слово
inмежду списком параметров и телом замыкания — без него компилятор не поймёт, где заканчивается заголовок и начинается тело. - Пытаться вынести замыкание за скобки (trailing closure), если оно не последний параметр функции — так не получится, порядок параметров переставить нельзя.
- Ждать неявного возврата в замыкании с несколькими строками кода — он работает только при одном выражении в теле, иначе нужен явный
return. - Смешивать
$0,$1с одновременным объявлением именованных параметров черезinв одном и том же замыкании — выбирайте что-то одно: либо имена, либо сокращённые номера. - Считать, что замыкание обязано что-то использовать снаружи себя, чтобы называться замыканием, — оно остаётся замыканием, даже если работает только со своими параметрами.
Резюме
- Замыкание — блок кода без имени, который можно сохранить в переменную, передать параметром или вызвать сразу; обычная функция — тоже замыкание, только именованное.
- Полная запись:
{ (параметры) -> Тип in тело }; типы параметров и возврата можно опустить, если компилятор выводит их из контекста. - Тело из одного выражения не требует
return— это неявный возврат. $0,$1и так далее — сокращённые имена параметров по позиции, без объявления списка параметров иin.- Замыкание, переданное последним параметром функции, можно вынести за скобки вызова — trailing closure; если параметр единственный, скобки убираются целиком.
Проверьте себя
4 вопроса
Дважды применить преобразование
Напишите функцию applyTwice, которая принимает целое число value и замыкание transform типа (Int) -> Int, а возвращает результат применения transform к value два раза подряд (второй раз — к результату первого применения).
Внутри функции просто вызовите transform дважды, передав во второй раз результат первого вызова.
Конвейер преобразований
Напишите функцию runPipeline, которая принимает стартовое число start и массив замыканий steps типа [(Int) -> Int]. Функция должна по очереди применить каждое замыкание из steps к текущему значению (первое замыкание — к start, второе — к результату первого, и так далее) и вернуть итоговое число. Если массив steps пустой, верните start без изменений.
Заведите переменную для текущего результата и в цикле for по steps обновляйте её вызовом очередного замыкания.