Unetway

Swift - Замыкания

Создать свой сайт

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

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

ЗАМЕТКА

Не беспокойтесь, если вы не знакомы с концепцией захвата. Это подробно объясняется ниже в разделе «Захват ценностей» .

Глобальные и вложенные функции, представленные в функциях , на самом деле являются частными случаями замыканий. Замыкания принимают одну из трех форм:

  • Глобальные функции - это замыкания, которые имеют имя и не захватывают никаких значений.
  • Вложенные функции - это замыкания, которые имеют имя и могут захватывать значения из своей вмещающей функции.
  • Выражения замыканий - это безымянные замыкания, написанные в упрощенном синтаксисе, которые могут захватывать значения из окружающего контекста.

Выражения замыкания Swift имеют чистый, понятный стиль с оптимизациями, которые способствуют короткому синтаксису без помех в общих сценариях. Эти оптимизации включают в себя:

  • Вывод параметров и типов возвращаемых значений из контекста
  • Неявные возвраты от замыканий с одним выражением
  • Сокращенные имена аргументов
  • Синтаксис замыкающего замыкания

Закрытие выражений

Вложенные функции, представленные в Nested Functions , являются удобным средством присвоения имен и определения автономных блоков кода как части более крупной функции. Однако иногда полезно писать более короткие версии функционально-подобных конструкций без полного объявления и имени. Это особенно верно, когда вы работаете с функциями или методами, которые принимают функции в качестве одного или нескольких своих аргументов.

Закрывающие выражения - это способ написать встроенные замыкания в кратком, сфокусированном синтаксисе. Выражения замыкания обеспечивают несколько оптимизаций синтаксиса для написания замыканий в сокращенной форме без потери ясности или намерения. Приведенные ниже примеры выражений замыканий иллюстрируют эти оптимизации, уточняя один пример sorted(by:)метода в течение нескольких итераций, каждая из которых выражает ту же функциональность более кратким способом.

Сортированный метод

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

Приведенные ниже примеры выражений замыкания используют sorted(by:)метод для сортировки массива Stringзначений в обратном алфавитном порядке. Вот начальный массив для сортировки:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:)Метод принимает замыкание , которое принимает два аргумента одного и того же типа, что и содержимое массива и возвращает Boolзначение сказать , должно ли первое значение перед или после второго значения как значения сортируются. Закрытие сортировки должно возвращаться, trueесли первое значение должно стоять перед вторым значением, и в falseпротивном случае.

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

Один из способов обеспечить закрытие сортировки - написать нормальную функцию правильного типа и передать ее в качестве аргумента sorted(by:)методу:

func backward(_ s1: String, _ s2: String) -> Bool {

return s1 > s2

}

var reversedNames = names.sorted(by: backward)

// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

Если первая строка ( s1) больше, чем вторая строка ( s2), backward(_:_:)функция вернется true, указывая, что s1должно быть раньше s2в отсортированном массиве. Для символов в строках «больше чем» означает «появляется позже в алфавите, чем». Это означает, что буква "B"«больше, чем» буква "A", а строка "Tom"больше, чем строка "Tim". Это дает обратный алфавитный вид с "Barry"размещением до "Alex"и так далее.

Однако это довольно многословный способ написать то, что по сути является функцией с одним выражением ( ). В этом примере было бы предпочтительнее написать закрытие сортировки встроенным, используя синтаксис выражения закрытия.a > b

Синтаксис выражения закрытия

Синтаксис выражения замыкания имеет следующую общую форму:

{ (parameters) -> return type in

statements

}

В параметры в синтаксисе выражения закрытия может быть в-из параметров, но они не могут иметь значения по умолчанию. Параметры Variadic можно использовать, если вы укажете параметр Variadic. Кортежи также могут использоваться в качестве типов параметров и типов возвращаемых данных.

Пример ниже показывает версию выражения замыкания backward(_:_:)функции сверху:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in

return s1 > s2

})

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

Начало тела замыкания вводится по inключевому слову. Это ключевое слово указывает на то, что определение параметров и типа возврата замыкания завершено, и тело замыкания должно начаться.

