Книга: Swift. Основы разработки приложений под iOS, iPadOS и macOS. 5-е изд. дополненное и переработанное
Назад: 15. Функции
Дальше: 17. Дополнительные возможности

16. Замыкания (closure)

Как объясняется в документации к языку Swift, замыкания (closu­res) — это организованные блоки с определенным функционалом, которые могут быть переданы и использованы в коде.

Согласитесь, не очень доступное объяснение. Попробуем иначе.

Замыкания (closure), или замыкающие выражения, — это сгруппированный программный код, который может быть передан в виде параметра и многократно использован. Ничего не напоминает? Если вы скажете, что в этом определении узнали функции, то будете полностью правы. Поговорим об этом подробнее.

16.1. Виды замыканий

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

В общем случае замыкание (closure) может принять две формы:

• именованная функция;

• безымянная функция, определенная с помощью облегченного синтаксиса.

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

ПРИМЕЧАНИЕ В дальнейшем безымянные функции будут именоваться замыканиями, или замыкающими выражениями. Говоря о функции, мы будем иметь в виду именно функции, а говоря о замыканиях — о безымянных функциях.

16.2. Введение в безымянные функции

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

Синтаксис

{ (входные_параметры) -> тип in

    // тело замыкающего выражения

}

• входные_параметры — список аргументов замыкания с указанием их имен и типов.

• Тип — тип данных значения, возвращаемого замыканием.

Замыкающее выражение пишется в фигурных скобках. После указания перечня входных аргументов и типа возвращаемого значения ставится ключевое слово in, после которого следует тело замыкания.

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

Пример

// безымянная функция в качестве значения константы

let functionInLet = {return true}

// вызываем безымянную функцию

functionInLet() // true

Константа functionInLet имеет функциональный тип () -> Bool (ничего не принимает на вход, но возвращает логическое значение) и хранит в себе тело функции.

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

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

В нашей программе объявлена переменная wallet, хранящая в себе программный аналог кошелька с купюрами (в предыдущей главе мы уже использовали подобный массив-кошелек). Каждый элемент этой коллекции представляет собой одну купюру определенного номинала. Перед нами стоит задача отбора купюр в кошельке по различным условиям. Для каждого условия может быть создана отдельная функция, принимающая на вход массив wallet и возвращающая отфильтрованную коллекцию.

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

Листинг 16.1

// массив с купюрами

var wallet = [10,50,100,100,5000,100,50,50,500,100]

// функция отбора купюр

func handle100(wallet: [Int]) -> [Int] {

    var returnWallet = [Int]()

    for banknot in wallet {

        if banknot==100{

            returnWallet.append(banknot)

        }

    }

    return returnWallet

}

// вызов функции отбора купюр достоинством 100

handle100(wallet: wallet) // [100, 100, 100, 100]

При каждом вызове функция handle100(wallet:) будет возвращать массив сторублевых купюр переданного массива-кошелька.

Но условия отбора не ограничиваются данной функцией. Расширим функционал нашей программы, написав дополнительную функцию для отбора купюр достоинством 1000 рублей и более (листинг 16.2).

Листинг 16.2

func handleMore1000(wallet: [Int]) -> [Int] {

    var returnWallet = [Int]()

    for banknot in wallet {

        if banknot>=1000{

            returnWallet.append(banknot)

        }

    }

    return returnWallet

}

// вызов функции отбора купюр достоинством более или равным 1000

handleMore1000(wallet: wallet) // [5000]

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

Для решения проблемы дублирования можно пойти двумя путями:

1) реализовать весь функционал отбора купюр в пределах одной функции, а в качестве входного аргумента передавать условие;

2) реализовать весь функционал в виде трех функций. Первая будет группировать повторяющийся код и принимать в виде аргумента одну из двух других функций. Переданная функция будет производить проверку условия в теле главной функции.

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

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

1. Функция с именем handle, принимающая массив-кошелек и условие отбора (в виде имени функции) в качестве входных аргументов и возвращающая массив отобранных купюр. В теле функции будут поочередно проверяться элементы входного массива на соответствие переданному условию.

2. Функция с именем compare100, принимающая на вход значение очередного элемента массива-кошелька, производящая сравнение с целым числом 100 и возвращающая логический результат этой проверки.

