How to use @autoclosure in Swift to improve performance
@autoclosure in Swift is a type of closure that allows to omit braces and make it look like a normal expression. Under the hood, however, it’s still a closure. By understanding what this means, we can improve the efficiency of our code.
The @autoclosure keyword might be new to you. For many of us, it’s hard to come up with use-cases for it. However, if you look closely, you’ll notice that it’s used in standard Swift APIs you’re using every day.
What is an @autoclosure?
It’s all in the name: @autoclosure automatically creates a closure from an argument passed to a function. Turning an argument into a closure allows us to delay the actual request of the argument.
Let’s explain this in more detail using the following code example. In this example, we’ve created a debugLog method and a Person struct which we’re going to print out:
Even-though we disabled debugging, the Person structure is still asked for its description. This is because the message argument of debugLog is directly computed.
We can solve this by making use of a closure:
The message() closure call is only called when debugging is enabled. You can see that we now need to pass in a closure argument to the debugLog method which doesn’t look so nice.
We can improve this code by making use of the @autoclosure keyword:
The logic within the debugLog method stays the same and still has to work with a closure. However, on the implementation level, we can now pass on the argument as if it were a normal expression. It looks both clean and familiar while we did optimize our debug logging code.
@autoclosure allows delaying an argument’s actual computing, just like we’ve seen before with lazy collections and lazy properties. In fact, if debugging is not enabled, we’re no longer computing debug descriptions while we did before!
Examples of standard Swift APIs using @autoclosure
Now that we know how @autoclosure works, we can briefly look at standard APIs using this keyword.
A common example is the assert(condition:message:file:line:) function. Its condition is only evaluated #if DEBUG is true and its message is only called if the condition failed. Both arguments are auto closures. In fact, many of the testing APIs use auto closures.
Lastly, there’s another blog post I wrote covering a useful example combining an @autoclosure with an autoreleasepool .
An @autoclosure can be a great solution to prevent unnecessary work if code isn’t actually used. On the implementation level, everything looks the same, while under the hood, we optimized our code.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Автозамыкания (autoclosures)
Автозамыкания — замыкания, которые автоматически создаются для заключения выражения, которое было передано в качестве аргумента функции, и оно не принимает никаких аргументов при вызове, оно возвращает значение выражения, которое заключено внутри нее. Автозамыкания позволяют вам откладывать вычисления, потому как код внутри них не исполняется, пока вы сами его не запустите. Это полезно для кода, который может иметь сторонние эффекты или просто является дорогим в вычислительном отношении, потому что вы можете контролировать время исполнения этого когда. Пример ниже отображает как замыкания откладывают вычисления:
Даже если первый элемент массива customersInLine удален в качестве части замыкания, то дальнейшее исполнение не поддерживается до тех пор, пока само замыкание фактически не будет вызвано. Если замыкание так и не вызывается, то выражение внутри него никогда и не вычисляется. Обратите внимание, что nextCustomer является не String, а () -> String, то есть функция не принимает аргументов, но возвращает строку. Вы получите то же самое поведение, когда сделаете это внутри функции:
Функция serveNextCustomer(_:) описанная выше принимает явное замыкание, которое возвращает имя следующего клиента. Следующая версия этой же функции ниже выполняет ту же самую операцию, но вместо использования явного замыкания, она использует автозамыкание, поставив маркировку при помощи атрибута @autoclosure. Теперь вы можете вызывать функцию, как будто бы она принимает аргумент String вместо замыкания.
Заметка
Слишком частое использование автозамыканий может сделать ваш код сложным для чтения. Контекст и имя функции должны обеспечивать ясность отложенности исполнения кода.
Атрибут @autoclosure предполагает наличие атрибута @noescape, который указывает на то, что замыкание используется только внутри функции. Что значит, что замыканию не позволяется храниться так, чтобы она могла “сбежать” из области видимости функции и исполниться после того, как функция уже возвратила значение. Если вы хотите, чтобы замыкание могло “сбежать”, то используйте другую форму атрибута — @autoclosure(escaping):
В коде выше, вместо того, чтобы вызывать переданное замыкание в качестве аргумента customer, функция collectCustomerClosures(_:) добавляет замыкание к массиву customerClosures. Массив объявлен за пределами функции, что означает, что замыкание в массиве может быть исполнено после того, как функция вернет значение. В результате значение аргумента customer должен иметь “разрешение” на “побег” из зоны видимости функции.
Для чего нужны автозамыкания swift