Поскольку тело замыкания очень короткое, его можно записать в одну строку:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

Это показывает, что общий вызов sorted(by:)метода остался прежним. Пара скобок по-прежнему переносит весь аргумент метода. Однако этот аргумент теперь является внутренним закрытием.

Вывод типа из контекста

Поскольку закрытие сортировки передается методу в качестве аргумента, Swift может определить типы его параметров и тип возвращаемого значения. sorted(by:)Метод вызывается на массив строк, поэтому его аргумент должен быть функцией типа . Это означает , что и типам не должны быть написано как часть определения Выражения закупоривающего. Поскольку все типы могут быть выведены, стрелка возврата ( ) и круглые скобки вокруг имен параметров также могут быть опущены:(String, String) -> Bool(String, String)Bool->

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

Всегда можно вывести типы параметров и возвращаемый тип при передаче замыкания в функцию или метод в виде встроенного выражения замыкания. В результате вам никогда не нужно писать встроенное замыкание в его наиболее полной форме, когда замыкание используется в качестве аргумента функции или метода.

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

Неявные возвраты из замыканий с одним выражением

Замыкания с одним выражением могут неявно возвращать результат их единственного выражения, опуская returnключевое слово из их объявления, как в этой версии предыдущего примера:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Здесь тип функции sorted(by:)аргумента метода проясняет, что Boolзначение должно быть возвращено замыканием. Поскольку тело замыкания содержит единственное выражение ( ), которое возвращает значение, двусмысленности нет, и ключевое слово может быть опущено.s1 > s2Boolreturn

Сокращенные имена аргументов

Swift автоматически предоставляет сокращенные имена аргументов встраивать затворы, которые могут быть использованы для обозначения значений аргументов замыкания по именам $0$1$2и так далее.

Если вы используете эти сокращенные имена аргументов в своем выражении замыкания, вы можете опустить список аргументов замыкания из его определения, а число и тип сокращенных имен аргументов будут выведены из ожидаемого типа функции. inКлючевое слово также может быть опущена, поскольку выражение укупорочное полностью состоит из его тела:

reversedNames = names.sorted(by: { $0 > $1 } )

Здесь $0и $1ссылаются на первый и второй Stringаргументы замыкания .

Операторские методы

На самом деле есть еще более короткий способ написания выражения замыкания выше. StringТип Swift определяет свою специфическую для строки реализацию оператора more-than ( >) как метод, который имеет два параметра типа Stringи возвращает значение типа Bool. Это точно соответствует типу метода, необходимому для sorted(by:)метода. Следовательно, вы можете просто передать оператор «больше, чем», и Swift определит, что вы хотите использовать его специфическую для строки реализацию:

reversedNames = names.sorted(by: >)

Для получения дополнительной информации о методе оператора см. Методы оператора .

Замыкающие затворы

Если вам необходимо передать выражение замыкания в функцию в качестве последнего аргумента функции, а выражение замыкания является длинным, вместо этого может быть полезно записать его в качестве завершающего замыкания . Завершающее замыкание записывается после круглых скобок вызова функции, даже если оно все еще является аргументом функции. Когда вы используете конечный синтаксис замыкания, вы не пишете метку аргумента для замыкания как часть вызова функции.

func someFunctionThatTakesAClosure(closure: () -> Void) {

// function body goes here

}



// Here's how you call this function without using a trailing closure:



someFunctionThatTakesAClosure(closure: {

// closure's body goes here

})



// Here's how you call this function with a trailing closure instead:



someFunctionThatTakesAClosure() {

// trailing closure's body goes here

}

Закрытие с сортировкой строк из приведенного выше раздела Синтаксис выражения закрытия может быть записано вне sorted(by:)скобок метода как завершающее замыкание:

reversedNames = names.sorted() { $0 > $1 }

Если выражение закрытия предоставляется в качестве единственного аргумента функции или метода, и вы предоставляете это выражение как конечное замыкание, вам не нужно писать пару скобок ()после имени функции или метода при вызове функции:

reversedNames = names.sorted { $0 > $1 }

Конечные замыкания наиболее полезны, когда замыкание достаточно длинное, и его невозможно записать в одной строке. Например, у Arrayтипа Swift есть map(_:)метод, который принимает выражение замыкания в качестве единственного аргумента. Замыкание вызывается один раз для каждого элемента в массиве и возвращает альтернативное сопоставленное значение (возможно, другого типа) для этого элемента. Характер сопоставления и тип возвращаемого значения оставляется на усмотрение замыкания.

После применения предоставленного замыкания к каждому элементу массива map(_:)метод возвращает новый массив, содержащий все новые сопоставленные значения, в том же порядке, что и их соответствующие значения в исходном массиве.

Вот как вы можете использовать map(_:)метод с замыкающим замыканием для преобразования массива 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]

Приведенный выше код создает словарь сопоставлений между целочисленными цифрами и англоязычными версиями их имен. Он также определяет массив целых чисел, готовый для преобразования в строки.

Теперь вы можете использовать numbersмассив для создания массива Stringзначений, передав выражение закрытия в метод массива map(_:)в качестве завершающего замыкания:

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 is inferred to be of type [String]

// its value is ["OneSix", "FiveEight", "FiveOneZero"]

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

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

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

ЗАМЕТКА

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

Строка извлекается из digitNamesсловаря добавляется в передней части output, эффективно строить строковую версию числа в обратном порядке . (Выражение дает значение для , для и для .)number % 106168580510

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

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

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

Захват ценностей

Замыкание может захватывать константы и переменные из окружающего контекста, в котором оно определено. Затем замыкание может ссылаться и изменять значения этих констант и переменных внутри своего тела, даже если исходная область, которая определяла константы и переменные, больше не существует.

В Swift самой простой формой замыкания, которая может захватывать значения, является вложенная функция, записанная в теле другой функции. Вложенная функция может захватывать любые аргументы своей внешней функции, а также может фиксировать любые константы и переменные, определенные во внешней функции.

Вот пример вызываемой функции makeIncrementer, которая содержит вложенную вызываемую функцию incrementer. Вложенная incrementer()функция захватывает два значения runningTotalи amountиз окружающего контекста. После захвата этих значений incrementerвозвращается makeIncrementerкак замыкание, которое увеличивается runningTotalпри amountкаждом вызове.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

var runningTotal = 0

func incrementer() -> Int {

runningTotal += amount

return runningTotal

}

return incrementer

}

Тип возврата makeIncrementeris . Это означает, что он возвращает функцию , а не простое значение. Функция, которую она возвращает, не имеет параметров и возвращает значение каждый раз, когда она вызывается. Чтобы узнать, как функции могут возвращать другие функции, см. Типы функций как Типы возврата .() -> IntInt

makeIncrementer(forIncrement:)Функция определяет целое переменное runningTotal, чтобы сохранить текущую текущую сумму инкрементор , которые будут возвращены. Эта переменная инициализируется значением 0.

makeIncrementer(forIncrement:)Функция имеет один Intпараметр с аргументом меткой forIncrement, и имя параметра amount. Значение аргумента, передаваемое этому параметру, указывает, сколько runningTotalнужно увеличивать при каждом вызове возвращаемой функции инкремента. makeIncrementerФункция определяет вложенную функцию с именем incrementer, которая выполняет фактическое приращение. Эта функция просто добавляет amountк runningTotal, и возвращает результат.

При рассмотрении в отдельности вложенная incrementer()функция может показаться необычной:

func incrementer() -> Int {

runningTotal += amount

return runningTotal

}

incrementer()Функция не имеет параметров, и все же это относится к runningTotalи amountвнутри его тела функции. Это делается путем захвата ссылки на runningTotalи amountиз окружающей функции и использования их в своем собственном теле функции. Захватив по ссылке гарантирует , что runningTotalи amountне исчезает , когда вызов makeIncrementerконцов, а также гарантирует , что runningTotalдоступно в следующий раз incrementerфункция вызывается.

ЗАМЕТКА

В качестве оптимизации Swift может вместо этого захватить и сохранить копию значения, если это значение не изменено закрытием и если значение не изменено после создания замыкания.

Swift также управляет всем управлением памятью, связанным с удалением переменных, когда они больше не нужны.