3. Функция с именем compareMore1000, аналогичная compare100, но производящая проверку на соответствие целому числу 1000.

В листинге 16.3 показана реализация описанного алгоритма.

Листинг 16.3

// единая функция формирования результирующего массива

func handle(wallet: [Int], closure: (Int) -> Bool) -> [Int] {

    var returnWallet = [Int]()

    for banknot in wallet {

        if closure(banknot) {

            returnWallet.append(banknot)

        }

    }

    return returnWallet

}

// функция сравнения с числом 100

func compare100(banknot: Int) ->Bool {

    return banknot==100

}

// функция сравнения с числом 1000

func compareMore1000(banknot:Int) -> Bool {

    return banknot>=1000

}

// отбор

let resultWalletOne = handle(wallet: wallet, closure: compare100)

let resultWalletTwo = handle(wallet: wallet, closure: compareMore1000)

Функция handle(wallet:closure:) получает в качестве входного параметра closure одну из функций проверки условия и в операторе if вызывает переданную функцию. Функции проверки принимают на вход анализируемую купюру и возвращают Bool в зависимости от результата сравнения. Чтобы получить купюры определенного достоинства, необходимо вызвать функцию handle(wallet:closure:) и передать в нее имя одной из функций проверки.

В итоге мы получили очень качественный и легкочитаемый код.

Представим, что возникла необходимость написать функции для отбора купюр по множеству условий (найти все полтинники; все купюры достоинством менее 1000 рублей; все купюры, которые без остатка делятся на 200, и т.д.). В определенный момент писать отдельную функцию проверки для каждого из них станет довольно тяжелой задачей, так как для того, чтобы использовать единую функцию ­проверки, ­необходимо знать имя проверяющей функции, а их могут быть десятки.

В подобной ситуации можно отказаться от создания отдельных функций и передавать в handle(wallet:closure:) условие отбора в виде безымянной функции. В листинге 16.4 показано, каким образом это может быть реализовано.

Листинг 16.4

// отбор купюр достоинством выше 1000 рублей

// аналог передачи compare100

handle(wallet: wallet, closure: {(banknot: Int) -> Bool in

    return banknot>=1000

})

// отбор купюр достоинством 100 рублей

// аналог передачи compareMore1000

handle(wallet: wallet, closure: {(banknot: Int) -> Bool in

    return banknot==100

})

Входной аргумент closure имеет функциональный тип (Int)->Bool, а значит, передаваемая безымянная функция должна иметь тот же тип данных, что мы и видим в коде.

Для переданного замыкания указан входной параметр типа Int и определен тип возвращаемого значения (Bool). После ключевого слова in следует тело функции, в котором с помощью оператора return возвращает логическое значение — результат проверки очередного элемента кошелька. Таким образом, в теле функции handle(wallet:closure:) будет вызываться не какая-то внешняя функция, имя которой передано, а безымянная функция, переданная в виде входного аргумента.

В результате такого подхода необходимость в существовании функций compare100(banknot:) и compareMore1000(banknot:) отпадает, так как код условия передается напрямую в качестве замыкания в аргумент closure.

ПРИМЕЧАНИЕ Далее в качестве примера будет производиться работа только с функцией отбора купюр достоинством больше или равным 1000 рублей.

16.3. Возможности замыканий

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

Пропуск указания типов

При объявлении входного параметра closure в функции handle(wal­let:closure:) указывается его функциональный тип (Int)->Bool, поэтому при передаче замыкающего выражения можно опустить данную информацию, оставив лишь имя входного аргумента (листинг 16.5).

Листинг 16.5

// отбор купюр достоинством выше 1000 рублей

handle(wallet: wallet, closure: {banknot in

    return banknot>=1000

})

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

Неявное возвращение значения

Если тело замыкающего выражения содержит всего одно выражение, которое возвращает некоторое значение (с использованием оператора return), то такие замыкания могут неявно возвращать выходное значение. Неявно — значит без использования оператора return (лис­тинг 16.6).

Листинг 16.6

handle(wallet: wallet,

    closure: {banknot in banknot>=1000})

Сокращенные имена параметров

В случае, когда замыкание состоит из одного выражения, можно опустить входные параметры (все до ключевого слова in, включая само слово). При этом доступ к аргументам внутри тела замыкания необходимо осуществлять через сокращенные имена в форме $номер_параметра. Номера входных параметров начинаются с нуля.

