Глава 3. Замыкания в языке Swift

Оглавление

Введение

Замыкания с множественными параметрами

Захват значений

Используем замыкания как данные

Использование тянущихся замыканий

Передача параметров тянущимся замыканиям

Передача параметров и получение результата из тянущихся замыканий

Заключение

Введение

Чтобы понять что такое замыкание, необходимо обратиться к аналогиям. Человеку проще посмотреть 1 короткое видео не больше 50-60 секунд, чем смотреть серьезное интервью продолжительностью больше 90 минут. В программирование все то же самое.

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

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

func descriptiveName(){
   //your code
}

Теперь для вызова функции необходимо только вызвать ее по имени  – descriptiveName(). Если функция возвращает значение, то запись немного изменится:

var value = descriptiveName()

Если кратко, то для использования функции необходимо:

  1. Создать функцию
  2. Вызвать функцию

Замыкания – это альтернативный путь создания функций. В данном случае создание и вызов функции объединяются в один этап.

Используя замыкания, Ваш код может стать более лаконичным и кратким. В то же время недостатком может стать код, сложный к чтению и пониманию. Если создаете большие и сложные для понимания конструкции, старайтесь больше комментировать код. Как мы увидим ниже, код замыканий может быть представлен в нескольких видах. От легко понимаемого до тяжело понимаемого и читаемого. Для начала создадим функцию, которая будет возвращать произведение аргумента на число 4:

func multiplyBy4(x: Int ) -> Int{
    return x*4
}

Первый вариант создания замыкания заключается в удалении ключевого слова func, имени функции squareValue.  В итоге оставляем только код в угловых скобках.

let valueFirst = {(x: Int ) -> Int in return x * 4 }

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

let valueSecond = {x in return x * 4 }

Казалось бы, мы уже пришли к очень краткой форме. Но нет, можно еще и убрать ключевое слово return:

let valueThird = {x in x*4 }

Как мы видим, мы максимально сократили нашу функцию. В таких простых примерах замыкания не влияют на понимание кода и способны значительно сокращать объем кода. Мы все еще можем сократить наше замыкание. Мы может убрать любые переменные и заменить их заполнителями(placeholder) –$0, которые идентичны различным параметрам:

var valueFourth = {$0*4}

Теперь протестируем наш код. В среде XCode можно создать проект Playground. Он служит своего рода песочницей для тестирования, изучения и экспериментирования с языком Swift. Исходный код этой главы Вы можете скачать по ссылке.

print("func multiplyBy4 (x: Int) -> Int{")
func multiplyBy4(x: Int ) -> Int{
    return x*4
}
print(multiplyBy4(x: 2))
print(multiplyBy4(x: 22))

print("(x: Int ) -> Int in return x*x ")
let valueFirst = {(x: Int ) -> Int in return x * 4 }
print(valueFirst(2))
print(valueFirst(22))

print("x in return x * 4")
let valueSecond = {x in return x * 4 }
print(valueSecond(2))
print(valueSecond(22))

print("x in x*4")
let valueThird = {x in x*4 }
print(valueThird(2))
print(valueThird(22))

print("$0*4")
var valueFourth = {$0*4}
print(valueFourth(2))
print(valueFourth(22))

После запуска нашего кода мы получим следующий вывод:

func multiplyBy4 (x: Int) -> Int{
8
88
(x: Int ) -> Int in return x*x 
8
88
x in return x * 4
8
88
x in x*4
8
88
$0*4
8
88

замыкания в языке swift

Как мы могли убедиться, все 4 версии представленных нами замыканий работают идентично. Главное отличие заключается в том, насколько просто и лаконично Вы можете составить замыкание. Теперь встречая замыкания в коде других разработчиков, Вы будете иметь представление о том, что это такое.

Как Вы могли обратить внимание, замыкания не всегда легки к пониманию. Для облегчения задачи разбора своего кода, многие программисты используют комбинации $0, $1, $2, $3 как первый, второй, третий, четвертый и т.д.  порядковые номера в качестве передаваемых заполнителей.

Замыкания с множественными параметрами

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

func multiplicationTwoNumber(x: Int, y: Int) -> Int{
     return x*y
}

Когда Вы используете замыкания, все параметры заключаются внутри круглых скобок. В большинстве случаев Вы не должны определять тип данных для каждого параметра, так как компилятор определяет тип передаваемых данных по типу возвращаемых данных. Например, если возвращаемый тип данных – integer, компилятор Swift делает вывод что передаваемые параметры тоже должны быть integer:

{(x,y) -> Int in return x*y}

Однако если в замыкании присутствует двухсмысленность, необходимо явно определить  типы данных параметров, как указано в примере:

{(x: Int, y : Int) in return x*y}
{(x: Int, y: Int) in x*y}
{$0 as Int * $1 as Int}

Если мы посмотрим на 3 верхних примера, то увидим что замыкание с использованием конструкции as в качестве определения типа данных принимает более лаконичный вид, чем использование определения типа данных входных аргументов через двоеточие.

Создадим новый PlayGround файл – ClosureMultiply для закрепления данной темы.

print("func multiplicationTwoNumber(x: Int, y: Int)-> Int")
func multiplicationTwoNumber(x: Int, y: Int) -> Int{
    return x*y
}
print(multiplicationTwoNumber(x:3, y:6))
print(multiplicationTwoNumber(x:11, y:10))

print("(x,y) -> Int in return x*y}")
let firstClosure = {(x,y) -> Int in return x*y}
print(firstClosure(3,6))
print(firstClosure(11,10))

print("{(x:Int, y: Int) in return x*y}")
let secondClosure = {(x:Int, y: Int) in return x*y}
print(secondClosure(3,6))
print(secondClosure(11,10))

print("{(x: Int, y: Int) in x*y}")
let thirdClosure = {(x: Int, y: Int) in x*y}
print(thirdClosure(3,6))
print(thirdClosure(11,10))

print("{$0 as Int * $1 as Int}")
let fourthClosure = {$0 as Int * $1 as Int}
print(fourthClosure(3,6))
print(fourthClosure(11,10))

Захват значений

Когда Вы определяете переменные и константы внутри самой функции, их область видимости ограничивается телом самой функции. А когда ВЫ определяете переменную вне тела функции, например в области видимости класса, другие функции имеют доступ к этой переменной, как указано на рисунке:

closure value capturing

Внимательно посмотрим на наш листинг кода. Константа valueOutside определена снаружи функции и поэтому функция имеет доступ к ней. В то же время константа insideParam определена внутри функции multiplyNumbers, и поэтому никакая другая функция или класс не имеет доступа к ней. Попробуем вызвать функцию print() с константой insideParam за пределами ее области видимости:

Closures value

И получим ошибку из за невозможности получить доступ к данным за пределами видимости. Дело в том, что print – это функция. В языке Swift замыкания устроены несколько по другому, чем функции. Поэтому замыкания могут получать доступ к значениям переменных, констант и и даже изменять значения переменных за пределами своей области видимости. Для лучшего понимания этого, для начала рассмотрим несколько важных подтем в такой важной главе, как замыкания. И уже по мере получения необходимых знаний снова вернемся к этой теме.

Используем замыкания как данные

Одна из самых интересных универсальных возможностей замыканий – это использование замыканий в роли данных, которые можно использовать между разными участками кода словно фиксированные значения. Это означает, что ВЫ можете передавать замыкания как параметры функции(или даже другого замыкания), сохранять замыкания в структурах данных наподобии массивов или присваивать замыкание переменной.

Когда Вы определяете функцию, Вы должны дать функции уникальное имя:

func multiplicationTwoNumber(x: Int, y: Int) -> Int{
    return x*y
}

Для вызова этой функции необходимо использовать имя функции и передать параметры ей:

multiplicationTwoNumber(x: 3, y: 11)

Замыканию также можно присвоить переменную или константу:

let firstClosure = {(x,y) -> Int in return x*y}

let secondClosure = {(x:Int, y: Int) in return x*y}

let thirdClosure = {(x: Int, y: Int) in x*y}

let fourthClosure = {$0 as Int * $1 as Int}

В предыдущих разделах мы уже частично сталкивались с назначением переменной замыканию.  Соответственно теперь замыкание можно вызывать по имени переменной:

firstClosure(3,11)

secondClosure(3,11)

thirdClosure(3,11)

fourthClosure(3, 11)

Замыкание можно передать как параметр другому замыканию:

firstClosure(3, secondClosure (3, 11))

Верхнему замыканию эквивалентно замыкание:

firstClosure(3, 33)

Результатом операции является число 99(3 * 33).

Другая интересная возможность применения замыканий – это возможность их сохранять как структуры данных. Также как фиксированные данные, некоторые замыкания можно представить как элементы массива.

let firstClosure = {(x,y) -> Int in return x*y}
let secondClosure = {(x:Int, y: Int) in return x*y}
let thirdClosure = {(x: Int, y: Int) in x*y}
let fourthClosure = {$0 as Int * $1 as Int}

let closureArray = [firstClosure(3,11),secondClosure(2,1),thirdClosure(4,5),fourthClosure(10,8)]
print(closureArray.count)
for i in closureArray{
    print(i)
}

Первые 4 строки нам уже известны – это наши замыкания по умножению 2-х чисел. Они все эквивалентны друг другу, просто разная запись. Далее мы создаем массив, элементами которого являются 4 замыкания с передаваемыми им параметрами. Мы указали разные параметры, чтобы можно было оценить работоспособность и правильность кода.  Обратите внимание на Вывод, приведенный ниже. При переборе элементов массива, мы можем убедиться, что в действительности сохранились результаты выполнения замыканий, а не сами синтаксические конструкции.

4
33
2
20
80

Использование тянущихся замыканий

Использование замыканий в качестве функций дает преимущество в упрощении кода . Мы привели много примеров когда замыкания состояли всего из одной строки и буквально нескольких символов. Когда же замыкание содержит более чем одну строку, тогда мы можем создать тянущееся замыкание.

По сути тянущееся замыкание – это функция, которая содержит в себе другую функцию в качестве аргумента. Простейшее замыкание не принимает аргументов и не возвращает значений:

()-> Void

Попробуем разобраться на практике. Создадим новый Playground страницу в XCode и назовем ее TrailingClosurePlayground. Далее добавим простую функцию:

func simpleExample(closure:() -> Void){
    print("1. Wake up")
    closure()
    print("4. Eat breakfast")
}

Хотя этот код не выполняет ничего важного и выводит всего лишь текст в консоль, необходимо детально его рассмотреть. Между 2-мя функциями print у нас расположено передаваемое в качестве параметра функции замыкание – closure().

И конечно же добавим код нашего замыкания ниже:

simpleExample(){
    print("---2. Go to bathroom")
    print("---3. Brush teeth")
}

Обратите внимание, что в тянущихся замыканиях(trailing closures) имя функции должно совпадать с наименованием замыкания. В нашем примере код замыкания заключен между {}. После запуска проекта мы увидим вывод:

1. Wake up
---2. Go to bathroom
---3. Brush teeth
4. Eat breakfast

trailing closures

Посмотрим как работает наше выражение. Сначала выводится первая команда print(), определенная в функции, затем выполняется 2 команды print() из тянущегося замыкания, и только потом выполняется последняя команда print() из функции.

Передача параметров тянущимся замыканиям

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

(Int, Int) -> Void

Закрепим материал на практике. Откроем наш файл TrailingClosurePlayground из предыдущего раздела и добавим туда следующую функцию:

func passParameters(closure: (Int, Int) -> Void){
    print("First line")
    closure(2,8)
    print("Second line")
}

Этот код определяет список входных параметров из 2-х переменных типа integer. Между 2-х вызовов print, мы вызываем замыкание и передаем 2 параметра типа Int – 2 и 8. И наконец напишем код тянущегося замыкания:

passParameters{ x,y in
    print("-- closure code begining")
    print("\(x*y)")
    print("-- ending")
}

Наше замыкание содержит 3 конструкции print, 2 из которых выводят текст, а третья выводит результат произведения 2-х чисел.

Нажмем кнопку Run и получим следующий вывод в консоли:

First line
-- closure code begining
16
-- ending
Second line

Передача параметров и получение результата из тянущихся замыканий

Замыкания, также как и функции могут возвращать значения. Все что Вам нужно – это определить тип входных данных и тип возвращаемого значения:

(Int, Int) -> Int

В коде выше определяются 2 входных параметра типа Int. И возвращаемый тип данных – integer. Для того, чтобы посмотреть, как передавать и возвращать результат из тянущихся замыканий, сделаем следующее:

  • Откроем наш файл TrailingClosurePlayground в среде XCode.
  • Создадим новую функцию:
func returnValue(closure: (Int, Int) -> Int){
    print("First Line")
    print("\(closure(6,1))")
    print("Second line")
}

Этот код определяет функцию, которая вызывает тянущееся замыкание, которое принимает 2 входных параметра типа            Int и возвращает  Int значение. Между первым и последним вызовом функции print  мы вызываем замыкание.

  • И напишем наше простое замыкание, которое возвращает значение:
returnValue{ x,y in
    x * y
}

Здесь мы определяем замыкание, которое возвращает результат произведения 2-х чисел.

  • Запустим наш код и получим следующий вывод:
First Line
6
Second line

Заключение

Замыкания – это альтернативный путь написания функций. Сначала Вы создаете функцию, а затем ее вызываете. При использовании замыканий все сводится к одному шагу – создание и использование.

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

func multiplyBy4(x: Int ) -> Int{
    return x*4
}

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

{(x: Int ) -> Int in return x * 4 }
{x in return x * 4 }
{x in x*4 }
{$0*4}

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

{(x,y) -> Int in return x*y}
{(x: Int, y: Int) in x*y}
{$0 as Int * $1 as Int}

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

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

Leave a Reply

Please disable your adblocker or whitelist this site!