Замыкания — это самодостаточные блоки с определенным функционалом, которые могут быть переданы и использованы в вашем коде. Замыкания в Swift похожи на блоки в C и Objective-C, и лямбды в других языках программирования.
Замыкания могут захватывать и хранить ссылки на любые константы и переменные из контекста, в котором они объявлены. Эта процедура известна как заключение этих констант и переменных, отсюда и название «замыкание». Swift выполняет всю работу с управлением памятью при захвате за вас.
Заметка
Не волнуйтесь, если вы не знакомы с понятием «захвата»(capturing). Это объясняется более подробно ниже в главе Захват значений.
Глобальные и вложенные функции, которые были представлены в главе Функции, являются частным случаем замыканий. Замыкания принимают одну из трех форм:
- Глобальные функции являются замыканиями, у которых есть имя и которые не захватывают никакие значения.
- Вложенные функции являются замыканиями, у которых есть имя и которые могут захватывать значения из включающей их функции.
- Замыкающие выражения являются безымянными замыканиями, написанные в облегченном синтаксисе, которые могут захватывать значения из их окружающего контекста.
Замыкающие выражения в Swift имеют четкий, ясный, оптимизированный синтаксис в распространенных сценариях. Эти оптимизации включают:
- Вывод типа параметра и возврат типа значения из контекста
- Неявные возвращающиеся значения однострочных замыканий
- Сокращенные имена параметров
- Синтаксис последующих замыканий
Замыкающие выражения
Вложенные функции, которые были представлены в главе Вложенные функции, являются удобным способом для обозначения и объявления самоорганизованных блоков кода, которые являются частью более крупной функции. Тем не менее, иногда полезно писать короткие версии функциональных конструкций, без полного объявления и указания имени. Это особенно верно, когда вы работаете с функциями которые принимают другие функции в виде одного из своих параметров.
Замыкающие выражения, являются способом написания встроенных замыканий через краткий и специализированный синтаксис. Замыкающие выражения обеспечивают несколько синтаксических оптимизаций для написания замыканий в краткой форме, без потери ясности и намерений. Примеры замыкающих выражений ниже, показывают эти оптимизации путем рассмотрения метода sorted(by:) при нескольких итерациях, каждая из которых изображает ту же функциональность в более сжатой форме.
Метод sorted
В стандартной библиотеке Swift есть метод sorted(by:) , который сортирует массив значений определенного типа, основываясь на результате сортирующего замыкания, которые вы ему передадите. После завершения процесса сортировки, метод sorted(by:) возвращает новый массив того же типа и размера как старый, с элементами в правильном порядке сортировки. Исходный массив не изменяется методом sorted(by:) .
Примеры замыкающих выражений ниже используют метод sorted(by:) для сортировки массива из String значений в обратном алфавитном порядке. Вот исходный массив для сортировки:
Замыкание метода sorted(by:) принимает два аргумента одного и того же типа, что и содержимое массива, и возвращает Bool значение, которое решает поставить ли первое значение перед вторым, или после второго. Замыкание сортировки должно вернуть true , если первое значение должно быть до второго значения, и false в противном случае.
Этот пример сортирует массив из String значений, так что сортирующее замыкание должно быть функцией с типом (String, String) -> Bool .
Один из способов обеспечить сортирующее замыкание, это написать нормальную функцию нужного типа, и передать ее в качестве аргумента метода sorted(by:) :
Если первая строка ( s1 ) больше чем вторая строка ( s2 ), функция backward(_:_:) возвращает true , что указывает, что s1 должна быть перед s2 в сортированном массиве. Для символов в строках, «больше чем» означает «появляется в алфавите позже, чем». Это означает что буква «B» «больше чем» буква «А» , а строка «Tom» больше чем строка «Tim» . Это делает обратную алфавитную сортировку, с «Barry» поставленным перед «Alex» , и так далее.
Тем не менее, это довольно скучный способ написать то, что по сути, является функцией с одним выражением ( a > b ). В этом примере, было бы предпочтительнее написать сортирующее замыкание в одну строку, используя синтаксис замыкающего выражения.
Синтаксис замыкающего выражения
Синтаксис замыкающего выражения имеет следующую общую форму:
Синтаксис замыкающего выражения может использовать сквозные параметры. Значения по умолчанию не могут быть переданы. Вариативные параметры могут быть использованы в любом месте в списке параметров. Кортежи также могут быть использованы как типы параметров и как типы возвращаемого значения.
Пример ниже показывает версию функции backward(_:_:) с использованием замыкающего выражения:
Обратите внимание, что объявление типов параметров и типа возвращаемого значения для этого однострочного замыкания идентично объявлению из функции backward(_:_:) . В обоих случаях, оно пишется в виде (s1: String, s2: String) -> Bool . Тем не менее, для однострочных замыкающих выражений, параметры и тип возвращаемого значения пишутся внутри фигурных скобок, а не вне их.
Начало тела замыкания содержит ключевое слово in . Это ключевое слово указывает, что объявление параметров и возвращаемого значения замыкания закончено, и тело замыкания вот-вот начнется.
Поскольку тело замыкания настолько короткое, оно может быть записано в одну строку:
Это показывает, что общий вызов метода sorted остался прежним. Пара скобок по-прежнему обособляют весь набор параметров метода.
Определение типа из контекста
Поскольку сортирующее замыкание передается как аргумент метода, Swift может вывести типы его параметров и тип возвращаемого значения, через тип параметра метода sorted(by:) . Этот параметр ожидает функцию имеющую тип (String, String) -> Bool . Это означает что типы (String, String) и Bool не нужно писать в объявлении замыкающего выражения. Поскольку все типы могут быть выведены, стрелка результата ( -> ) и скобки вокруг имен параметров также могут быть опущены:
Всегда можно определить типы параметров и тип возвращаемого значения, когда мы передаем замыкание функции в виде однострочного замыкающего выражения. В результате, когда замыкание используется как аргумент метода, вам никогда не нужно писать однострочное замыкание в его полном виде.
Тем не менее, вы всё равно можете явно указать типы, если хотите. И делать это предполагается, если это поможет избежать двусмысленности для читателей вашего кода. В случае с методом sorted(by:) , цель замыкания понятна из того факта, что сортировка происходит, и она безопасна для читателя, который может предположить, что замыкание, вероятно, будет работать со значениями String , поскольку оно помогает сортировать массив из строк.
Неявные возвращаемые значения из замыканий с одним выражением
Замыкания с одним выражением могут неявно возвращать результат своего выражения через опускание ключевого слова return из их объявления, как показано в этой версии предыдущего примера:
Здесь, функциональный тип аргумента метода sorted(by:) дает понять, что замыкание вернет Bool значение. Поскольку тело замыкания содержит одно выражение ( s1 > s2 ), которое возвращает Bool значение, то нет никакой двусмысленности, и ключевое слово return можно опустить.
Сокращенные имена аргументов
Swift автоматически предоставляет сокращённые имена для однострочных замыканий, которые могут быть использованы для обращения к значениям аргументов замыкания через имена $0 , $1 , $2 , и так далее.
Если вы используете эти сокращенные имена параметров с вашим замыкающим выражением, вы можете пропустить список параметров замыкания из его объявления, а количество и тип сокращенных имен параметров будет выведено из ожидаемого типа метода. Ключевое слово in также может быть опущено, поскольку замыкающее выражение полностью состоит из его тела:
Здесь, $0 и $1 обращаются к первому и второму String параметру замыкания.
Операторные функции
Здесь есть на самом деле более короткий способ написать замыкающее выражение выше. Тип String в Swift определяет свою специфичную для строк реализацию оператора больше ( > ) как функции, имеющей два строковых параметра и возвращающей значение типа Bool . Это точно соответствует типу метода, для параметра метода sorted(by:) . Таким образом, вы можете просто написать оператор больше, а Swift будет считать, что вы хотите использовать специфичную для строк реализацию:
Более подробную информацию о операторных функциях смотрите в разделе Операторные функции.
Последующее замыкание
Если вам нужно передать замыкающее выражение в качестве последнего аргумента функции и само выражение замыкания длинное, то оно может быть записано в виде последующего замыкания. Последующее замыкание — замыкание, которое записано в виде замыкающего выражения вне (и после) круглых скобок вызова функции, даже несмотря на то, что оно все еще является аргументом функции. Когда вы используете синтаксис последующего замыкания, то вы не должны писать ярлык аргумента замыкания в качестве части вызова самой функции. Функция может включать в себя несколько последующих замыканий, однако, первые несколько примеров используют по одному последующему замыканию:
Сортирующее строки замыкание из раздела Синтаксис замыкающего выражения может быть записано вне круглых скобок функции sorted(by:) , как последующее замыкание:
Если выражение замыкания является единственным аргументом функции, и вы пишете его используя синтаксис последующего замыкания, то вы можете опустить написание круглых скобок вызова самой функции после ее имени.
Последующие замыкания полезны в случаях, когда само замыкание достаточно длинное, и его невозможно записать в одну строку. В качестве примера приведем вам метод map(_:) типа Array в языке Swift, который принимает выражение замыкания как его единственный аргумент. Замыкание вызывается по одному разу для каждого элемента массива и возвращает альтернативную отображаемую величину (возможно другого типа) для этого элемента. Природа отображения и тип возвращаемого значения определяется замыканием.
После применения замыкания к каждому элементу массива, метод map(_:) возвращает новый массив, содержащий новые преобразованные величины, в том же порядке, что и в исходном массиве.
Вот как вы можете использовать метод map(_:) вместе с последующим замыканием для превращения массива значений типа Int в массив типа String . Массив [16, 58, 510] используется для создания нового массива [«OneSix», «FiveEight», «FiveOneZero»] :
Код выше создает словарь отображающий цифры и их английскую версию имен. Так же он объявляет массив целых значений для преобразования в массив строк.
Вы можете использовать массив numbers для создания значений типа String , передав замыкающее выражение в метод map(_:) массива в качестве последующего замыкания. Обратите внимание, что вызов numbers.map не включает в себя скобки после map , потому что метод map(_:) имеет только один параметр, который мы имеем в виде последующего замыкания:
Метод map(_:) вызывает замыкание один раз для каждого элемента массива. Вам не нужно указывать тип входного параметра замыкания, number , так как тип может быть выведен из значений массива, который применяет метод map .
В этом примере переменная number инициализирована при помощи значения параметра замыкания number , так что значение может быть изменено внутри тела замыкания. (Параметры функций и замыкания всегда являются константами.) Выражение замыкания так же определяет возвращаемый тип String для указания типа, который будет храниться в массиве на выходе из метода map(_:) .
Замыкающее выражение строит строку, названную output , каждый раз, когда оно вызывается. Оно рассчитывает последнюю цифру number , используя оператор деления с остатком ( number % 10 ) и использует затем эту получившуюся цифру, чтобы найти соответствующую строку в словаре digitNames . Это замыкание может быть использовано для создания строкового представления любого целого числа, большего чем 0.
Заметка
Вызов словаря digitNames синтаксисом сабскрипта сопровождается знаком ( ! ), потому что сабскрипт словаря возвращает опциональное значение, так как есть такая вероятность, что такого ключа в словаре может и не быть. В примере выше мы точно знаем, что number % 10 всегда вернет существующий ключ словаря digitNames , так что восклицательный знак используется для принудительного извлечения значения типа String в возвращаемом опциональном значении сабскрипта.
Строка, полученная из словаря digitNames , добавляется в начало переменной output , путем правильного формирования строковой версии числа наоборот.(Выражение number % 10 дает нам 6 для 16 , 8 для 58 и 0 для 510 ).
Переменная number после вычисления остатка делится на 10 . Так как тип значения Int , то наше число округляется вниз, таким образом 16 превращается в 1 , 58 в 5 , 510 в 51 .
Процесс повторяется пока number /= 10 не станет равным 0 , после чего строка output возвращается замыканием и добавляется к выходному массиву функции map(_:) .
Использование синтаксиса последующих замыканий в примере выше аккуратно инкапсулирует функциональность замыкания сразу после функции map(_:) , которой замыкание помогает, без необходимости заворачивания всего замыкания внутрь внешних круглых скобок функции map(_:) .
Если функция принимает несколько последующих замыканий, вы можете пропустить ярлык параметра для первого из них, а для остальных уже указать нужно. Например, функция ниже загружает изображение в фотогалерею:
Когда вы вызываете эту функцию для загрузки изображений, вы используете два замыкания. Первое замыкание — это обработчик, который отображает изображение после успешной загрузки. Второе замыкание — обработчик, который отображает ошибку пользователю, если произошла ошибка во время загрузки.
В этом примере метод loadPicture(from:completion:onFailure:) передает свою сетевую задачу в фоновый поток и вызывает одно из замыканий, когда сетевая задача выполнена. Написание функции таким способом позволяет вам разделять код, который ответственен за обработку ошибки во время сетевой задачи от кода, который отвечает за успешную загрузку.
Захват значений
Замыкания могут захватывать константы и переменные из окружающего контекста, в котором они объявлены. После захвата замыкание может ссылаться или модифицировать значения этих констант и переменных внутри своего тела, даже если область, в которой были объявлены эти константы и переменные уже больше не существует.
В Swift самая простая форма замыкания может захватывать значения из вложенных функций, написанных внутри тела других функций. Вложенная функция может захватить любые значения из аргументов окружающей ее функции, а так же константы и переменные, объявленные внутри тела внешней функции.
Вот пример функции makeIncrementer , которая содержит вложенную функцию incrementer . Вложенная функция incrementer() захватывает два значения runningTotal и amount из окружающего контекста. После захвата этих значений incrementer возвращается функцией makeIncrementer как замыкание, которое увеличивает runningTotal на amount каждый раз как вызывается.
Возвращаемый тип makeIncrementer Void -> Int . Это значит, что он возвращает функцию, а не простое значение. Возвращенная функция не имеет параметров и возвращает Int каждый раз как ее вызывают. Узнать как функции могут возвращать другие функции можно в главе «Функциональные типы как возвращаемые типы».
Функция makeIncrementer(forIncrement:) объявляет целочисленную переменную runningTotal , для хранения текущего значения инкрементора, которое будет возвращено. Переменная инициализируется значением 0 .
Функция makeIncrementer(forIncrement:) имеет единственный параметр Int с внешним именем forIncrement и локальным именем amount . Значение аргумента передается этому параметру, определяя на сколько должно быть увеличено значение runningTotal каждый раз при вызове функции.
Функция makeIncrementer объявляет вложенную функцию incrementer , которая непосредственно и занимается увеличением значения. Эта функция просто добавляет amount к runningTotal и возвращает результат.
Если рассматривать функцию incrementer() отдельно, то она может показаться необычной:
Функция incrementer() не имеет ни одного параметра и она ссылается на runningTotal и amount внутри тела функции. Она делает это, захватывая существующие значения от runningTotal и amount из окружающей функции и используя их внутри. Захват ссылки дает гарантию того, что runningTotal и amount не исчезнут при окончании вызова makeIncrementer и гарантирует, что runningTotal останется переменной в следующий раз, когда будет вызвана функция incrementer() .
Заметка
В качестве оптимизации Swift может захватить и хранить копию значения, если это значение не изменяется самим замыканием, а так же не изменяется после того, как замыкание было создано. Swift также берет на себя управление памятью по утилизации переменных, когда они более не нужны.
Приведем пример makeIncrementer в действии:
Этот пример заставляет константу incrementByTen ссылаться на функцию инкрементора, которая добавляет 10 к значению переменной runningTotal каждый раз как вызывается. Многократный вызов функции показывает ее в действии:
Если вы создаете второй инкрементор, он будет иметь свою собственную ссылку на новую отдельную переменную runningTotal :
Повторный вызов первоначального инкрементора ( incrementByTen ) заставит увеличиваться его собственную переменную runningTotal и никак не повлияет на переменную, захваченную в incrementBySeven :
Заметка
Если вы присваиваете замыкание свойству экземпляра класса, и замыкание захватывает этот экземпляр по ссылке на него или его члены, вы создаете сильные обратные связи между экземпляром и замыканием. Swift использует списки захвата, для разрыва этих сильных обратных связей. Подробнее можно прочитать в главе Циклы сильных ссылок для замыканий.
Замыкания — ссылочный тип
В примере выше incrementBySeven и incrementByTen константы, но замыкания, на которые ссылаются эти константы имеют возможность увеличивать значение переменных runningTotal , которые они захватили. Это из-за того, что функции и замыкания являются ссылочными типами.
Когда бы вы ни присваивали функцию или замыкание константе или переменной, вы фактически присваиваете ссылку этой константе или переменной на эту функцию или замыкание. В примере выше выбор замыкания, на которое ссылается incrementByTen , константа, но не содержимое самого замыкания.
Это так же значит, что если вы присвоите замыкание двум разным константам или переменным, то оба они будут ссылаться на одно и то же замыкание:
Пример выше показывает, что вызов alsoIncrementByTen то же самое, что и вызов incrementByTen . Потому что и та и другая функция ссылаются на одно и то же замыкание: и то, и другое замыкание возвращают один и тот же runningTotal .
Сбегающие замыкания
Когда говорят, что замыкание сбегает из функции, то это значит, что это замыкание было передано в функцию в качестве аргумента и вызывается уже после того, как функция вернула значение. Когда вы объявляете функцию, которая имеет замыкание в качестве одного из параметров, то вы пишете @escaping до типа параметра, для того чтобы указать, что замыкание может сбежать.
Если замыкание хранится в переменной, которая была объявлена вне функции, а затем эта переменная была передана в качестве аргумента в функцию, то получается, что замыкание, которое посредством переменной передается в функцию, сбегающее. В качестве примера можно рассмотреть функции, которые выполняют асинхронные операции в завершающем обработчике, который является замыканием. То есть получается, что функция завершает свою работу, после чего вызывается завершающий обработчик. Или другими словами обработчик не вызывается, пока не завершится работа функции, таким образом получается, что данному замыканию нужно сбежать из области работы функции, чтобы отработать позже. Например:
Функция someFunctionWithEscapingClosure(_:) принимает и добавляет в массив замыкание, объявленное за пределами функции. Если вы не поставите маркировку @escaping , то получите ошибку компиляции.
Сбегающее замыкание, которое имеет ссылку на self требует отдельного рассмотрения, если self ссылается на экземпляр класса. Захватывая self в сбегающем замыкании, вы можете случайно создать зацикленность сильных ссылок. Для дополнительной информации читайте раздел «Автоматический подсчет ссылок».
Обычно замыкание захватывает переменные неявно, просто используя их внутри тела, но в случае с self вам нужно делать это явно. Если вы хотите захватить self , напишете self явно, когда используете его, или включите self в лист захвата замыкания. Когда вы пишете self явно, вы явно указываете свое намерение, а так же помогаете сами себе тем, что напоминаете проверить наличие цикла сильных ссылок. Например, в коде ниже замыкание переданное в метод someFunctionWithEscapingClosure(_:) ссылается на self явно. А вот замыкание, переданное в метод someFunctionWithNonescapingClosure(_:) является несбегающим, что значит, что оно может ссылаться на self неявно.
Ниже приведена версия doSomething(), которая захватывает self, включая его в лист захвата замыкания, а затем неявно ссылается на него:
Если self является экземпляром структуры или перечисления, то вы можете всегда ссылаться на self неявно. Однако, сбегающие замыкания не могут захватить изменяемую ссылку на self , когда self является экземпляром структуры или перечисления. Структуры и перечисления не допускают общей изменчивости, как это обсуждалось в главе «Структуры и перечисления — типы значения»
Вызов функции someFunctionWithEscapitngClosure в примере выше вызовет ошибку, так как находится замыкание внутри mutable метода, таким образом self так же получается изменяемым (mutable). Ошибка получается из-за того, что мы нарушаем правило, которое гласит, что в структурах сбегающие замыкания не могут захватывать изменяемую ссылку на self.
Автозамыкания (autoclosures)
Автозамыкания — замыкания, которые автоматически создаются для заключения выражения, которое было передано в качестве аргумента функции. Такие замыкания не принимают никаких аргументов при вызове и возвращают значение выражения, которое заключено внутри нее. Синтаксически вы можете опустить круглые скобки функции вокруг параметров функции, просто записав обычное выражение вместо явного замыкания.
Нет ничего необычного в вызове функций, которые принимают автозамыкания, но необычным является реализовывать такие функции. Например, функция assert(condition:message:file:line:) принимает автозамыкания на место condition и message параметров. Ее параметр condition вычисляется только в сборке дебаггера, а параметр message вычисляется, если только condition равен false .
Автозамыкания позволяют вам откладывать вычисления, потому как код внутри них не исполняется, пока вы сами его не запустите. Это полезно для кода, который может иметь сторонние эффекты или просто является дорогим в вычислительном отношении, потому что вы можете контролировать время исполнения этого кода. Пример ниже отображает как замыкания откладывают вычисления:
Даже если первый элемент массива customersInLine удаляется кодом внутри замыкания, элемент массива фактически не удаляется до тех пор, пока само замыкание не будет вызвано. Если замыкание так и не вызывается, то выражение внутри него никогда не выполнится и, соответственно, элемент не будет удален из массива. Обратите внимание, что customerProvider является не String , а () -> String , то есть функция не принимает аргументов, но возвращает строку. Вы получите то же самое поведение, когда сделаете это внутри функции:
Функция serve(customer:) описанная выше принимает явное замыкание, которое возвращает имя клиента. Версия функции serve(customer:) ниже выполняет ту же самую операцию, но вместо использования явного замыкания, она использует автозамыкание, поставив маркировку при помощи атрибута @autoclosure . Теперь вы можете вызывать функцию, как будто бы она принимает аргумент String вместо замыкания. Аргумент автоматически преобразуется в замыкание, потому что тип параметра customerProvider имеет атрибут @autoclosure .
Заметка
Слишком частое использование автозамыканий может сделать ваш код сложным для чтения. Контекст и имя функции должны обеспечивать ясность отложенности исполнения кода.
Если вы хотите чтобы автозамыкание могло сбежать, то вам нужно использовать оба атрибута и @autoclosure , и @escaping . Атрибут @escaping подробнее описан в главе Сбегающие замыкания.
В коде выше, вместо того, чтобы вызывать переданное замыкание в качестве аргумента customer , функция collectCustomerProviders(_:) добавляет замыкание к массиву customerProviders . Массив объявлен за пределами функции, что означает, что замыкание в массиве может быть исполнено после того, как функция вернет значение. В результате значение аргумента customerProvider должен иметь “разрешение” на “побег” из зоны видимости функции.
Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.
Все, что вы хотели знать о замыканиях в Swift

Многие языки программирования имеют функцию, которая включает использование анонимной функции, которая содержит блок кода, выполняющий определенную задачу — например, лямбды в C # и Python.
Например, в следующем фрагменте кода Python мы используем лямбда-выражение, выделенное жирным шрифтом, в функции sorted() для сортировки списка словарей с использованием указанного ключа (как показано в предыдущей статье).
Определение закрытия
Как современный язык, Swift имеет аналогичную концепцию анонимной функции, называемую закрытием, которая служит этой цели. Официальная документация определяет закрытие следующим образом:
«Замыкания — это автономные блоки функциональности, которые можно передавать и использовать в вашем коде».
Стоит выделить несколько ключевых фраз. Не беспокойтесь, если эти моменты вам непонятны, поскольку в следующем разделе мы увидим несколько распространенных примеров в Swift.
- «Автономные функциональные блоки» означает, что закрытие в целом предназначено для выполнения полностью определенной задачи или функции в рамках своей области.
- «Передавать и использовать» означает, что замыкание можно использовать в качестве параметра в функции, которую мы можем указать при вызове этой функции.
По сравнению с другими языками программирования замыкания в Swift шире в том смысле, что они представляют собой нечто большее, чем просто анонимные функции как часть вызова функции. Вот краткий обзор доступных типов закрытия в Swift.
Как показано в приведенной выше таблице, в Swift есть три типа замыканий, а именно глобальные функции, вложенные функции и выражения замыкания. Они различаются по нескольким аспектам, включая области применения, их имена и то, фиксируют ли они значения, что будет более подробно обсуждено в следующем разделе.
Поскольку глобальные и вложенные функции более или менее похожи на обычную функцию, которая не является фокусом данной части, остальные разделы будут сосредоточены только на закрывающих выражениях (если не указано иное).
Встроенные застежки
Во многих случаях нам не нужно писать закрытие в наших собственных проектах. Но нетривиальное количество функций в стандартной библиотеке, а также в некоторых распространенных фреймворках (например, UIKit ) используют замыкания. В следующем фрагменте кода показано использование метода map(_:) для массива и метода present(_:animated:completion:) для UIViewController .
Как вы могли заметить, код, заключенный в фигурные скобки в строках 3 и 10-12, является закрытием. У тех, кто плохо знаком с закрытием Swift, может возникнуть несколько вопросов.
- Почему при вызове метода map(_:) отсутствуют круглые скобки?
- Что означает знак доллара ( $ )?
- Почему закрытие находится за пределами круглых скобок в методе present(_:animated:completion:) ?
Обзор синтаксиса
Общая форма
Общая форма закрытия показана во фрагменте кода ниже. В частности, есть четыре ключевых компонента.
- Параметры: параметры в закрытии (называемые parameters , как указано выше) аналогичны параметрам в функции. Замыкание может иметь ноль или несколько параметров, и они могут быть стандартными типами данных, такими как целые числа, строки или настраиваемые структуры и классы, а также кортежи.
- Тип возвращаемых данных: нам нужно указать тип возвращаемых данных (называемый returned , как указано выше) для данного замыкания. Как и параметры, тип данных, возвращаемых замыканием, также является гибким — допустимы как стандартные, так и настраиваемые типы данных. Кроме того, Void также является допустимым типом возвращаемых данных, что означает, что тело замыкания не должно ничего возвращать.
- Ключевое слово in: Ключевое слово in используется после определения параметров и возвращаемого типа данных, но перед закрывающим телом.
- Тело закрытия. В теле закрытия (называемом statements , как указано выше) мы пишем код, который использует parameters и генерирует данные returned .
Укороченные формы
Заметили вы это или нет во фрагменте «Встроенные замыкания в Swift», ни метод map(_:) , ни present(_:animated:completion:) не использует общую форму.
Например, ни один из них не имеет ключевого слова in и не возвращает никаких данных. Но почему они по-прежнему являются действительными замыканиями без синтаксических ошибок?
Этот вопрос подводит нас к сокращенным формам укупорочных средств. Собственно, есть несколько способов сократить замыкание в общем виде. Используя приведенный выше метод map(_:) в качестве примера в демонстрационных целях, давайте сначала напишем этот метод в общей форме.
Как и следовало ожидать, у нас есть все четыре элемента в полном закрытии. У параметров есть один, тип данных String . Тип возвращаемых данных также String . Мы используем ключевое слово in , чтобы ввести тело закрытия, которое возвращает String в верхнем регистре.
Сценарий 1. Неявный возвращаемый тип данных
Поскольку операторы в теле замыкания являются явными с точки зрения его возвращаемых данных, мы можем опустить тип возвращаемых данных в замыкании. Другими словами, компилятор определит возвращаемый тип из тела замыкания.
Сценарий 2. Неявный тип данных параметра
Поскольку массив относится к типу String , элемент массива может быть выведен как String . Таким образом, мы можем пойти дальше, опустив тип параметра, как показано ниже:
Поскольку параметр не имеет спецификации с точки зрения его типа, мы можем опустить круглые скобки.
Сценарий 3. Пропуск return ключевого слова
Другой особый случай — это когда тело замыкания представляет собой всего лишь одно выражение. Фактически вы можете опустить ключевое слово return и поместить тело закрытия сразу после ключевого слова in .
Сценарий 4. Короткие аргументы в пользу мам
Одна интересная особенность использования замыканий в Swift — это набор сокращенных имен аргументов, которые будут автоматически созданы для встроенных замыканий . В частности, вы используете число с префиксом знака доллара, например $0 , $1 и $2 , для ссылки на первый, второй и третий параметры соответственно.
Когда используются эти сокращенные имена аргументов, мы можем опустить параметры замыкания, а также их типы данных, потому что все они могут быть выведены из ожидаемого типа функции. Кроме того, мы можем опустить ключевое слово in , так что замыкание будет полностью состоять из его тела, как показано ниже:
Сценарий 5. Отслеживание закрытия
Как вы, возможно, заметили, после выполнения описанных выше шагов закрытие почти идентично тому, что показано в примере, приведенном в начале этой статьи. Единственное отличие — пара скобок. Почему допускается их пропуск?
Опять же, это еще одна удобная функция закрытия в Swift. Когда замыкание должно быть передано в качестве последнего аргумента функции, мы можем опустить круглые скобки и вместо этого записать замыкание как завершающее замыкание.
По сути, замыкающее замыкание состоит из тела замыкания, которое следует непосредственно за круглыми скобками функции. Кроме того, если замыкание является единственным аргументом в функции, мы можем опустить круглые скобки, как мы это делали для метода map(_:) в начале. Более того, при использовании синтаксиса замыкающего замыкания мы также опускаем метку аргумента для замыкания.
Ключевые идеи
В предыдущем разделе рассматривается синтаксис замыканий. Есть несколько ключевых концепций, которые нам необходимо понять, прежде чем мы сможем правильно использовать замыкания в наших проектах.
1. Получение ценности
Первая концепция называется захватом значений . Он относится к экземпляру, когда используется замыкание, и захватывает значения используемых констант и переменных из окружающего контекста, в котором оно определено.
Подразумевается, что даже если константы и переменные из окружающего контекста замыкания стали недоступны или изменены, замыкание все еще может работать со значениями этих аргументов в пределах своей области действия.
Чтобы показать вам быстрый пример сбора значений, давайте сначала создадим функцию с именем visitCountry() , которая возвращает замыкание. Мы можем присвоить возвращенное замыкание переменной с именем helloWorld . Когда мы повторяем массив countries , мы можем передать каждый из элементов замыканию helloWorld , которое напечатает указанный оператор в теле замыкания.
Что, если мы хотим записать количество посещенных стран? Давайте немного изменим код, чтобы добиться этого.
Некоторые могут ошибочно подумать, что переменная счетчика numberOfCountriesVisited всегда будет иметь значение 1 . Однако из-за захвата значения замыкания его значение продолжает расти на единицу каждый раз, когда мы вызываем замыкание. Другими словами, закрытие helloWorld фиксирует значение переменной numberOfCountriesVisited .
Чтобы еще раз показать вам, что это так, мы можем создать другое замыкание под названием helloAsia из функции visitCountry() . Следующий фрагмент кода демонстрирует тот же феномен захвата значения. Следует отметить, что поскольку helloAsia — это не закрытие helloWorld , у него есть собственный счетчик для отслеживания количества посещенных стран. Это объясняет, почему переменная счетчика numberOfCountriesVisited начинается с 1 для нового закрытия.
2. Тип ссылки
Еще одна ключевая концепция правильного использования замыканий — замыкания — это ссылочные типы, как и функции. На самом деле это основная причина, по которой вызов закрытия helloWorld приведет к увеличению переменной счетчика. Поскольку замыкание helloWorld является ссылочным типом, его многократный вызов увеличивает значение одного и того же счетчика, захваченного замыканием.
Чтобы показать вам, что закрытие является справочным, мы можем попробовать приведенный ниже код после приведенного выше фрагмента кода для азиатских стран. Просто мы присваиваем helloAsia другой переменной с именем visitAsia . Неудивительно, что когда мы вызываем закрытие visitAsia , оно меняет счетчик на 4 . И helloAsia , и visitAsia — это одно и то же замыкание, и они захватывают один и тот же счетчик, значение которого было 3 .
3. Побег
Еще одна важная особенность замыканий Swift — это возможность экранирования функции, когда замыкание передается в качестве аргумента. Чтобы объявить закрытие, которое можно избежать, мы используем ключевое слово @escaping перед закрытием. Вот краткий пример функции, включающей избегающее замыкание.
Приведенный выше фрагмент кода показывает очень распространенный вариант использования экранирующего закрытия. В частности, код используется для выборки некоторых данных в фоновом потоке. Когда это будет сделано, мы сможем соответствующим образом обновить пользовательский интерфейс.
Когда вы смотрите на напечатанные операторы, интересно отметить, что “just a log statement” печатается перед оператором “data fetched” . Это действительно ожидаемое поведение. Просто потому, что функция fetchDataRemotely(_:) сначала возвращает неявные Void данные, прежде чем она начнет операцию закрытия. Другими словами, закрытие completionHandler не вызывается, пока функция fetchDataRemotely(_:) не будет завершена.
Заключение
В этом руководстве мы узнали, что такое замыкания в Swift.
В частности, мы рассмотрели синтаксис замыканий и обсудили некоторые важные концепции, которые необходимы для понимания правильного использования замыканий в ваших проектах.
Замыкания в Swift: полное руководство по использованию
Замыкания — это автономные блоки функциональности, которые можно передавать и использовать в вашем коде.
Иными словами, замыкание — это блок кода, который вы можете присвоить переменной. Затем вы можете передать его в своем коде, например, в другую функцию.
Давайте посмотрим на аналогию:
- Боб говорит Алисе: «Помаши руками!» Алиса слышит инструкцию и машет руками. Размахивание руками — это функция, которую Боб вызвал напрямую.
- Алиса записывает свой возраст на листе бумаги и передает его Бобу. Лист бумаги является переменной. Алиса использовала лист бумаги для сохранения данных.
- Боб пишет: «Помашите руками!» на листе бумаги и дает его Алисе. Алиса читает инструкцию на листе бумаги и машет руками. Инструкция, переданная на листе бумаги, является замыканием.
Позже мы можем отправить этот лист бумаги по почте или сказать Алисе, чтобы она прочитала его после того, как закончит выполнять другое задание. Это одно из преимуществ использования замыканий.
- В первой строке мы определяем замыкание и назначаем его константе birthday. Замыкание — это код между фигурными скобками. Обратите внимание, что замыкание назначается birthday с помощью оператора присваивания =.
- Когда вы запускаете код, замыкание вызывается с помощью синтаксиса birthday(). То есть мы используем имя константы birthday с круглыми скобками (). Это похоже на вызов функции.
Как и функции, замыкания могут иметь параметры.
- Как и раньше, мы объявляем замыкание в первой строке, затем назначаем его константе birthday и вызываем замыкание в последней строке.
- Замыкание теперь имеет один параметр типа String. Этот параметр объявлен как тип замыкания — (String) -> ()
- Затем вы можете использовать параметр name в замыкании. При вызове замыкания мы указываем значение параметра.
Здесь важны три вещи:
- Тип замыкания — (String) -> ().
- Код замыкания — .
- Вызов замыкания — birthday(···).
Параметры замыкания не имеют названий в отличие от функций. Когда вы объявляете замыкание, вы можете указать типы параметров, которые у него есть, например, String. В коде замыкания вы назначаете локальную переменную первому параметру. Это дает параметру имя в замыкании.
Мы можем опустить имя переменной и использовать сокращение для первого параметра — $0:
В приведенном выше коде замыкание birthday имеет один параметр. Внутри замыкания сокращение используется для ссылки на значение первого параметра — $0.
Типы замыканий
Каждое замыкание имеет тип, как и любая другая переменная или константа.
Мы объявляем замыкание с одним параметром следующим образом:
Замыкание имеет один параметр типа и возвращает (). Первый параметр замыкания также принимает данный тип (name: String) -> (). Ключевое слово in отделяет параметры замыкания от кода.
Синтаксис для типа замыкания состоит из типов параметров и типа возвращаемого значения:
- (Int, Int) -> Double — имеет 2 параметра Double и возвращает значение Double.
- () -> Int — не имеет параметров и возвращает целое число.
- (String) -> String — принимает строку и возвращает строку.
Давайте рассмотрим код самого замыкания:
Тип замыкания объявляется несколько иначе и включает имена параметров. Посмотрим на несколько примеров:
- (Int, Int) -> Double становится
- () -> Int становится
- (String) -> String становится
Вы можете называть эти параметры как угодно, но вам нужно будет дать им имена. Эти константы могут использоваться «локально» внутри замыкания по аналогии с параметрами внутри тела функции.
in проще всего воспринимать как «У нас есть параметры X, Y, Z в блоке кода, который является замыканием».
Замыкание без параметров и без возвращаемого значения имеет следующий тип:
Вы также можете использовать Void в качестве типа возвращаемого значения. В Swift Void означает «ничего»:
Посмотрим на последний пример:
Замыкание greeting имеет два параметра типа String. Также замыкание возвращает значение типа String. Тип greeting явно определен, как и параметры замыкания.
Когда вызывается замыкание, ему предоставляется два аргумента типа String, и его возвращаемое значение присваивается text, а затем выводится на консоль.
Замыкания и вывод типа
Swift может сам выводить типы. Когда вы не указываете явно тип переменной, Swift может самостоятельно определить, какой тип у данной переменной. Это зависит от контекста вашего кода.
Swift выводит тип age на основании контекста. 104 — это значение для целого числа, так что константа age имеет тип Int. Swift выясняет это самостоятельно без необходимости явно указывать тип.
Вывод типа часто используется в замыканиях. В результате вы можете опустить часть кода для замыкания.
Мы создаем массив с именами, а затем сортируем их в алфавитном порядке, вызывая функцию sorted(by:). Параметр by: принимает замыкание <, которое используется для сортировки массива.
Рассмотрим полный код замыкания:
Здесь используется полный синтаксис замыкания, включая два имени параметра s1 и s2 типа String, а также тип возвращаемого значения Bool. Мы также используем ключевые слова in и return .
Этот код можно сократить:
Здесь пропускаются типы параметров замыкания, поскольку они могут быть выведены из контекста. Поскольку мы сортируем массив строк, эти два параметра выводятся как String. Опуская типы, мы также можем опустить окружающие их скобки. Также мы опускаем тип возвращаемого значения.
Здесь опущен return, потому что замыкание представляет собой только одну строку кода.
Мы используем сокращенные имена для первого и второго параметра замыкания.
Когда замыкание является последним или единственным параметром функции, вы можете написать код замыкания вне скобок функции. Это называется выходящее замыкание (traling closure).
Здесь мы используем оператор < в качестве замыкания. В Swift операторы являются функциями верхнего уровня. Его тип (lhs: (), rhs: ()) -> Bool, что соответствует типу sorted(by:).
Замыкания и захват значений
В Swift замыкания захватывают переменные и константы из окружающей их области видимости.
Каждая переменная, функция и замыкание имеет свою область видимости. Область видимости определяет, где вы можете получить доступ к определенной переменной, функции или замыканию. Если переменная, функция или замыкание не находятся в области видимости, вы не можете получить к ним доступ. Область видимости иногда называется «контекстом».
Любой код имеет глобальные и локальные области видимости. К примеру:
- Свойство, определенное в классе, является частью глобальной области видимости. В любом месте этого класса вы можете установить и получить значение свойства.
- Переменная, как определено в функции, является частью локальной области действия функции. В любом месте этой функции вы можете установить и получить значение переменной.
Рассмотрим пример того, как замыкание захватывает окружающую область видимости:
Константа name присваивается значение «Александр» типа String. Затем создается замыкание и назначается константе greeting. Замыкание выводит некоторый текст. Наконец, замыкание выполняется путем вызова greeting().
В этом примере замыкание захватывает значение переменной name. То есть инкапсулирует переменные, которые доступны в области, в которой определено замыкание. В результате мы можем получить доступ к name даже если оно не объявлено локально в замыкании.
Посмотрим на более сложный пример:
Сначала мы создаем функцию addScore(_:), которое возвращает новое значение на основании параметра и предустановленного значения внутри функции. Затем внутри функции определяется замыкание calculate, которое добавляет score и points, а затем возвращает результат. Функция возвращает замыкание calculate().
При этом замыкание calculate захватывает оба значения score и points. Ни одна из этих переменных не объявляется локально в замыкании, но замыкание может получить доступ к их значениям.
Сильные ссылки и захват значений
Когда замыкание захватывает значение, оно автоматически создает сильную ссылку на это значение.
Когда Боб имеет сильную ссылку на Алису, Алиса не удаляется из памяти, пока Боб не будет удален из памяти. Но что, если у Алисы есть сильная ссылка на Боба? Тогда и Боб, и Алиса не будут удалены из памяти, потому что они держатся друг за друга. Боб не может быть удален, потому что Алиса держит его, а Алиса не может быть удалена, потому что Боб держит ее.
Это называется сильным циклом ссылки (strong reference cycle) и вызывает утечку памяти. Представьте, что сотня Бобов и Алис занимают по 10 МБ в памяти и тогда у нас определенно возникнет проблема.
Память в iOS управляется с помощью концепции под названием автоматический подсчет ссылок (Automatic Reference Counting или ARC). Большая часть управления памятью с помощью ARC сделана за вас, но вы должны избегать сильных циклов ссылок.
Вы можете разорвать цикл сильных ссылок, связанных с захватом значений. Так же, как вы можете пометить свойство как weak, вы можете пометить захваченные значения в замыкании как weak или unowned ссылку.
Сначала мы определяем класс Database. У него есть одно свойство data. Затем мы создаем экземпляр Database с именем database и устанавливаем для его свойства data целочисленное значение. Далее мы создаем замыкание calculate. Замыкание принимает один аргумент multiplier. Внутри замыкания data умножается на multiplier. Наконец, замыкание вызывается с аргументом 2 и его результат присваивается result.
Ключевой частью кода здесь является список захвата:
Список захвата — это список имен переменных, разделенных запятыми, с префиксом weak или unowned, заключенный в квадратные скобки.
Мы используете список захвата, чтобы указать, что на конкретное захваченное значение нужно ссылаться как weak или unowned. weak или unowned нарушает цикл сильных ссылок, поэтому замыкание не будет удерживать захваченный объект.
- Ключевое слово weak указывает на то, что захваченное значение может стать nil.
- Ключевое слово unowned указывает на то, что захваченное значение не становится nil.
Мы обычно используем unowned, когда замыкание и захваченное значение будут ссылаться друг на друга и будут освобождены одновременно. Примером является [unowned self] в View Controller. Замыкание уничтожается вместе с контекстом.
Мы обычно используем weak, когда зафиксированное значение в какой-то момент становится nil. Это может произойти, когда замыкание переживает контекст, в котором оно было создано. Например, View Controller, который освобождается до завершения длительной задачи. В результате захваченное значение является опциональным.
Обработчки завершения (completion handlers)
Распространенным применением замыканий является обработчик завершения.
- Вы выполняете длительную по времени задачу в своем коде, например, загружаете файл, делаете расчет или ждете ответа веб-сервиса.
- Вы хотите выполнить некоторый код, когда долгая задача будет завершена, но вы не хотите постоянно выяснять статус, чтобы узнать, завершена ли она.
- Вместо этого вы предоставляете замыкание для долгой задачи, которое будет вызвано, когда задача будет завершена (отсюда название «обработчик завершения»).
В приведенном выше коде мы делаем сетевой запрос на загрузку некоторых данных и что-то делаем с этими данными, когда запрос будет завершен.
Вы можете писать код так, как будто между началом запроса и его завершением нет времени. Вместо того, чтобы ждать завершения длинной задачи, мы можем просто предоставить код, который будет выполнен по завершению задачи, в то время как мы кодируем сетевой запрос здесь и сейчас.
Запускается длинная задача, и мы определяем обработчик завершения. Обработчик завершения является замыканием, поэтому он захватывает переменные в окружающей его области. Длительная задача завершается, и ваш обработчик выполнения выполняется. Область действия замыкания сохраняется, поэтому мы можем использовать любые переменные и константы, определенные в области замыкания.
К примеру, мы хотим использовать данные сетевого запроса для отображения изображения:
Мы определям изображение с помощью UIImageView(), запускаете сетевой запрос и предоставляем обработчик завершения. Обработчик завершения выполняется, когда длинная задача будет завершена.
Замыкание захватило ссылку imageView, так что вы можете установить данные изображения, когда длинная задача будет завершена.
Using @autoclosure when designing Swift APIs
Swift’s @autoclosure attribute enables you to define an argument that automatically gets wrapped in a closure. It’s primarily used to defer execution of a (potentially expensive) expression to when it’s actually needed, rather than doing it directly when the argument is passed.
One example of when this is used in the Swift standard library is the assert function. Since asserts are only triggered in debug builds, there’s no need to evaluate the expression that is being asserted in a release build. This is where @autoclosure comes in:
I’m paraphrasing the implementation of assert a bit above, the actual implementation can be found here.
The nice thing about @autoclosure is that it has no effect on the call site. If assert was implemented using “normal” closures you’d have to use it like this:
But now, you can just call it like you would any function that takes non-closure arguments:
This week, let’s take a look at how we can use @autoclosure in our own code, and how it enables us to design some pretty nice APIs.
Inlining assignments
One thing that @autoclosure enables is to inline expressions in a function call. This enables us to do things like passing assignment expressions as an argument. Let’s take a look at an example where this can be useful.
On iOS, you normally define view animations using this API:
With @autoclosure , we could write an animate function that automatically creates an animation closure and executes it, like this:
Now, we can simply perform our animation with a simple function call without any extra <> syntax:
Using the above technique, we can really reduce the verbosity of our animation code, without sacrificing readability or expressiveness
Passing errors as expressions
Another situation that I find @autoclosure very useful in is when writing utilities that deal with errors. For example, let’s say we want to add an extension on Optional that enables us to unwrap it using a throwing API. That way we can require the optional to be non- nil , or else throw an error, like this:
Similar to how assert is implemented, we only evaluate the error expression when needed, rather than having to do it for every time we attempt to unwrap an optional. We can now use our unwrapOrThrow API like this:
Type inference using default values
The final use case for @autoclosure that I’ve found is when extracting an optional value from a dictionary, a database, or UserDefaults .
Normally, when extracting a value from an untyped dictionary and providing a default value, you’d have to write something like this:
That’s kind of hard to read, and has a lot of syntax cruft with the casting and the ?? operator. With @autoclosure , we can define an API that enables us to write the same expression like this instead:
Above, we can see that the default value is both used for when a value was missing, but also enables Swift to do type inference on the value, without us having to specify the type or perform casting. Pretty neat
Let’s take a look at how we would write such an API:
Again, we use @autoclosure to avoid having to evaluate the default value every time this method is called.
Conclusion
Reducing verbosity is always something that needs to be done with careful consideration. Our goal should always be to write expressive, easy to read code, so we need to make sure that we don’t remove important information from the call site when designing low verbosity APIs.
I think when used in appropriate situations, @autoclosure is a great tool for doing just that. Dealing with expressions, instead of just values, enables us to reduce verbosity and cruft, while also potentially gaining better performance.
Do you have some other uses of @autoclosure that you think are really useful? Let me know, along with any other comments, questions or feedback that you might have — on Twitter @johnsundell.
Замыкания в Swift: полное руководство по использованию
Замыкания – это автономные блоки функциональности, которые можно передавать и использовать в вашем коде.
Иными словами, замыкание – это блок кода, который вы можете присвоить переменной. Затем вы можете передать его в своем коде, например, в другую функцию.
Давайте посмотрим на аналогию:
- Боб говорит Алисе: «Помаши руками!» Алиса слышит инструкцию и машет руками. Размахивание руками – это функция, которую Боб вызвал напрямую.
- Алиса записывает свой возраст на листе бумаги и передает его Бобу. Лист бумаги является переменной. Алиса использовала лист бумаги для сохранения данных.
- Боб пишет: «Помашите руками!» на листе бумаги и дает его Алисе. Алиса читает инструкцию на листе бумаги и машет руками. Инструкция, переданная на листе бумаги, является замыканием.
Позже мы можем отправить этот лист бумаги по почте или сказать Алисе, чтобы она прочитала его после того, как закончит выполнять другое задание. Это одно из преимуществ использования замыканий.
- В первой строке мы определяем замыкание и назначаем его константе birthday. Замыкание – это код между фигурными скобками. Обратите внимание, что замыкание назначается birthday с помощью оператора присваивания =.
- Когда вы запускаете код, замыкание вызывается с помощью синтаксиса birthday(). То есть мы используем имя константы birthday с круглыми скобками (). Это похоже на вызов функции.
Как и функции, замыкания могут иметь параметры.
- Как и раньше, мы объявляем замыкание в первой строке, затем назначаем его константе birthday и вызываем замыкание в последней строке.
- Замыкание теперь имеет один параметр типа String. Этот параметр объявлен как тип замыкания – (String) -> ()
- Затем вы можете использовать параметр name в замыкании. При вызове замыкания мы указываем значение параметра.
Здесь важны три вещи:
- Тип замыкания – (String) -> ().
- Код замыкания – < name in ··· >.
- Вызов замыкания – birthday(···).
Параметры замыкания не имеют названий в отличие от функций. Когда вы объявляете замыкание, вы можете указать типы параметров, которые у него есть, например, String. В коде замыкания вы назначаете локальную переменную первому параметру. Это дает параметру имя в замыкании.
Мы можем опустить имя переменной и использовать сокращение для первого параметра – $0:
В приведенном выше коде замыкание birthday имеет один параметр. Внутри замыкания сокращение используется для ссылки на значение первого параметра – $0.
Типы замыканий
Каждое замыкание имеет тип, как и любая другая переменная или константа.
Мы объявляем замыкание с одним параметром следующим образом:
Замыкание имеет один параметр типа и возвращает (). Первый параметр замыкания также принимает данный тип (name: String) -> (). Ключевое слово in отделяет параметры замыкания от кода.
Синтаксис для типа замыкания состоит из типов параметров и типа возвращаемого значения:
- (Int, Int) -> Double – имеет 2 параметра Double и возвращает значение Double.
- () -> Int – не имеет параметров и возвращает целое число.
- (String) -> String – принимает строку и возвращает строку.
Давайте рассмотрим код самого замыкания:
Тип замыкания объявляется несколько иначе и включает имена параметров. Посмотрим на несколько примеров:
- (Int, Int) -> Double становится
- () -> Int становится
- (String) -> String становится
Вы можете называть эти параметры как угодно, но вам нужно будет дать им имена. Эти константы могут использоваться «локально» внутри замыкания по аналогии с параметрами внутри тела функции.
in проще всего воспринимать как “У нас есть параметры X, Y, Z в блоке кода, который является замыканием”.
Замыкание без параметров и без возвращаемого значения имеет следующий тип:
Вы также можете использовать Void в качестве типа возвращаемого значения. В Swift Void означает «ничего»:
Посмотрим на последний пример:
Замыкание greeting имеет два параметра типа String. Также замыкание возвращает значение типа String. Тип greeting явно определен, как и параметры замыкания.
Когда вызывается замыкание, ему предоставляется два аргумента типа String, и его возвращаемое значение присваивается text, а затем выводится на консоль.
Замыкания и вывод типа
Swift может сам выводить типы. Когда вы не указываете явно тип переменной, Swift может самостоятельно определить, какой тип у данной переменной. Это зависит от контекста вашего кода.
Swift выводит тип age на основании контекста. 104 – это значение для целого числа, так что константа age имеет тип Int. Swift выясняет это самостоятельно без необходимости явно указывать тип.
Вывод типа часто используется в замыканиях. В результате вы можете опустить часть кода для замыкания.
Мы создаем массив с именами, а затем сортируем их в алфавитном порядке, вызывая функцию sorted(by:). Параметр by: принимает замыкание <, которое используется для сортировки массива.
Рассмотрим полный код замыкания:
Здесь используется полный синтаксис замыкания, включая два имени параметра s1 и s2 типа String, а также тип возвращаемого значения Bool. Мы также используем ключевые слова in и return .
Этот код можно сократить:
Здесь пропускаются типы параметров замыкания, поскольку они могут быть выведены из контекста. Поскольку мы сортируем массив строк, эти два параметра выводятся как String. Опуская типы, мы также можем опустить окружающие их скобки. Также мы опускаем тип возвращаемого значения.
Здесь опущен return, потому что замыкание представляет собой только одну строку кода.
Мы используем сокращенные имена для первого и второго параметра замыкания.
Когда замыкание является последним или единственным параметром функции, вы можете написать код замыкания вне скобок функции. Это называется выходящее замыкание (traling closure).
Здесь мы используем оператор < в качестве замыкания. В Swift операторы являются функциями верхнего уровня. Его тип (lhs: (), rhs: ()) -> Bool, что соответствует типу sorted(by:).
Замыкания и захват значений
В Swift замыкания захватывают переменные и константы из окружающей их области видимости.
Каждая переменная, функция и замыкание имеет свою область видимости. Область видимости определяет, где вы можете получить доступ к определенной переменной, функции или замыканию. Если переменная, функция или замыкание не находятся в области видимости, вы не можете получить к ним доступ. Область видимости иногда называется «контекстом».
Любой код имеет глобальные и локальные области видимости. К примеру:
- Свойство, определенное в классе, является частью глобальной области видимости. В любом месте этого класса вы можете установить и получить значение свойства.
- Переменная, как определено в функции, является частью локальной области действия функции. В любом месте этой функции вы можете установить и получить значение переменной.
Рассмотрим пример того, как замыкание захватывает окружающую область видимости:
Константа name присваивается значение “Александр” типа String. Затем создается замыкание и назначается константе greeting. Замыкание выводит некоторый текст. Наконец, замыкание выполняется путем вызова greeting().
В этом примере замыкание захватывает значение переменной name. То есть инкапсулирует переменные, которые доступны в области, в которой определено замыкание. В результате мы можем получить доступ к name даже если оно не объявлено локально в замыкании.
Посмотрим на более сложный пример:
Сначала мы создаем функцию addScore(_:), которое возвращает новое значение на основании параметра и предустановленного значения внутри функции. Затем внутри функции определяется замыкание calculate, которое добавляет score и points, а затем возвращает результат. Функция возвращает замыкание calculate().
При этом замыкание calculate захватывает оба значения score и points. Ни одна из этих переменных не объявляется локально в замыкании, но замыкание может получить доступ к их значениям.
Сильные ссылки и захват значений
Когда замыкание захватывает значение, оно автоматически создает сильную ссылку на это значение.
Когда Боб имеет сильную ссылку на Алису, Алиса не удаляется из памяти, пока Боб не будет удален из памяти. Но что, если у Алисы есть сильная ссылка на Боба? Тогда и Боб, и Алиса не будут удалены из памяти, потому что они держатся друг за друга. Боб не может быть удален, потому что Алиса держит его, а Алиса не может быть удалена, потому что Боб держит ее.
Это называется сильным циклом ссылки (strong reference cycle) и вызывает утечку памяти. Представьте, что сотня Бобов и Алис занимают по 10 МБ в памяти и тогда у нас определенно возникнет проблема.
Память в iOS управляется с помощью концепции под названием автоматический подсчет ссылок (Automatic Reference Counting или ARC). Большая часть управления памятью с помощью ARC сделана за вас, но вы должны избегать сильных циклов ссылок.
Вы можете разорвать цикл сильных ссылок, связанных с захватом значений. Так же, как вы можете пометить свойство как weak, вы можете пометить захваченные значения в замыкании как weak или unowned ссылку.
Сначала мы определяем класс Database. У него есть одно свойство data. Затем мы создаем экземпляр Database с именем database и устанавливаем для его свойства data целочисленное значение. Далее мы создаем замыкание calculate. Замыкание принимает один аргумент multiplier. Внутри замыкания data умножается на multiplier. Наконец, замыкание вызывается с аргументом 2 и его результат присваивается result.
Ключевой частью кода здесь является список захвата:
Список захвата – это список имен переменных, разделенных запятыми, с префиксом weak или unowned, заключенный в квадратные скобки.
Мы используете список захвата, чтобы указать, что на конкретное захваченное значение нужно ссылаться как weak или unowned. weak или unowned нарушает цикл сильных ссылок, поэтому замыкание не будет удерживать захваченный объект.
- Ключевое слово weak указывает на то, что захваченное значение может стать nil.
- Ключевое слово unowned указывает на то, что захваченное значение не становится nil.
Мы обычно используем unowned, когда замыкание и захваченное значение будут ссылаться друг на друга и будут освобождены одновременно. Примером является [unowned self] в View Controller. Замыкание уничтожается вместе с контекстом.
Мы обычно используем weak, когда зафиксированное значение в какой-то момент становится nil. Это может произойти, когда замыкание переживает контекст, в котором оно было создано. Например, View Controller, который освобождается до завершения длительной задачи. В результате захваченное значение является опциональным.
Обработчки завершения (completion handlers)
Распространенным применением замыканий является обработчик завершения.
- Вы выполняете длительную по времени задачу в своем коде, например, загружаете файл, делаете расчет или ждете ответа веб-сервиса.
- Вы хотите выполнить некоторый код, когда долгая задача будет завершена, но вы не хотите постоянно выяснять статус, чтобы узнать, завершена ли она.
- Вместо этого вы предоставляете замыкание для долгой задачи, которое будет вызвано, когда задача будет завершена (отсюда название «обработчик завершения»).
В приведенном выше коде мы делаем сетевой запрос на загрузку некоторых данных и что-то делаем с этими данными, когда запрос будет завершен.
Вы можете писать код так, как будто между началом запроса и его завершением нет времени. Вместо того, чтобы ждать завершения длинной задачи, мы можем просто предоставить код, который будет выполнен по завершению задачи, в то время как мы кодируем сетевой запрос здесь и сейчас.
Запускается длинная задача, и мы определяем обработчик завершения. Обработчик завершения является замыканием, поэтому он захватывает переменные в окружающей его области. Длительная задача завершается, и ваш обработчик выполнения выполняется. Область действия замыкания сохраняется, поэтому мы можем использовать любые переменные и константы, определенные в области замыкания.
К примеру, мы хотим использовать данные сетевого запроса для отображения изображения:
Мы определям изображение с помощью UIImageView(), запускаете сетевой запрос и предоставляем обработчик завершения. Обработчик завершения выполняется, когда длинная задача будет завершена.
Замыкание захватило ссылку imageView, так что вы можете установить данные изображения, когда длинная задача будет завершена.