ПРИМЕЧАНИЕ В сокращенной форме записи имен входных параметров обозначение $0 указывает на первый передаваемый аргумент. Для доступа ко второму аргументу необходимо использовать обозначение $1, к третьему — $2 и т.д.

Перепишем вызов функции handle(wallet:closure:) с использованием сокращенных имен (листинг 16.7).

Листинг 16.7

handle(wallet: wallet,

    closure: {$0>=1000})

Здесь $0 — это аргумент banknot входного аргумента замыкания closure в функции handle(wallet:closure:).

Вынос замыкания за скобки

Если входной параметр-функция расположен последним в списке входных параметров функции (как в данном случае в функции handle(wallet:closure:), где параметр closure является последним), Swift позволяет вынести его значение (тело замыкающего выражения) за круглые скобки (листинг 16.8).

Листинг 16.8

handle(wallet: wallet){$0>=1000}

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

Листинг 16.9

handle(wallet: wallet){

banknot in

    for number in Array(arrayLiteral: 100,500) {

        if number == banknot {

            return true

        }

    }

    return false

}

ПРИМЕЧАНИЕ Существует и другой способ реализовать проверку из предыдущего листинга. Для этого можно использовать метод contains(_:), передавая в него очередную купюру:

var successBanknots = handle(wallet: wallet)

    { Array(arrayLiteral: 100,500).contains($0) }

successBanknots //[100, 100, 100, 500, 100]

16.4. Безымянные функции в параметрах

В листинге 16.10 показан пример инициализации замыкания в параметр closure. При этом у параметра явно указан функциональный тип (ранее в примерах он определялся неявно).

Листинг 16.10

var closure: () -> () = {

    print("Замыкающее выражение")

}

closure()

Консоль

Замыкающее выражение

Так как данное замыкающее выражение не имеет входных параметров и возвращаемого значения, то его тип равен ()->(). Для вызова записанного в константу замыкающего выражения необходимо написать имя константы с круглыми скобками, точно так же, как мы делали это ранее.

Явное указание функционального типа позволяет определить входные аргументы и тип выходного значения (листинг 16.11).

Листинг 16.11

// передача в функцию строкового значения

var closurePrint: (String) -> () = {text in

    print(text)

}

closurePrint("Text")

// передача в функцию целочисленных значений

// с осуществлением доступа через краткий синтаксис $0 и $1

var sum: (_ numOne: Int, _ numTwo: Int) -> Int = {

    return $0 + $1

}

sum(10, 34) // 44

Консоль

Text

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

16.5. Пример использования замыканий при сортировке массива

Swift предлагает большое количество функций и методов, позволяющих значительно упростить разработку приложений. Одним из таких методов является sorted(by:), предназначенный для сортировки массивов, как строковых, так и числовых. Он принимает на входе массив, который необходимо отсортировать, и условие сортировки.

Принимаемое условие сортировки — это обыкновенное замыкающее выражение, которое вызывается внутри метода sorted(by:), принимает на входе два очередных элемента сортируемого массива и возвращает значение Bool в зависимости от результата их сравнения.

В листинге 16.12 отсортируется массив array таким образом, чтобы элементы были расположены по возрастанию. Для этого в метод sorted(by:) передается замыкающее выражение, которое возвращает true, когда второе из сравниваемых чисел больше.

Листинг 16.12

var array = [1,44,81,4,277,50,101,51,8]

var sortedArray = array.sorted(by: { (first: Int, second: Int) -> Bool in

    return first < second

})

sortedArray //[1, 4, 8, 44, 50, 51, 81, 101, 277]

Теперь применим все рассмотренные ранее способы оптимизации замыкающих выражений:

• уберем функциональный тип замыкания;

• уберем оператор return;

• заменим имена переменных сокращенной формой.

В результате получится выражение, приведенное в листинге 16.13. Как и в предыдущем примере, здесь тоже необходимо отсортировать массив array таким образом, чтобы элементы были расположены по возрастанию. Для этого в метод sorted(by:) передается такое замыкающее выражение, которое возвращает true, когда второе из сравниваемых чисел больше.

Листинг 16.13

sortedArray = array.sorted(by: {$0<$1})

sortedArray //[1, 4, 8, 44, 50, 51, 81, 101, 277]

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

