Основы Swift / 9.1. Замыкания - Основы


Видео


Основы замыканий
Замыкания - это самосодержащиеся блоки функциональности, которые могут быть переданы и использованы в Вашем коде. Замыкания в Swift схожи с блоками в C и Objective-C и лямбдами в других языках программирования.

Замыкания могут захватывать и хранить ссылки на любые константы и переменные из контекста, где они объявлены. Это известно как замыкание на эти константы и переменные. Swift управляет всем взаимодействием с памятью при захвате вместо Вас.

Глобальные и вложенные функции - это на самом деле специальные варианты замыканий. Замыкания могут принимать одну их трёх форм:
  • Глобальные функции - это замыкания, которые имеют имена и не захватывают никаких значений.
  • Вложенные функции - это имена, которые имеют имя и могут захватывать значения из их окружающей функции.
  • Замыкания-выражения - это безымянные замыкания, написанные в простом синтаксисе, которые могут захватывать значения из их окружающего контекста.
Выражения-замыкания в Swift имеют простой и понятый стиль с оптимизациями, которые добавляют краткости при использовании в общих сценариях. Эти оптимизации включают:
  • Выведение параметров и возвращаемого значения из контекста
  • Неявное возвращение из замыканий с одним выражением
  • Сокращённые имена аргументов
  • Trailing-синтаксис замыканий
Выражения-замыкания
Вложенные функции - это удобный способ для наименования и определения самосодержащихся блоков кода в качестве части большей функции. Однако иногда бывает полезным записать краткие версии функционально-подобных конструкций без полной декларации и имени. Это особенно полезно, когда Вы работаете с функциями или методами, которые принимают функции в качестве своих аргументов.

Функции-выражения - это способ писать подставляемые замыкания в кратком и понятном стиле. Выражения-замыкания предоставляют некоторые оптимизации синтаксиса для написания замыканий в краткой форме без потери прозрачности или смысла.
Метод sortedby:
Стандартная библиотека Swift предоставляет метод sorted(by:), который сортирует массив значений известного типа, основываясь на выходном значении сортирующего замыкания, которое Вы предоставляете. Как только он завершает процесс сортировки, то метод sorted(by:) возвращает новый массив того же типа и размера, что и старый, элементы которое отсортированы в корректном порядке. Оригинальный массив не изменяется методом sorted(by:).
@{9.1\1\1}
let names = ["Саша", "Алекс", "Артемий", "Оля", "Маша"]
Пример использует метод sorted(by:) для сортировки массива значений String в обратном алфавитном порядке.
Метод sortedby: 2
Метод sorted(by:) принимает замыкание, которое имеет два аргумента того же типа, что и тип, содержащийся в массиве, и возвращает значение Bool, чтобы сказать, должно ли первое значение появится перед или после второго значения, когда значения будут отсортированы. Сортирующее замыкание должно вернуть true, если первое значение должно появится после второго, и false - в противном случае.

Этот пример сортирует массив значений типа String, так что сортирующее замыкание должно быть функцией типа (String, String) -> Bool.

Один способ предоставить сортирующее замыкание - это написать обычную функцию подходящего типа передать её в качестве аргумента в метод sorted(by:):
@{9.1\1\2}
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames теперь равен ["Саша", "Оля", "Маша", "Артемий", "Алекс"]
Если первая строка (s1) больше второй строки (s2), то функция backward(_:_:) вернёт значение true, обозначающее, что s1 должна появиться перед s2 в отсортированном массиве. Для символов в строке "больше чем" означает "появляется позже в алфавите чем". Это означает, что буква "B" "больше чем" буква "A", а строка "Tom" больше нежели строка "Tim". Это даст нам обратную алфавитную сортировку, где "Barry" будет расположен перед "Alex" и так далее.

Однако это крайне длинный способ написать на самом деле одноутробное выражение (a > b). В этом примере предпочтительнее было бы написать сортирующее замыкание встроено, используя синтаксис замыкания-выражения.
Синтаксис выражения-замыкания
Параметры в замыкании-выражении могут быть in-out параметрами, но они не могут иметь значений по-умолчанию. Вариативные параметры могут быть использованы, если Вы зададите имя для такого параметра. Кортежи так же могут быть использованы в качестве типов параметров и возвращаемых типов.
@{9.1\1\3}
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
} )
// reversedNames теперь равен ["Саша", "Оля", "Маша", "Артемий", "Алекс"]
Пример демонстрирует версию функции backward(_:_:) с помощью выражения-замыкания из примера ранее.

