Для чего нужны автозамыкания swift
Перейти к содержимому

Для чего нужны автозамыкания swift

  • автор:

Замыкания в 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, так что вы можете установить данные изображения, когда длинная задача будет завершена.

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.

Featured SwiftLee Jobs

Find your next Swift career step at world-class companies with impressive apps by joining the SwiftLee Talent Collective. I’ll match engineers in my collective with exciting app development companies. SwiftLee Jobs

Antoine van der Lee

iOS Developer since 2010. Lead developer of the Collect by WeTransfer app. Writing a new blog post every week related to Swift, iOS and Xcode. Regular speaker and workshop host.

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: 7 секретов оптимизации

Swift: 7 секретов оптимизации

Ключевое слово indirect применяется только с перечислениями enum . Как известно, они являются типами значений и хранятся в стеке. Поэтому компилятору необходимо знать, сколько памяти занимает каждое перечисление.

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

А что если сделать enum рекурсивным?

Это определение порождает ошибку компилятора.

Суть ошибки в том, что компилятор не может вычислить размер Foo , так как он стремится к бесконечности. В этом случае требуется indirect .

  • простой — видоизменяет структуру памяти enum для решения проблемы рекурсии;
  • конкретный — .bizz(Foo) больше не хранится как вложение в памяти. С модификатором indirect данные хранятся в указателе (косвенно).

Проблема решена! Кроме того, мы можем преобразовать все кейсы перечисления в indirect :

2. Атрибут @autoclosure

Атрибут Swift @autoclosure определяет аргумент, который автоматически оборачивается в замыкание. Как правило, он необходим для того, чтобы отложить выполнение выражения до нужного момента.

Вызов вычисления происходит следующим образом:

В этом примере при условии zero: true вызов calculate не вычисляет выражение, тем самым улучшая производительность кода.

3. Свойства Lazy

Свойство отложенного хранения lazy — это свойство, начальное значение которого не вычисляется до первого применения. “Ленивые” свойства всегда объявляются как переменные. Обратите внимание, что при указании lazy в struct следует обозначить функцию, задействующую эту структуру, как mutating .

Мы уже познакомились с атрибутом @autoclosure , который позволяет отложить вычисление выражения. Он также может использоваться с lazy ! Рассмотрим распространенный случай внедрения зависимости:

4. Перечисления как пространства имен

В Swift нет пространств имен, что может обернуться сложностями при работе с большими проектами. Но эта проблема легко решается с помощью перечислений.

5. Атрибут @dynamicMemberLookup

Данный раздел посвящен атрибуту @dynamicMemberLookup (динамический поиск элементов). Он пригодится для работы со структурами и классами.

Если мы просто добавим @dynamicMemberLookup в определение, то получим ошибку.

Атрибут @dynamicMemberLookup требует наличия у класса Foo метода subscript(dynamicMember:) , который принимает либо ExpressibleByStringLiteral , либо key path .

Следовательно, необходимо определить метод subscript :

В subscript есть возможность реализовать намного более сложную логику для извлечения данных. При этом видно, что данная реализация ограничена только строками и не гарантирует полную безопасность. Исправим это с помощью key path :

Знание этой функциональности не обязывает использовать ее повсеместно. Вы сами определяете, какой из двух вариантов отличается лучшей читаемостью и выразительностью: a.himself.age или a.age .

6. Атрибут @dynamicCallable

Речь идет о функциональности компилятора для вызова объектов, которая применяется со struct , enum и class .

Добавление атрибута приводит к ошибке:

Атрибут @dynamicCallable требует наличия у структуры RangeGenerator либо метода dynamicallyCall(withArguments:) , либо dynamicallyCall(withKeywordArguments:) .

Сигнатура метода аналогична сигнатуре @dynamicMemberLookup :

7. Встраивание кода

Иногда требуется предоставить дополнительную информацию об оптимизациях, которые может задействовать компилятор. Встраиваемый код — одна из важнейших функциональностей оптимизации. Рассмотрим, как работать с атрибутами ‌@inlinable , @inline(__always) и @usableFromInline .

Атрибут @inlinable экспортирует тело функции как часть интерфейса модуля, делая его доступным для оптимизатора при обращении к нему из других модулей.

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

@inline(__always) указывает компилятору игнорировать эвристику встраивания и (почти) всегда встраивать функцию.

Функция с @inline(__always) в отличии от функции с @inlinable не может использоваться для встраивания вне своего модуля из-за недоступности ее кода.

@inline(__always) может как улучшить производительность, так и негативно сказаться на производительности макросов из-за увеличения размера кода.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *