Несмотря на то, что в Objective-C 2.0 присутствуют замыкания (известные как блоки), ранее эппловский API использовал их неохотно. Возможно, отчасти поэтому многие программисты под iOS с удовольствием эксплуатировали сторонние библиотеки, вроде AFNetworking, где блоки применяются повсеместно. С выходом Swift, а также добавлением новой функциональности в API, работать с замыканиями стало чрезвычайно удобно. Давайте рассмотрим, какими особенностями обладает их синтаксис в Swift, и какие трюки можно с ними «вытворять».
Продвигаться будем от простого к сложному, от скучного к веселому. Заранее приношу извинения за обильное использование мантр «функция», «параметр» и «Double», но из песни слов не выкинешь.
Часть 1. Вводная
1.1. Объекты первого класса
Для начала укрепимся с мыслью, что в Swift функции являются носителями гордого статуса объектов первого класса. Это значит, что функцию можно хранить в переменной, передавать как параметр, возвращать в качестве результата работы другой функции. Вводится понятие «типа функции». Этот тип описывает не только тип возвращаемого значения, но и типы входных аргументов.
Допустим, у нас есть две похожие функции, которые описывают две математические операции сложения и вычитания:
func add(op1: Double, op2: Double) -> Double {
return op1 + op2
}
func subtract(op1: Double, op2: Double) -> Double {
return op1 - op2
}
Их тип будет описываться следующим образом:
(Double, Double) -> Double
Прочесть это можно так: «Перед нами тип функции с двумя входными параметрами типа Double и возвращаемым значением типа Double.»
Мы можем создать переменную такого типа:
// Описываем переменную
var operation: (Double, Double) -> Double
// Смело присваиваем этой переменной значение
// нужной нам функции, в зависимости от каких-либо условий:
for i in 0..<2 {
if i == 0 {
operation = add
} else {
operation = subtract
}
let result = operation(1.0, 2.0) // "Вызываем" переменную
println(result)
}
Код, описанный выше, выведет в консоли:
3.0
-1.0
1.2. Замыкания
Используем еще одну привилегию объекта первого класса. Возвращаясь к предыдущему примеру, мы могли бы создать такую новую функцию, которая бы принимала одну из наших старых функций типа (Double, Double) -> Double в качестве последнего параметра. Вот так она будет выглядеть:
// (1)
func performOperation(op1: Double, op2: Double, operation: (Double, Double) -> Double) -> Double { // (2)
return operation(op1, op2) // (3)
}
Разберем запутанный синтаксис на составляющие. Функция performOperation принимает три параметра:
- op1 типа Double (op — сокращенное от «операнд»)
- op2 типа Double
- operation типа (Double, Double) -> Double
В своем теле performOperation просто возвращает результат выполнения функции, хранимой в параметре operation, передавая в него первых два своих параметра.
Пока что выглядит запутанно, и, возможно, даже не понятно. Немного терпения, господа.
Давайте теперь передадим в качестве третьего аргумента не переменную, а анонимную функцию, заключив ее в фигурные {} скобки. Переданный таким образом параметр и будет называться замыканием:
let result = performOperation(1.0, 2.0, {(op1: Double, op2: Double) -> Double in
return op1 + op2 // (5)
}) // (4)
println(result) // Выводит 3.0 в консоли
Отрывок кода (op1: Double, op2: Double) -> Double in — это, так сказать, «заголовок» замыкания. Состоит он из:
- псевдонимов op1, op2 типа Double для использования внутри замыкания
- возвращаемого значения замыкания -> Double
- ключевого слова in
Еще раз о том, что сейчас произошло, по пунктам:
(1) Объявлена функция performOperation
(2) Эта функция принимает три параметра. Два первых — операнды. Последний — функция, которая будет выполнена над этими операндами.
(3) performOperation возвращает результат выполнения операции.
(4) В качестве последнего параметра в performOperation была передана функция, описанная замыканием.
(5) В теле замыкания указывается, какая операция будет выполняться над операндами.
Часть 2. Веселая.
Синтаксический сахар и неожиданные «плюшки»
Авторы Swift приложили немало усилий, чтобы пользователи языка могли писать как можно меньше кода и как можно больше тратить свое драгоценное время на чтение Хабра размышления об архитектуре проекта. Взяв за основу наш пример с арифметическими операциями, посмотрим, до какого состояния мы сможем его «раскрутить».
2.1. Избавляемся от типов при вызове.
Во-первых, можно не указывать типы входных параметров в замыкании явно, так как компилятор уже знает о них. Вызов функции теперь выглядит так:
performOperation(1.0, 2.0, {(op1, op2) -> Double in
return op1 + op2
})
2.2. Используем синтаксис «хвостового замыкания».
Во-вторых, если замыкание передается в качестве последнего параметра в функцию, то синтаксис позволяет сократить запись, и код замыкания просто прикрепляется к хвосту вызова:
performOperation(1.0, 2.0) {(op1, op2) -> Double in
return op1 + op2
}
2.3. Не используем ключевое слово «return».
Приятная (в некоторых случаях) особенность языка заключается в том, что если код замыкания умещается в одну строку, то результат выполнения этой строки автоматичеси будет возвращен. Таким образом ключевое слово «return» можно не писать:
performOperation(1.0, 2.0) {(op1, op2) -> Double in
op1 + op2
}
2.4. Используем стенографические имена для параметров.
Идем дальше. Интересно, что Swift позволяет использовать так называемые стенографические (англ. shorthand) имена для входных параметров в замыкании. Т.е. каждому параметру по умолчанию присваивается псевдоним в формате $n, где n — порядковый номер параметра, начиная с нуля. Таким образом, нам, оказывается, даже не нужно придумывать имена для аргументов. В таком случае весь «заголовок» замыкания уже не несет в себе никакой смысловой нагрузки, и его можно опустить:
performOperation(1.0, 2.0) { $0 + $1 }
Согласитесь, эта запись уже совсем не похожа на ту, которая была в самом начале.
2.5. Ход конем: операторные функции.
Все это были еще цветочки. Сейчас будет ягодка.
Давайте посмотрим на предыдущую запись и зададимся вопросом, что уже знает компилятор о замыкании? Он знает количество параметров (2) и их типы (Double и Double). Знает тип возвращаемого значения (Double). Так как в коде замыкания выполняется всего одна строка, он знает, что ему нужно возвращать в качестве результата его выполнения. Можно ли упростить эту запись как-то еще?
Оказывается, можно. Если замыкание работает только с двумя входными аргументами, в качестве замыкания разрешается передать операторную функцию, которая будет выполняться над этими аргументами (операндами). Теперь наш вызов будет выглядеть следующим образом:
performOperation(1.0, 2.0, +)
Красота!
Теперь можно производить элементарные операции над нашими операндами в зависимости от некоторых условий, написав при этом минимум кода.
Кстати, Swift также позволяет использовать операции сравнения в качестве операторной фуниции. Выглядеть это будет примерно так:
func performComparisonOperation(op1: Double, op2: Double, operation: (Double, Double) -> Bool) -> Bool {
return operation(op1, op2)
}
println(performComparisonOperation(1.0, 1.0, >=)) // Выведет "true"
println(performComparisonOperation(1.0, 1.0, <)) // Выведет "false"
Или битовые операции:
func performBitwiseOperation(op1: Bool, op2: Bool, operation: (Bool, Bool) -> Bool) -> Bool {
return operation(op1, op2)
}
println(performBitwiseOperation(true, true, ^)) // Выведет "false"
println(performBitwiseOperation(true, false, |)) // Выведет "true"
Swift — в некотором роде забавный язык программирования. Надеюсь, статья будет полезной для тех, кто начинает знакомиться с этим языком, а также для тех, кому просто интересно, что там происходит у разработчиков под iOS и Mac OS X.
Автор: agee