Замыкания


Замыкания (сlosures) представляют самодостаточные блоки кода, которые могут использоваться многократно в различных частях программы, в том числе в виде параметров в функциях.

По сути функции являются частным случаем замыканий. Замыкания могут иметь одну из трех форм:

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

вложенные функции, которые имеют имя и которые сохраняют значения внешних переменных и констант

замыкающие выражения (closure expressions), которые не имеют имени и которые могут сохранять значения внешних переменных и констант

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

Замыкающие выражения в общем случае имеют следующий синтаксис:

1
2
3
4
{ (параметры) -> тип_возвращаемого_значения in

инструкции
}
Если замыкания не имеют параметров или не возвращают никакого значения, то соответствующие элементы при определении замыкания могут опускаться.

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

1
2
3
let hello = { print("Hello world")}
hello()
hello()
В данном случае константе hello присваивается анонимная функция, которая состоит из блока кода, в котором выполняются некоторые действия. Эта функция не имеет никакого имени, мы ее можем вызывать только через константу hello.

Фактически константа hello в данном случае имеет тип ()->() или ()-gt;Void:

1
let hello: ()->Void = { print("Hello world")}
Дополнительно можно определить список параметров с помощью ключевого слова in:

1
2
3
4
5
6
7
let hello = {
(message: String) in
print(message)
}
hello("Hello")
hello("Salut")
hello("Ni hao")
В данном случае замыкание принимает один параметр - message, который представляет тип String. Список параметров указывается до ключевого слова in, а после идут инструкции функции.

Также можно определить возвращаемое значение:

1
2
3
4
5
6
7
let sum = {
(x: Int, y: Int) -> Int in
return x + y
}
print(sum(2, 5)) // 7
print(sum(12, 15)) // 27
print(sum(5, 3)) // 8
Замыкания как аргументы функций
Как правило, анонимные функции используются в том месте, где они определены. Нередко анонимные функции передаются в другие функции в качестве параметра, если параметр представляет функцию:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func operation(_ a: Int, _ b: Int, _ action: (Int, Int) -> Int) -> Int{

return action(a, b)
}

let x = 10
let y = 12

let result1 = operation(x, y, {(a: Int, b: Int) -> Int in

return a + b
})

print(result1) // 22

var result2 = operation(x, y, {(a: Int, b: Int) -> Int in return a - b})

print(result2) // -2
Здесь функция operation() в качестве третьего параметра принимает другую функцию, которой передаются значения первого и второго параметров. Но нам необязательно определять дополнительные функции, поскольку в operation мы можем передать замыкающее выражение.

В первом случае это выражение производит сложение параметров, а во втором случае - их вычитание.

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

1
2
3
4
5
6
7
8
let x = 10
let y = 12

let result1 = operation(x, y, {(a, b) in a + b })
print(result1) // 22

let result2 = operation(x, y, {(a, b) in a - b })
print(result2) // -2
Компилятор видит, замыкающее выражение передается в качестве значения для параметра типа (Int, Int) -> Int, то есть в качестве функции, которая принимает параметры типа Int. Поэтому можно не указывать тип параметров a и b. Также компилятор определяет, что функция возвращает значение типа Int, поэтому выражение после ключевого слова in воспринимается как возвращаемое значение, и явным образом можно не использовать оператор return.

Но мы можем еще больше сократить замыкание, используя сокращения для параметров:

1
2
3
4
5
6
7
8
let x = 10
let y = 12

let result1 = operation(x, y, {$0 + $1})
print(result1) // 22

let result2 = operation(x, y, {$0 - $1})
print(result2) // -2
$0 представляет первый переданный в функцию параметр, а $1 - второй параметр. Система автоматически распознает их и поймет, что они представляют числа.

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

1
2
3
4
5
6
7
8
let x = 10
let y = 12

let result1 = operation(x, y, +)
print(result1) // 22

let result2 = operation(x, y, -)
print(result2) // -2
Система автоматически распознает,что должны выполняться операции сложения и вычитания двух переданных параметров, и поэтому результат будет тот же.

Доступ к контексту
Замыкания имеют полный доступ к контексту, в котором они определены. Кроме того, замыкания могут использовать внешние переменные и константы как состояние, которое может храниться на всем протяжении жизни замыкания:

1
2
3
4
5
6
7
8
9
10
11
func action() -> (()->Int){

var val = 0
return {
val = val+1
return val
}
}
let inc = action()
print(inc()) // 1
print(inc()) // 2
Здесь определена функция action, которая, в свою очередь, сама возвращает функцию. По факту она возвращает замыкающее выражение, которое увеличивает внешнюю переменную val на единицу и затем возвращает ее значение. Но при вызове мы видим, что переменная val сохраняет свое значение после увеличения, оно не сбрасывается обратно к нулю при каждом вызове функции. То есть переменная val представляет состояние, где замыкание может хранить данные.

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

1
2
3
4
5
6
7
8
9
var a = 14
var b = 2

let myClosure: () -> Int = {return a + b}
print(myClosure()) // 16

a = 5
b = 6
print(myClosure()) // 11
Замыкающее выражение, на которое указывает константа myClosure, складывает значения переменных a и b. С изменением значений переменных также меняется результат замыкания myClosure. Однако мы можем зафиксировать начальные значения переменных:

1
2
3
4
5
6
7
8
9
var a = 14
var b = 2

let myClosure: () -> Int = {[a, b] in return a + b}
print(myClosure()) // 16

a = 5
b = 6
print(myClosure()) // 16
Передав переменные в квадратные скобки: [a, b], мы тем самым фиксируем их начальные значения. И даже если значения этих переменных в какой-то момент изменятся, замыкание будет оперировать прежними значениями.