Сожалеем, но текст оригинала доступен только зарегистрированным пользователям. |
Замыкания
Замыкания это самостоятельно функционирующие блоки, которые возможно передавать и использовать в своем коде. Замыкания в Swift похожи на блоки в Objective-C или на лямбда-функции в других языках программирования.
Замыкания могут захватывать (capture) и хранить ссылки на любые константы и переменные из контекста в котором эти замыкания определены. Также захват называют запиранием (closing over) этих констант и переменных. Swift осуществляет управление памятью при захвате.
trambalda
24.07.17 в 13:46
|
|
Обратите внимание
Не нужно беспокоиться, если вы не знакомы с концепцией захвата, она описана в разделе "Захват значений" в этой главе.
trambalda
24.07.17 в 13:47
|
|
Глобальные и вложенные функции, описанные в главе "Функции", на самом деле являются частным случаем замыканий. Замыкания могут принимать одну из трех форм:
- Глобальные функции - это замыкания, у которых есть имя и которые не захватывают значений.
- Вложенные функции - это замыкания, у которых есть имя, и они могут захватывать значения из функции, в которую они вложены.
- Выражения-замыкания - это замыкания, у которых нет имени, записанные в сокращенном стиле, и они могут захватывать значения из окружающего контекста.
Выражения-замыкания в Swift записываются в простом и понятном стиле, с упрощениями которые поощряют краткий, аккуратный синтаксис в большинстве случаев. Эти упрощения включают:
- Вывод параметров и возвращаемого значения, исходя из контекста.
- Неявный возврат из однострочных замыканий.
- Синтаксис выносных замыканий.
trambalda
24.07.17 в 13:55
|
|
Выражения-замыкания
Вложенные функции, описанные в одноименном разделе главы "Функции", это удобный способ именовать и определить самостоятельный блок кода, размещенный внутри содержащей его функции. Однако, иногда удобно написать короткую версию вложенной функции, но без полного определения и задания имени. Это особенно удобно, когда вы работаете с функциями или методами, которые принимают функции в качестве своих аргументов.
trambalda
23.07.17 в 19:54
|
|
Выражения-замыкания - это способ записывать вложенные замыкания компактным синтаксисом в различной форме без потери читаемости и смысла. Ниже, на примере использования метода sorted(by:), показано, как в несколько этапов, запись замыкания становится все более эффективной, без потери функциональности.
trambalda
23.07.17 в 18:46
|
|
Метод Sorted
Стандартная библиотека Swift предоставляет метод sorted(by:), который сортирует массив значений одного и того же типа на основании результатов работы замыкания, которое передается этому методу. По окончании работы, метод sorted(by:) возвращает новый массив того же типа и размера, но с элементами в заданном порядке. Изначальный массив метод не изменяет.
trambalda
23.07.17 в 18:49
|
|
Приведенный ниже пример с использованием метода sorted(by:) сортирует массив типа String в обратном алфавитном порядке. Ниже показан массив, который необходимо отсортировать:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
Метод sorted(by:) использует замыкание, которое принимает два аргумента того же типа, что и содержимое массива и возвращает Bool, для того чтобы определить - какое из переданных значений должно быть впереди - первое или второе. Замыкание должно вернуть true, если впереди должно быть первое значение и false, если наоборот.
trambalda
23.07.17 в 18:55
|
|
Этот пример сортирует массив типа 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"]
trambalda
23.07.17 в 18:57
|
|
Если первая строка (s1) больше чем вторая строка (s2), то функция backward(_:_:) вернет true, указывая на то, что s1 должна стоять перед s2 в отсортированном массиве. Для строк, состоящих из символов, выражение "больше чем", означает "по алфавиту позже чем". Это означает, что буква "B" "больше чем" буква "A", а строка "Tom" больше чем строка "Tim". Поэтому сравнение "больше чем" приведет к обратной сортировке по алфавиту, то есть "Barry" расположится перед "Alex" и так далее.
Однако, приведенный пример слишком громоздкий для написания простой функции (a > b). В данном примере более предпочтительно было бы использовать вложенное замыкание, используя синтаксис записи замыканий.
trambalda
23.07.17 в 19:13
|
|
Синтаксис выражений-замыканий
Синтаксис записи замыканий в общей форме выглядит так:
{ (parameters) -> return type in statements }
Параметры могут быть сквозными, но не могут иметь значений по-умолчанию. Вариативные параметры допустимы, если у них будет задано имя. Кортежи могут быть как параметрами, так и возвращаемыми значениями.
trambalda
23.07.17 в 19:57
|
|
Пример ниже показывает, как записать предыдущий пример с использованием замыкания:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
Обратите внимание, что объявление параметров и возвращаемого значения для вложенного замыкания точно такое же, как и при объявлении функции backward(_:_:). В обоих случаях, эта запись выглядит как (s1: String, s2: String) -> Bool. Однако для встроенных замыканий, параметры и возвращаемый тип записываются внутри фигурных скобок, а не за ними.
Ключевое слово in указывает но то, что определение параметров и возвращаемого типа завершены и начинается тело замыкания.
trambalda
23.07.17 в 20:03
|
|
Так как тело замыкания довольно короткое, то всё можно записать в одну строку:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
Здесь видно, что в целом, вызов метода sorted(by:) остался точно таким же - пара круглых скобок содержат аргумент метода. Но в данном случае этот аргумент - это вложенное замыкание.
trambalda
23.07.17 в 20:06
|
|
Вывод типа из контекста
Так как сортирующее замыкание передается в метод в качестве аргумента, то Swift может самостоятельно вывести тип параметров и возвращаемого значения для этого метода. Так как метод sorted(by:) был вызван для обработки массива строк, очевидно, что его аргумент должен быть типом функции (String, String) -> Bool. То есть, не обязательно указывать типы при объявлении замыкания. Кроме того, так как все типы могут быть выведены, то и стрелку (->) и скобки вокруг имен параметров можно не указывать:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
trambalda
24.07.17 в 03:56
|
|
При передаче замыкания в функцию всегда можно вывести типы параметров и возвращаемого значения, поэтому, в подобном случае, не требуется записывать замыкание в полной форме.
Тем не менее вы можете указывать типы и это оправданно, если это улучшает читабельность вашего кода. В случае использования метода sorted(by:), очевидно, что замыкание используется для сортировки, и что замыкание работает со строками, так как сортируется массив строк.
trambalda
24.07.17 в 04:04
|
|
Неявный возврат из однострочного замыкания
Однострочное замыкание может неявно вернуть результат своей работы, если пропустить ключевое слово return:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Здесь тип функции метода sorted(by:) очевиден, так как возвращаемое значение замыкания должно быть Bool. Так как единственное выражения замыкания (s1 > s2) возвращает Bool, то нет никакой неоднозначности, и ключевое слово return можно не указывать.
trambalda
24.07.17 в 04:08
|
|
Сокращенные имена аргументов
Swift позволяет сокращать имена аргументов во вложенных замыканиях, используя имена типа $0, $1, $2, и т.д.
Если вы используете сокращенные имена, то вы можете не записывать список аргументов при определении замыкания, номер и тип аргументов будут выведены из типа функции. Также можно не указывать ключевое слово in, в этом случае запись замыкания будет состоять только из его тела:
reversedNames = names.sorted(by: { $0 > $1 } )
Здесь $0 и $1 ссылаются на первый и второй аргументы замыкания типа String.
trambalda
24.07.17 в 04:18
|
|
Методы-операторы
На самом деле можно записать замыкание еще короче. Тип String подразумевает, что реализация оператора "больше чем" (>) как метода требует два параметра типа String и возвращаемое значение Bool. Это в точности совпадает с требованиями метода sorted(by:). Таким образом, вы можете просто передать оператор "больше чем" (>), и Swift выведет из этого соответствующую реализацию:
reversedNames = names.sorted(by: >)
Более подробно смотрите раздел "Метода-операторы" в главе "Более сложные операторы" (Advanced Operators).
trambalda
24.07.17 в 05:03
|
|
Выносные замыкания
Если вам нужно передать замыкание в качестве аргумента функции, но код замыкания большой, то удобно применять выносное замыкание вместо вложенного. Оно записывается после вызова функции, после закрывающейся круглой скобки, несмотря на то, что это замыкание является её аргументом и должно быть указано внутри круглых скобок.
func someFunctionThatTakesAClosure(closure: () -> Void) { // Здесь размещается тело функции }
// Ниже описан вызов функции с вложенным замыканием:
someFunctionThatTakesAClosure(closure: { // Здесь размещается тело замыкания })
// Ниже описан вызов функции с выносным замыканием:
someFunctionThatTakesAClosure() { // Здесь размещается тело выносного замыкания }
trambalda
24.07.17 в 07:44
|
|
Однострочное замыкание из раздела "Синтаксис записи замыканий", которое приводилось выше в виде выносного замыкания будет записываться так:
reversedNames = names.sorted() { $0 > $1 }
Если замыкание является единственным аргументом функции, то можно не указывать и круглые скобки, так как единственный аргумент вынесен:
reversedNames = names.sorted { $0 > $1 }
trambalda
24.07.17 в 07:50
|
|
Выносные замыкания используются, когда код замыкания существенно большой и невозможно его разместить как вложенное в одну строку. Например, массив (тип Array), имеет метод map(_:), у которого единственный аргумент - замыкание . Это замыкание исполняется для каждого элемента массива и возвращает некое значение, которое сопоставляется с данным элементом, возможно и другого типа. Принцип сопоставления и тип возвращаемого значения указывается в замыкании.
После применения замыкания для каждого элемента массива метод map(_:) возвращает новый массив, состоящий из сопоставленных значений в том же порядке, в котором находились элементы оригинального массива.
trambalda
24.07.17 в 07:55
|
|
Ниже показано, как можно использовать метод 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]
Этот код создает словарь для сопоставления целых чисел с их англоязычными наименованиями, а также определяет массив, готовый для преобразования.
trambalda
24.07.17 в 07:58
|
|
Теперь можно использовать массив целых числе для создания массива строк передав в метод 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 } // типы строк выведены как [String] // со значением ["OneSix", "FiveEight", "FiveOneZero"]
Метод map(_:) вызывает замыкание для каждого элемента массива. Вам не нужно указывать тип входного параметра для замыкания number, так как тип выводится автоматически исходя из типа значений исходного массива.
trambalda
24.07.17 в 08:06
|
|
В этом примере переменная number инициализируется значением параметра замыкания number, поэтому её значение может быть изменено в теле замыкания (параметры в функциях и замыканиях являются константами). В записи замыкания указан тип возвращаемого значения String, для указания типа, который будет сохранен в сопоставляемом выходном массиве.
При каждом вызове, замыкание создает строку output, получает последнюю цифру числа, используя остаток от деления на 10, и использует это число для поиска подходящей строки в словаре digitNames. Подобное замыкание модно использовать для создания строкового представления любых положительных чисел.
trambalda
24.07.17 в 08:11
|
|
Обратите внимание
Получение значения словаря по ключу сопровождается восклицательным знаком, потому что значение возвращается в виде опционала, так как есть вероятность, что такого ключа в словаре найдено не будет. В примере гарантируется что результат number % 10 всегда дает существующий ключ, поэтому происходит принудительное развертывание опционала для получения строкового значения.
trambalda
24.07.17 в 08:18
|
|
Строка, полученная из словаря digitNames добавляется перед уже имеющимся значениtм в переменной output, таким образом строя результирующую строку в обратном порядке. Выражение number % 10 дает значение 6 для 16, 8 до 58, 0 для 510.
Затем переменная number вновь делится на 10. Так как это тип Int, то дробная часть отбрасывается и 16 становится 1, 58 становится 5, 510 становится 51.
В дальнейшем процесс повторяется пока number не станет равным нулю, на этой стадии замыкание возвращает строку и добавляется методом map(_:) в выходной массив.
Использование синтаксиса выносного замыкания добавляет аккуратности коду, так как размещается непосредственно после вызова функции, без необходимости охватывать скобками метода map(_:) все тело замыкания.
trambalda
24.07.17 в 08:24
|
|
Захват значений
Замыкание может захватить значение констант и переменных из контекста, в котором оно определено. Замыкание может ссылаться и модифицировать значение даже тех констант и переменных внутри своего тела, чья область видимости уже не существует.
В Swift простейшей формой замыкания, которое может захватывать значения является вложенная функция, записанная в теле другой функции. Вложенная функция может захватить любой из аргументов функции и любую переменную или константу, определенную внутри функции.
trambalda
24.07.17 в 09:50
|
|
Ниже пример функции makeIncrementer, содержащую вложенную функцию incrementer. Вложенная функция из окружающего контекста захватывает два значения - runningTotal и amount. После захвата этих значений, вложенная функция возвращается функцией makeIncrementer в качестве замыкания, которое увеличивает runningTotal каждый раз когда оно вызывается.
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer }
trambalda
24.07.17 в 09:53
|
|
Функция makeIncrementer возвращает тип () -> Int. Это означает, что она возвращает функцию, а не просто значение. У возвращаемой функции нет параметров, а она возвращает значение типа Int, каждый раз, когда она выполняется. Более подробно о том, как функции возвращают другие функции см. главу "Функции", раздел "Типы функций как возвращаемые значения".
Функция makeIncrementer(forIncrement:) имеет один параметр типа Int, с меткой forIncrement, и именем параметра amount. Значение аргумента передается в этот параметр, указывая, на сколько переменная runningTotal должна увеличиваться с каждым вызовом функции incrementer. Функция makeIncrementer определяет вложенную функцию incrementer, которая и осуществляет увеличение. Эта функция просто добавляет значение amount к runningTotal и возвращает результат.
trambalda
24.07.17 в 09:59
|
|
Если рассматривать вложенную функцию в отрыве от контекста, то она кажется бесполезной:
func incrementer() -> Int { runningTotal += amount return runningTotal }
Она не имеет параметров и ссылается на переменные runningTotal и amount, которые определены где-то вне ее тела. Она захватывает их значения из окружающей ее функции и использует их в своем теле. Получение доступа по ссылке гарантирует, что переменные runningTotal and amount не исчезнут когда вызов функции makeIncrementer завершится, а также гарантирует что runningTotal будет доступна при следующем вызове функции incrementer.
trambalda
24.07.17 в 10:04
|
|
Обратите внимание
Выполняя оптимизацию, Swift может вместо захвата и сохранения значения переменной может сделать копию значения, если значение в замыкании не изменяется, и если значение не изменяется после создания замыкания.
Swift также управляет всей памятью, участвуя в утилизации ненужных переменных.
trambalda
24.07.17 в 10:07
|
|
Ниже пример функции makeIncrementer в действии:
let incrementByTen = makeIncrementer(forIncrement: 10)
trambalda
24.07.17 в 10:08
|
|
Этот пример присваивает константе incrementByTen значение функции incrementer, которая увеличивает на 10 переменную runningTotal при каждом ее вызове. Результат вызова функции несколько раз показан ниже:
incrementByTen() // возвращает значение 10 incrementByTen() // возвращает значение 20 incrementByTen() // возвращает значение 30
trambalda
24.07.17 в 10:10
|
|
Если вы создадите вторую функцию incrementer, то она будет хранить свое значение runningTotal, не связанное с предыдущей:
let incrementBySeven = makeIncrementer(forIncrement: 7) incrementBySeven() // возвращает значение 7
Вызов первой функции incrementer (incrementByTen) продолжит увеличение своей runningTotal и никак не повлияет на значение в функции incrementBySeven:
incrementByTen() // возвращает значение 40
Обратите внимание
Если вы назначите замыкание свойству или методу экземпляра класса, и замыкание захватить значение этого экземпляра, ссылаясь на его члены, вы создадите сильные обратные связи (strong reference cycles). Для более подробной информации смотрите главу "Автоматический подсчет ссылок", раздел "Сильные обратные связи в замыканиях".
trambalda
24.07.17 в 10:16
|
|
Замыкания - это ссылочные типы
В примере, приведенном выше, incrementBySeven и incrementByTen являются константами, но замыкания этих констант способны изменять захваченные ими переменные runningTotal. Это происходит потому что функции и замыкания являются ссылочными типами.
При присвоении функции или замыкания константе или переменной, на самом деле вы присваиваете ссылку на функцию или замыкание. В примере выше, incrementByTen, ссылающееся на замыкание, является константой, но содержимое замыкания константой не является.
Это также означает, что присвоение замыкания двум разным переменным или константам, приведет к тому, что эти переменные или константы будут ссылаться на одно и то же замыкание:
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // возвращает значение 50
trambalda
24.07.17 в 10:41
|
|
Сбегающие замыкания
Замыкание, которое передается в функцию в качестве аргумента, но вызывается после возврата из функции, называют сбегающим замыканием. Для указания такого замыкания, нужно при объявлении функции указать атрибут @escaping, перед замыканием, которое должно быть сбегающим.
trambalda
24.07.17 в 10:51
|
|
Способ, при помощи которого может получиться сбегающее замыкание - это сохранить его в переменной, определенной вне этой функции. Например, много функций, запускающих асинхронные операции принимают аргументом замыкание в качестве завершающего обработчика. Функция, запустив операцию, завершается, а замыкание не будет вызвано, пока операция не будет завершена - в этом случае необходимо сбегающее замыкание, чтобы оно могло быть вызвано позже. Например:
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) }
Функция someFunctionWithEscapingClosure(_:) принимает замыкание в качестве аргумента и добавляет его в массив, который объявлен вне этой функции. Если вы не отметите параметр этой функции как @escaping, то компилятор выдаст ошибку.
trambalda
24.07.17 в 10:57
|
|
Пометка замыкания как @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"
trambalda
24.07.17 в 11:05
|
|
Автозамыкания
Автозамыкание это замыкание, которое обертывает выражение и передается в качестве аргумента функции. Оно не имеет собственных аргументов и при вызове возвращает значение выражения из которого оно состоит. Синтаксис позволяет не указывать фигурные скобки вокруг параметра функции, таким образом все выглядит как обычное выражение вместо явно указанного замыкания.
Функции, которые принимают автозамыкания являются обычным делом, но вот реализовывают такие функции не часто. Например, функция assert(condition:message:file:line:) принимает автозамыкания для параметров condition и message; параметр condition вычисляется только в отладочных билдах, а параметр message вычисляется только если condition возвращает false.
trambalda
24.07.17 в 13:08
|
|
Автозамыкания позволяют вам выполнять отложенные вычисления, так как код внутри автозамыкания не выполняется пока вы его не вызовите. Отложенные вычисления полезны, когда выполнение кода приводит к побочным эффектам или вычисления в коде ресурсозатратны. Пример ниже показывает, как автозамыкание выполняет отложенные вычисления.
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"
trambalda
24.07.17 в 13:14
|
|
Хотя первый элемент массива customersInLine удаляется в коде внутри замыкания, он не будет удален пока замыкание не будет выполнено. Если замыкание никогда не будет выполнено, то выражение внутри замыкания не будет вычислено, что означает что элемент массива не будет удален. Обратите внимание, что тип customerProvider не String, а () -> String, то есть функция без параметров, возвращающая строку.
trambalda
24.07.17 в 13:18
|
|
Можно получить такое же поведение отложенного вычисления, когда замыкание передается как аргумент функции.
// 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!"
trambalda
24.07.17 в 13:18
|
|
Функция serve(customer:) приведенная выше принимает явно указанное замыкание и возвращает имя заказчика. Функция serve(customer:) приведенная ниже выполняет ту же операцию, но вместо принятия явного замыкания, она принимает автозамыкание, которое отмечено атрибутом @autoclosure. Теперь можно вызывать функцию, как если бы она принимала тип String вместо замыкания. Этот аргумент будет автоматически преобразован в замыкание, потому что параметр функции помечен атрибутом @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!"
trambalda
24.07.17 в 13:26
|
|
Избыточное употребление автозамыканий может сделать код трудным для чтения. Контекст и имя функции должны способствовать улучшению понимания, что вычисление будет отложенным.
trambalda
24.07.17 в 13:28
|
|
Если требуется автозамыкание, которое одновременно будет и сбегающим замыканием, то нужно указать оба атрибута - @autoclosure и @escaping.
// 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!"
trambalda
24.07.17 в 13:29
|
|
В коде, что приведен выше, вместо вызова замыкания, которое передается в качестве аргумента customerProvider, функция collectCustomerProviders(_:) добавляет его в массив customerProviders. Массив объявлен вне зоны видимости функции, что означает, что замыкание будет выполнено после возврата из функции. Как результат, значение аргумента customerProvider должно быть доступно вне этой функции.
trambalda
24.07.17 в 13:38
|
|