Заметьте, что декларация параметров и возвращаемого типа для этого инлайн-замыкания идентично таковым у функции backward(_:_:). В обоих случаях оно записывается как (s1: String, s2: String) -> Bool. Однако для инлайн-замыкания параметры и возвращаемое значение записываются внутри фигурных скобок, а не снаружи их.

Начало тела замыкания отображается с помощью ключевого слова in. Это ключевое слово обозначает, что определение параметров и возвращаемого типа завершено, и начинается тело функции.
Однострочный синтаксис
Так как тело замыкания довольно коротко, то оно может быть записано в одну строку.
@{9.1\1\4}
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
// reversedNames теперь равен ["Саша", "Оля", "Маша", "Артемий", "Алекс"]
Это иллюстрирует, что в общем вызов метода sorted(by:) остался тем же. Пара круглых скобок продолжает обхватывать весь аргумент для метода. Однако аргумент теперь является инлайн-замыканием.
Выведение типа из контекста
Так как сортирующее замыкание передаётся в качестве аргумента метода, то Swift может вывести типы его параметров и тип возвращаемого значения. Метод sorted(by:) всё так же продолжает вызываться на массиве строк, так что аргумент должен быть функцией типа (String, String) -> Bool. Это означает, что нет необходимости явно указывать (String, String) и Bool в качестве части определения выражения-замыкания. Так как все типы могут быть выведены, то возвращающая стрелка (->) и круглые скобки вокруг имён параметров могут быть так же опущены.
@{9.1\1\5}
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
// reversedNames теперь равен ["Саша", "Оля", "Маша", "Артемий", "Алекс"]
Выведение типа из контекста 2
Всегда является возможным вывести типы параметров и возвращаемого значения, когда замыкание передаётся в функцию или метод в качестве инлайн-замыкания. В результате Вам никогда не нужно писать инлайн-замыкание в его полной форме, когда замыкание используется в качестве функции или метода аргумента.

Однако Вы так же можете явно указывать типы, если Вы так пожелаете, и делать так может быть предпочтительно, если Вы хотите избежать неопределённости для читателей Вашего кода. В случае с методом sorted(by:) цель замыкания понятно из факта, что имеет место быть сортировка, и для читателя будет безопасным принять, что замыкание скорее всего будет работать со значениями String, так как массив имеет такой тип.

Однострочное замыкание может неявно возвращать результат своего единственного выражения путём опускания ключевого слова return из их определения, как в случае с этой версией предыдущего примера:
@{9.1\1\6}
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Здесь функциональный тип аргумента метода sorted(by:) делает понятным, что из замыкания должно быть возвращено значение типа Bool. Так как тело замыкания содержит единственное выражение (s1 > s2), которое возвращает значение типа Bool, так что нет никакой неопределённости, а ключевое слово return может быть опущено.
Краткие имена аргументов
Swift автоматически предоставляет сокращённые формы имён аргументов для вставки в инлайн-замыкания, которые могут быть использованы для ссылки на значения аргументов замыканий по их именам $0, $1, $2 и так далее.

Если Вы используете эти сокращённые имена аргументов внутри Вашего замыкания, Вы можете опустить список аргументов замыкания из его определения, и количество и тип аргументов будет выведен из ожидаемого типа функции. Ключевое слово in так же может быть опущенной так как выражение-замыкание состоит только из одного лишь тела:
@{9.1\1\7}
reversedNames = names.sorted(by: { $0 > $1 } )
Здесь $0 и $1 ссылаются на первый и второй аргументы типа String.
Методы-операторы
На самом деле есть даже более короткий способ записать выражение-замыкание из примера выше. Тип String в Swift определяет специальную строковую реализацию оператор больше чем (>) в качестве метода, который имеет два параметра типа String, а возвращает значение типа Bool. Это полностью совпадает с типом метода, который нужен методу sorted(by:). На самом деле Вы просто можете передать оператор больше-чем, и Swift выведет, что Вы хотите использовать его специальную строковую имплементацию.
@{9.1\1\8}
reversedNames = names.sorted(by: >)
Волочащиеся или trailing- замыкания
Если Вам нужно передать замыкание в функцию в качестве последнего аргумент функции и замыкание длинное, то написать вместо обычного волочащееся замыкание. волочащееся замыкание пишется после круглых скобок функции, хотя это всё так же аргумент функции. Когда Вы используете такой синтаксис для замыканий, Вам не нужно писать ярлык аргумента для замыкания в качестве части вызова функции.
@{9.1\2}
func someFunctionThatTakesAClosure(closure: () -> Void) {
    // здесь будет тело функции
}