Но и это еще не все. Так как выражение в замыкании состоит всего из одного бинарного оператора, то можно убрать даже имена параметров, оставив лишь оператор сравнения (листинг 16.14).

Листинг 16.14

sortedArray = array.sorted(by: <)

sortedArray //[1, 4, 8, 44, 50, 51, 81, 101, 277]

Надеюсь, вы приятно удивлены потрясающими возможностями Swift!

16.6. Захват переменных

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

Синтаксис захвата переменных

Обратимся к примеру в листинге 16.15. Существуют два параметра, a и b, которые не передаются в качестве входных аргументов в замыкание, но используются им в вычислениях. При каждом вызове такого замыкания оно будет определять значения данных параметров, прежде чем приступить к выполнению операции с их участием.

Листинг 16.15

var a = 1

var b = 2

let closureSum : () -> Int = {

    return a+b

}

closureSum() // 3

a = 3

b = 4

closureSum() // 7

Замыкание, хранящееся в константе closureSum, складывает значения переменных a и b. При изменении их значений возвращаемое замыканием значение меняется.

Существует способ «захватить» значения параметров, то есть зафиксировать те значения, которые имеют эти параметры на момент объявления замыкающего выражения. Для этого в начале замыкания в квадратных скобках необходимо перечислить захватываемые переменные, разделив их запятой, после чего указать ключевое слово in.

Перепишем инициализированное переменной closureSum замыкание таким образом, чтобы оно захватывало первоначальные значения переменных a и b (листинг 16.16).

Листинг 16.16

var a = 1

var b = 2

let closureSum : () -> Int = {

    [a,b] in

    return a+b

}

closureSum() // 3

a = 3

b = 4

closureSum() // 3

Замыкание, хранящееся в константе closureSum, складывает значения перемененных a и b. При изменении этих значений возвращаемое замыканием значение не меняется.

Захват вложенной функцией

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

Рассмотрим пример из листинга 16.17.

Листинг 16.17

func makeIncrement(forIncrement amount: Int) -> () -> Int {

    var runningTotal = 0

    func increment() -> Int {

        runningTotal += amount

        return runningTotal

    }

    return increment

}

Функция makeIncrement(forIncrement:) возвращает значение с функцио­нальным типом ()->Int. Это значит, что вернется замыкание, не имеющее входных аргументов и возвращающее целочисленное значение.

Функция makeIncrement(forIncrement:) использует два параметра:

• runningTotal — переменную типа Int, объявляемую в теле функции. Именно ее значение является результатом работы всей конструкции;

• amount — входной аргумент, имеющий тип Int. Он определяет, насколько увеличится значение runningTotal при очередном обращении.

Вложенная функция increment() не имеет входных или объявляемых параметров, но при этом обращается к runningTotal и amount внутри своей реализации. Она делает это в автоматическом режиме путем захвата значений обоих параметров по ссылке. Захват значений по ссылке гарантирует, что измененные значения параметров не исчезнут после окончания работы функции makeIncrement(forIncrement:) и будут доступны при повторном вызове функции increment().

Теперь обратимся к листингу 16.18.

Листинг 16.18

var incrementByTen = makeIncrement(forIncrement: 10)

var incrementBySeven = makeIncrement(forIncrement: 7)

incrementByTen()   // 10

incrementByTen()   // 20

incrementByTen()   // 30

incrementBySeven() // 7

incrementBySeven() // 14

incrementBySeven() // 21

В переменных incrementByTen и incrementBySeven хранятся возвращаемые функцией makeIncrement(forIncrement:) замыкания. В первом случае значение runningTotal увеличивается на 10, а во втором — на 7. Каждая из переменных хранит свою копию захваченного значения runningTotal, именно поэтому при их использовании увеличиваемые значения не пересекаются и увеличиваются независимо друг от друга.

Внимание Так как в переменных incrementByTen и incrementBySeven хранятся замыкания, то при доступе к ним после их имени необходимо использовать скобки (по аналогии с доступом к функциям).

16.7. Замыкания передаются по ссылке

Функциональный тип данных — это ссылочный тип (reference type). Это значит, что замыкания передаются не копированием, а с помощью ссылки на область памяти, где хранится это замыкание.

Рассмотрим пример, описанный в листинге 16.19.

Листинг 16.19

