Перейти к содержимому
Теория · Замыкания · урок 2 из 8

Синтаксис замыканий

В прошлом уроке каждую функцию для applyTwice объявляли отдельно: через func и с именем. Но если функция нужна один раз, давать ей имя неудобно. Это как вписывать рецепт бутерброда в кулинарную книгу, чтобы один раз отдать его другу. Удобнее написать рецепт прямо на карточке, которую вы отдаёте.

Функцию без имени, записанную прямо там, где она нужна, называют замыканием:

SWIFT
func applyTwice(_ f: (Int) -> Int, to x: Int) -> Int {
    f(f(x))
}

let triple = { (x: Int) -> Int in
    x * 3
}
print(triple(2)) // 6
print(applyTwice({ (x: Int) -> Int in x + 10 }, to: 1)) // 21

Давайте разберём программу:

  1. applyTwice дважды вызывает функцию, которую ей передали.
  2. Константа triple хранит замыкание, которое утраивает число. Вызывают его так же, как функцию: triple(2) даёт 6.
  3. В последней строке замыкание написано прямо в вызове applyTwice, на месте первого аргумента. Оно прибавляет 10, поэтому 1 превращается в 11, а потом в 21.

Из чего состоит замыкание

Запись { (x: Int) -> Int in x * 3 } — это функция без слова func и без имени. Её части:

  • фигурные скобки — начало и конец замыкания;
  • (x: Int) -> Int — параметры и тип результата, как в заголовке функции;
  • in — слово, которое отделяет заголовок от тела;
  • x * 3 — тело; если оно из одного выражения, return не нужен, как у функции.

Тип у triple — (Int) -> Int. Замыкание — такое же значение, как функция: его кладут в константу, передают аргументом и возвращают из функции.

Короткие записи

Полная запись длинная, и Swift позволяет её сокращать. Сокращать удобнее всего там, где компилятор уже знает нужный тип функции, например в аргументе applyTwice или sorted(by:):

SWIFT
print(applyTwice({ x in x + 10 }, to: 1)) // 21
print(applyTwice({ $0 + 10 }, to: 1)) // 21

let numbers = [5, 2, 8, 1]
print(numbers.sorted(by: { a, b in a > b })) // [8, 5, 2, 1]
print(numbers.sorted { $0 > $1 }) // [8, 5, 2, 1]
print(numbers.sorted(by: >)) // [8, 5, 2, 1]

Без типов

Параметр f у applyTwice имеет тип (Int) -> Int. Поэтому в замыкании можно не писать типы: { x in x + 10 }. Компилятор сам поймёт, что x — это Int и результат тоже Int. Это тот же вывод типов, что в let n = 5.

Имена $0 и $1

Можно не придумывать имена параметрам вовсе. Тогда первый параметр называется $0, второй — $1 и так далее, а заголовок вместе с in пропадает: { $0 + 10 }. Так пишут короткие замыкания в одну строку. В длинном замыкании свои имена параметров читать удобнее.

Замыкание после скобок

Метод sorted(by:) принимает функцию типа (Int, Int) -> Bool. Она получает два элемента и отвечает, должен ли первый стоять раньше второго. Разберём два вызова:

  1. { a, b in a > b } ставит a раньше b, если a больше. Получается порядок по убыванию.
  2. sorted { $0 > $1 } — то же замыкание, вынесенное за скобки вызова. Если замыкание — последний аргумент, его можно написать после закрывающей скобки. Здесь других аргументов нет, поэтому пропали и скобки, и метка by:.

Оператор вместо замыкания

sorted(by: >) — самая короткая запись. Оператор > для чисел — тоже функция типа (Int, Int) -> Bool. Поэтому его передают по имени, как любую функцию.

Почему «замыкание»

Здесь картинка с карточкой неточна. Бумажная карточка ничего не знает о кухне, где её написали. А замыкание видит константы и переменные вокруг того места, где его написали:

SWIFT
let bonus = 5
print(applyTwice({ $0 + bonus }, to: 1)) // 11

bonus — не параметр замыкания, а константа рядом с ним. Замыкание пользуется ею так же, как своим $0: 1 + 5 даёт 6, 6 + 5 даёт 11. Отсюда и название: замыкание замыкает в себе имена из окружающего кода.

Этот вопрос часто задают на собеседовании: что такое замыкание? Ответ из трёх частей:

  • функция без имени, которую пишут прямо в коде, в фигурных скобках;
  • её можно сохранить в константу и передать аргументом;
  • внутри она видит константы и переменные вокруг себя.

Частая ошибка

В замыкании стало две строки, а return не написали:

SWIFT
let shout = { (word: String) -> String in
    let loud = word.uppercased()
    loud + "!" // ✗
}
TEXT
error: missing return in closure expected to return 'String'

Программа не компилируется. Без return обходится только тело из одного выражения. Здесь в теле две строки, и последнюю Swift считает обычной инструкцией, а не результатом. Поэтому над ошибкой компилятор ещё и предупреждает, что результат + не используется. Допишите return: return loud + "!".

Главное

  • Замыкание — функция без имени в фигурных скобках: { (x: Int) -> Int in x * 3 }; слово in отделяет заголовок от тела.
  • Там, где тип функции известен, типы можно не писать, а параметры называть $0, $1.
  • Последний аргумент-замыкание можно вынести за скобки вызова: numbers.sorted { $0 > $1 }.
  • Замыкание видит константы и переменные вокруг места, где его написали.