Вот пример makeIncrementerв действии:

let incrementByTen = makeIncrementer(forIncrement: 10)

В этом примере устанавливается константа, вызываемая incrementByTenдля ссылки на функцию инкремента, которая добавляет 10к своей runningTotalпеременной каждый раз, когда она вызывается. Многократный вызов функции показывает это поведение в действии:

incrementByTen()

// returns a value of 10

incrementByTen()

// returns a value of 20

incrementByTen()

// returns a value of 30

Если вы создадите второй инкремент, он будет иметь собственную сохраненную ссылку на новую отдельную runningTotalпеременную:

let incrementBySeven = makeIncrementer(forIncrement: 7)

incrementBySeven()

// returns a value of 7

Повторный вызов исходного incrementer ( incrementByTen) продолжает увеличивать собственную runningTotalпеременную и не влияет на переменную, захваченную incrementBySeven:

incrementByTen()

// returns a value of 40

ЗАМЕТКА

Если вы назначаете замыкание свойству экземпляра класса, и замыкание захватывает этот экземпляр, ссылаясь на экземпляр или его члены, вы создадите сильный ссылочный цикл между замыканием и экземпляром. Swift использует списки захвата, чтобы разорвать эти сильные циклы ссылок. Для получения дополнительной информации см. Strong Reference Cycles для замыканий .

Замыкания являются ссылочными типами

В приведенном выше примере, incrementBySevenи incrementByTenпостоянные, но замыкания эти константы относятся к еще способны увеличивать те runningTotalпеременные , которые они захватили. Это потому, что функции и замыкания являются ссылочными типами .

Всякий раз, когда вы назначаете функцию или замыкание для константы или переменной, вы фактически устанавливаете эту константу или переменную как ссылкуна функцию или замыкание. В приведенном выше примере это выбор затвора, который incrementByTen относится к константе, а не содержимое самого затвора.

Это также означает, что если вы назначаете замыкание двум различным константам или переменным, обе эти константы или переменные ссылаются на одно и то же замыкание.

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen()

// returns a value of 50



incrementByTen()

// returns a value of 60

В приведенном выше примере показано, что вызов alsoIncrementByTenаналогичен вызову incrementByTen. Поскольку оба они относятся к одному и тому же замыканию, они оба увеличивают и возвращают один и тот же промежуточный итог.

Экранирование

Говорят, что замыкание экранирует функцию, когда замыкание передается в качестве аргумента функции, но вызывается после ее возврата. Когда вы объявляете функцию, которая принимает замыкание в качестве одного из своих параметров, вы можете писать @escapingперед типом параметра, чтобы указать, что замыканию разрешено экранировать.

Один из способов избежать замыкания - сохранить его в переменной, определенной за пределами функции. Например, многие функции, которые запускают асинхронную операцию, принимают в качестве обработчика завершения аргумент замыкания. Функция возвращается после запуска операции, но замыкание не вызывается до тех пор, пока операция не будет завершена - замыкание необходимо закрыть, чтобы вызвать его позже. Например:

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {

completionHandlers.append(completionHandler)

}

someFunctionWithEscapingClosure(_:)Функция принимает замыкание в качестве аргумента и добавляет его в массив , который объявлен вне функции. Если вы не пометили параметр этой функции @escaping, вы получите ошибку во время компиляции.

Маркировка закрытия @escapingозначает, что вы должны selfявно ссылаться на него. Например, в приведенном ниже коде переданное закрытие someFunctionWithEscapingClosure(_:)является экранирующим закрытием, что означает, что оно должно ссылаться selfявно. Напротив, переданное закрытие someFunctionWithNonescapingClosure(_:)является неэскапирующим закрытием, что означает, что оно может ссылаться selfнеявно.

func someFunctionWithNonescapingClosure(closure: () -> Void) {

closure()

}



class SomeClass {

var x = 10

func doSomething() {

someFunctionWithEscapingClosure { self.x = 100 }

someFunctionWithNonescapingClosure { x = 200 }

}

}



let instance = SomeClass()

instance.doSomething()

print(instance.x)

// Prints "200"



