$ sudo teach IT

Зачем нужны замыкания

Когда вы сортировали массивы через 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 обновляйте её вызовом очередного замыкания.