var incrementByFive = makeIncrement(forIncrement: 5)

var copyIncrementByFive = incrementByFive

В данном примере используется функция makeIncre­ment(forIn­crement:), объявленная ранее. Напомню, она возвращает замыкание типа ()->Int, которое в данном случае предназначено для увеличения значения на 5. Возвращаемое замыкание записывается в переменную incrementByFive, после чего копируется в переменную copyIncrementByFive. В результате можно обратиться к одному и тому же замыканию, используя как copyIncrementByFive, так и incrementByFive (листинг 16.20).

Листинг 16.20

incrementByFive() // 5

copyIncrementByFive() // 10

incrementByFive() // 15

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

16.8. Автозамыкания

Автозамыкания — это замыкания, которые автоматически создаются из переданного выражения. Иными словами, может существовать функция, имеющая один или несколько входных параметров, которые при ее вызове передаются как значения, но во внутренней реализации функции используются как самостоятельные замыкания. Рассмотрим пример из листинга 16.21.

Листинг 16.21

var arrayOfNames = ["Helga", "Bazil", "Alex"]

func printName(nextName: String ) {

    print(nextName)

}

printName(nextName: arrayOfNames.remove(at: 0))

Консоль

Helga

При каждом вызове функции printName(nextName:) в качестве входного значения ей передается результат вызова метода remove(at:) массива arrayOfNames.

Независимо от того, в какой части функции будет использоваться переданный параметр (или не будет использоваться вовсе), значение, возвращаемое методом remove(at:), будет вычислено в момент вызова функции printName(nextName:). Получается, что передаваемое значение вычисляется независимо от того, нужно ли оно в ходе выполнения функции.

Отличным решением данной проблемы станет использование ленивых вычислений, то есть таких вычислений, которые будут выполняться лишь в тот момент, когда это понадобится. Для того чтобы реализовать этот подход, можно передавать в функцию printName(nextName:) замыкание, которое будет вычисляться в тот момент, когда к нему обратятся (листинг 16.22).

Листинг 16.22

func printName(nextName: ()->String) {

    // какой-либо код

    print(nextName())

}

printName(nextName: {arrayOfNames.remove(at: 0)})

Консоль

Helga

Для решения этой задачи потребовалось изменить тип входного параметра nextName на ()->String и заключить передаваемый метод remove(at:) в фигурные скобки. Теперь внутри реализации функции printName(nextName:) к входному аргументу nextName необходимо обращаться как к самостоятельной функции (с использованием круглых скобок после имени параметра). Таким образом, значение метода remove(at:) будет вычислено именно в тот момент, когда оно понадобится, а не в тот момент, когда оно будет передано. Единственным недостатком данного подхода является то, что входной параметр должен быть заключен в фигурные скобки, а это несколько усложняет использование функции и чтение кода.

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

Для реализации автозамыкания необходимо следующее:

• Входной аргумент должен иметь функциональный тип.

В примере, приведенном ранее, аргумент nextName уже имеет функциональный тип ()->String.

• Функциональный тип не должен определять типы входных параметров.

В примере типы входных параметров не определены (пустые скобки).

• Функциональный тип должен определять тип возвращаемого значения.

В примере тип возвращаемого значения определен как String.

• Переданное выражение должно возвращать значение того же типа, которое определено в функциональном типе замыкания.

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

• Перед функциональным типом необходимо использовать атрибут @autoclosure.

• Передаваемое значение должно указываться без фигурных скобок.

Перепишем код из предыдущего листинга в соответствии с указанными требованиями (листинг 16.23).

Листинг 16.23

func printName(nextName: @autoclosure ()->String) {

    print(nextName())

}

printName(nextName: arrayOfNames.remove(at: 0))

Консоль

Helga

Теперь метод remove(at:) передается в функцию printName(nextName:) как обычный аргумент, без использования фигурных скобок, но внутри тела используется как самостоятельная функция.

Ярким примером глобальной функции, входящей в стандартные возможности Swift и использующей механизм автозамыканий, является функция assert(condition:message). Аргументы condition и message — это автозамыкания, первое из которых вычисляется только в случае активного debug-режима, а второе — только в случае, когда condition соответствует false.

ПРИМЕЧАНИЕ Это еще одна встреча с так называемыми ленивыми вычислениями, о которых мы начали говорить в предыдущей главе.