completionHandlers.first?()

print(instance.x)

// Prints "100"

Autoclosures

Autoclosure является замыкание, которое создается автоматически , чтобы обернуть выражение, это время передается в качестве аргумента функции. Он не принимает никаких аргументов, и когда он вызывается, он возвращает значение выражения, заключенного в него. Это синтаксическое удобство позволяет опускать скобки вокруг параметра функции, записывая нормальное выражение вместо явного замыкания.

Распространено вызывать функции, которые принимают автозамены, но это не распространено для реализации такого рода функции. Например, assert(condition:message:file:line:)функция принимает autoclosure для своих conditionи messageпараметров; его conditionпараметр оценивается только в отладочных сборках, а его messageпараметр оценивается только в том случае, если он conditionесть false.

Автоматическое закрытие позволяет отложить оценку, потому что код внутри не выполняется, пока вы не вызовете замыкание. Задержка оценки полезна для кода, который имеет побочные эффекты или является дорогостоящим в вычислительном отношении, потому что он позволяет вам контролировать, когда этот код оценивается. Код ниже показывает, как закрытие задерживает оценку.

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

print(customersInLine.count)

// Prints "5"



let customerProvider = { customersInLine.remove(at: 0) }

print(customersInLine.count)

// Prints "5"



print("Now serving \(customerProvider())!")

// Prints "Now serving Chris!"

print(customersInLine.count)

// Prints "4"

Даже если первый элемент customersInLineмассива удаляется кодом внутри замыкания, элемент массива не удаляется до тех пор, пока замыкание не будет фактически вызвано. Если замыкание никогда не вызывается, выражение внутри замыкания никогда не вычисляется, что означает, что элемент массива никогда не удаляется. Обратите внимание, что тип customerProvider- это не Stringпросто функция без параметров, которая возвращает строку.() -> String

Вы получаете такое же поведение отложенной оценки, когда передаете замыкание в качестве аргумента функции.

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]

func serve(customer customerProvider: () -> String) {

print("Now serving \(customerProvider())!")

}

serve(customer: { customersInLine.remove(at: 0) } )

// Prints "Now serving Alex!"

serve(customer:)Функция в листинге выше требуется явное замыкание , которое возвращает имя клиента. Версия serve(customer:)ниже выполняет ту же операцию, но вместо явного закрытия она выполняет автозаполнение, помечая тип своего параметра @autoclosureатрибутом. Теперь вы можете вызывать функцию, как если бы она взяла Stringаргумент вместо замыкания. Аргумент автоматически преобразуется в замыкание, поскольку customerProviderтип параметра помечается @autoclosureатрибутом.

// customersInLine is ["Ewa", "Barry", "Daniella"]

func serve(customer customerProvider: @autoclosure () -> String) {

print("Now serving \(customerProvider())!")

}

serve(customer: customersInLine.remove(at: 0))

// Prints "Now serving Ewa!"

ЗАМЕТКА

Чрезмерное использование автозаполнений может затруднить понимание вашего кода. Контекст и имя функции должны прояснить, что оценка откладывается.

Если вы хотите autoclosure , что позволило избежать, использовать как @autoclosureи @escapingатрибуты. @escapingАтрибут описано выше в Экранирование Closures .

// customersInLine is ["Barry", "Daniella"]

var customerProviders: [() -> String] = []

func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {

customerProviders.append(customerProvider)

}

collectCustomerProviders(customersInLine.remove(at: 0))

collectCustomerProviders(customersInLine.remove(at: 0))



print("Collected \(customerProviders.count) closures.")

// Prints "Collected 2 closures."

for customerProvider in customerProviders {

print("Now serving \(customerProvider())!")

}

// Prints "Now serving Barry!"

// Prints "Now serving Daniella!"

В приведенном выше коде вместо вызова замыкания, переданного ему в качестве customerProviderаргумента, collectCustomerProviders(_:)функция добавляет замыкание к customerProvidersмассиву. Массив объявляется вне области действия функции, что означает, что замыкания в массиве могут быть выполнены после возврата из функции. В результате значение customerProviderаргумента должно быть разрешено за пределами области действия функции.