// Вот так эта функция вызвается без trailing-замыкания:

someFunctionThatTakesAClosure(closure: {
    // Здесь будет тело замыкания
})

// Вот так эта функция вызвается с помощью trailing-замыкания:

someFunctionThatTakesAClosure() {
    // Здесь будет тело trailing-замыкания
}
@{9.1\3\1}
Замыкание для сортировки строк выше может быть записано вне круглых скобок метода sorted(by:) в качестве trailing-замыкания.
let names = ["Саша", "Алекс", "Артемий", "Оля", "Маша"]
var reversedNames = names.sorted() { $0 > $1 }
@{9.1\3\2}
Если замыкание предоставляется в качестве единственного аргумента функции или метода, и Вы предоставляете это выражение в качестве trailing-замыкания, то Вам не нужно писать пару круглых скобок () после имени функции или метода, когда Вы вызываете функцию.
reversedNames = names.sorted { $0 > $1 }
Применение волочащихся замыканий
Trailing-замыкания наиболее полезны, когда замыкание достаточно длинное, так что невозможно написать его в одну строку. В качестве примера, тип Array в Swift имеет метод map(_:), который принимает замыкание в качестве своего единственного аргумента. Замыкание вызывается один раз для каждого элемента массива и возвращает поставленное ему в соответствие значение (возможно какого-то другого типа). Природа отображения и тип возвращаемого значения оставлены на откуп замыканию.

После применения предоставленного замыкания к каждому элементу массива метод map(_:) возвращает новый массив, содержащий все новые отображённые значения в том же порядке, что и корреспондирующие им значения оригинального массива.
@{9.1\4\1}
Здесь приведён пример того, как Вы можете использовать метод map(_:) с trailing-замыканием для конвертации массива Int в массив значений String. Массив [16, 58, 510] используется для создания нового массива ["OneSix", "FiveEight", "FiveOneZero"].

Код создаёт словарь связанных между собой целых цифр и их английских названий. Он так же определяет массив целых чисел, готовых к конвертации в строки.
let digitNames = [
    0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
@{9.1\4\2}
let strings = numbers.map{ (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings выведено в тип [String]
// его значение равно ["OneSix", "FiveEight", "FiveOneZero"]
Теперь Вы можете использовать массив numbers для создания массива типа String, передав в метод массива map(_:) волочащееся замыкание.

Метод map(_:) вызывает замыкание единожды для каждого элемента в массиве. Вам не нужно явно указывать тип входного параметра замыкания, number, так как этот тип может быть выведен из значений отображаемого массива.

В этом примере переменная number инициализируется значением параметра замыкания number, так что это значение может быть изменено внутри тела замыкания. (Параметры функций и замыканий всегда константы.) Замыкание так же специфицирует возвращаемый тип String, чтобы указать, какиой тип будет храниться в отображённом выходном массиве.

Замыкание строит строку с названием output всякий раз при своём вызове. Оно вычисляет последнюю цифру number с использованием оператора остатка от деления (number % 10) и использует эту цифру для выбора подходящей строки в словаре digitNames. Это замыкание может быть использовано для создания строки, отображающей любое целое число, большее 0.

Вызов сабскрипта словаря digitNames сопровождается восклицательным знаком (!), так как сабскрипты словарей возвращают опциональное значение для индикации того, что выбор значения из словаря может провалиться, если указанный ключ не существует. В примере Выше гарантировано, что number % 10 всегда будет корректным ключом сабскрита для словаря digitNames, так что восклицательный знак используется для принудительной распаковки значения типа String, хранимого в опциональном возвращаемом значении сабскрипта.

Строка, полученная из словаря digitNames прибавляется в переднюю часть output, эффективно создавая строковые представление числа в обратном порядке. (Выражение number % 10 даёт число 6 для 16, 8 для 58 и 0 для 510.)

Переменная number затем делится на 10. Так как это целое число, то оо будет округлено вниз при делении, так что 16 станет 1, 58 станет 5, а 510 станет 51.

Этот процесс повторяется, пока number не станет равен 0, в этот момент строка output будет возвращена замыканием и добавлена в выходной массив метода map(_:).

Использование синтаксиса trailing-замыкания в примере выше позволяет инкаспулировать функциональность замыкания сразу после функции, поддерживающей это замыкание без необходимости оборачивать всё замыкание во внешнюю круглую скобку метода map(_:).