16.9. Выходящие замыкания

Как вы уже неоднократно убеждались и убедитесь еще не раз, Swift — очень умный язык программирования. Он старается экономить ваши ресурсы там, где вы можете об этом даже не догадываться.

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

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

Рассмотрим пример. Предположим, что в программе есть специальная переменная, предназначенная для хранения замыканий типа ()->Int, то есть являющаяся коллекцией замыканий (листинг 16.24).

Листинг 16.24

var arrayOfClosures: [()->Int] = []

Пока еще пустой массив arrayOfClosures может хранить в себе замыкания с функциональным типом ()->Int. Реализуем функцию, добавляющую в этот массив переданные ей в качестве аргументов замыкания (листинг 16.25).

Листинг 16.25

func addNewClosureInArray(_ newClosure: ()->Int){

    arrayOfClosures.append(newClosure) // ошибка

}

Xcode сообщит вам об ошибке. И тому есть две причины:

• Замыкание — это тип-ссылка, то есть оно передается по ссылке, но не копированием.

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

Для решения этой проблемы необходимо указать, что замыкание, хранящееся в переменной newClosure, является выходящим. Для этого перед описанием функционального типа данного аргумента укажите атрибут @escaping, после чего вы сможете передать в функцию addNewClosureInArray(_:) произвольное замыкание (листинг 16.26).

Листинг 16.26

func addNewClosureInArray(_ newClosure: @escaping ()->Int){

    arrayOfClosures.append(newClosure)

}

addNewClosureInArray({return 100})

addNewClosureInArray{return 1000}

arrayOfClosures[0]() // 100

arrayOfClosures[1]() // 1000

Обратите внимание на то, что в одном случае замыкание передается с круглыми скобками, а в другом — без них. Так как функция addNewClosureInArray(_:) имеет один входной аргумент, то допускаются оба варианта.

ПРИМЕЧАНИЕ Если вы передаете замыкание в виде параметра, то можете использовать модификатор inout вместо @escaping.

16.10. Каррирование функций

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

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

Разложим все по полочкам.

У нас есть функция с типом (Int, Int, Int)->Int, которая зависит от трех входных аргументов типа Int и возвращает значение типа Int.

При каррировании мы получим функцию типа (Int)->(Int)->(Int)->Int. Для этого выполним следующие шаги:

• обозначим функциональный тип (Int)->Int последней функции в цепочке как А. Тогда каррированная функция будет выглядеть как (Int)->(Int)->A;

• обозначим функциональный тип (Int)->A как B. Тогда каррированная функция будет выглядеть как (Int)->B.

Получается, что наша функция принимает на вход одно целое число и возвращает значение типа B.

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

Значение типа A также является функцией, которая принимает на вход одно целое число и возвращает одно целое число.

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

Рассмотрим пример каррирования. Существует функция с типом (Int, Int)->Int, которая получает на вход два целочисленных значения, производит сложение и возвращает его результат в виде целого числа (листинг 16.27).

Листинг 16.27

func sum(x: Int, y: Int) -> Int {

    return x + y

}

sum(x: 1,y: 4) // 5

С целью каррирования напишем новую функцию, которая принимает на вход всего один целочисленный параметр, а возвращает функцию типа (Int)->Int (листинг 16.28).

Листинг 16.28

func sumTwo(_ x: Int) -> (Int) -> Int {

    return {  return $0+x }

}

var anotherClosure = sumTwo(1)

anotherClosure(12) // 13

Переменная anotherClosure получает в качестве значения, которому мы можем передать входной параметр, безымянную функцию. Прелесть каррирования в том, что мы можем объединить вызов функции sum2(x:) и передачу значения в возвращаемое ею замыкание (лис­тинг 16.29).

Листинг 16.29

sumTwo(5)(12) // 17

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

Если говорить о пользе, то каррирование хорошо именно тем, что устраняет зависимость функции от нескольких параметров. Мы можем вызвать функцию, как только получим первое нужное значение, и далее при необходимости многократно вызывать возвращенное ею замыкание при поступлении второго требуемого параметра (лис­тинг 16.30).

Листинг 16.30

var sumClosure = sumTwo(1)

sumClosure(12) // 13

sumClosure(19) // 20

Назад: 15. Функции
Дальше: 17. Дополнительные возможности