Swift замыкания: Замыкания — SwiftBook – Используем замыкания в Swift по полной / Habr

Содержание

Замыкания — SwiftBook

Замыкания — это самодостаточные блоки с определенным функционалом, которые могут быть переданы и использованы в вашем коде. Замыкания в Swift похожи на блоки в C и Objective-C, и лямбды в других языках программирования.

Замыкания могут захватывать и хранить ссылки на любые константы и переменные из контекста, в котором они объявлены. Эта процедура известна как заключение этих констант и переменных, отсюда и название «замыкание». Swift выполняет всю работу с управлением памятью при захвате за вас.

Заметка

Не волнуйтесь, если вы не знакомы с понятием «захвата»(capturing). Это объясняется более подробно ниже в главе Захват значений.

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

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

Выражения замыкания в Swift имеют четкий, ясный, оптимизированный синтаксис в распространенных сценариях. Эти оптимизации включают:

  • Вывод типа параметра и возврат типа значения из контекста
  • Неявные возвращающиеся значения однострочных замыканий
  • Сокращенные имена параметров
  • Синтаксис последующих замыканий

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

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

Метод sorted

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

Примеры замыкающих выражений ниже используют метод sorted(by:)для сортировки массива из String значений в обратном алфавитном порядке. Вот исходный массив для сортировки:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

Замыкание метода sorted(by:) принимает два аргумента одного и того же типа, что и содержимое массива, и возвращает Bool значение, которое решает поставить ли первое значение перед вторым, или после второго. Замыкание сортировки должно вернуть true, если первое значение должно быть до второго значения, и false в противном случае.

Этот пример сортирует массив из String значений, так что сортирующее замыкание должно быть функцией с типом (String, String) -> Bool.

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

func backward(_ s1: String, _ s2: String) -> Bool {
   return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames равен ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

Если первая строка (s1) больше чем вторая строка (s2), функция backward(_:_:) возвращает true, что указывает, что s1 должна быть перед s2 в сортированном массиве. Для символов в строках, «больше чем» означает «появляется в алфавите позже, чем». Это означает что буква «B» «больше чем» буква «А», а строка «Tom» больше чем строка «Tim». Это делает обратную алфавитную сортировку, с «Barry» поставленным перед «Alex», и так далее.

Тем не менее, это довольно скучный способ написать то, что по сути, является функцией с одним выражением (a > b). В этом примере, было бы предпочтительнее написать сортирующее замыкание в одну строку, используя синтаксис замыкающего выражения.

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

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

  1. { (параметры) -> тип результата in
  2. выражения
  3. }

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

Пример ниже показывает версию функции backwards(_:_:) с использованием замыкающего выражения:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
   return s1 > s2
})

Обратите внимание, что объявление типов параметров и типа возвращаемого значения для этого однострочного замыкания идентично объявлению из функции backwards(_:_:). В обоих случаях, оно пишется в виде (s1: String, s2: String) -> Bool. Тем не менее, для однострочных замыкающих выражений, параметры и тип возвращаемого значения пишутся внутри фигурных скобок, а не вне их.

Начало тела замыкания содержит ключевое слово in. Это ключевое слово указывает, что объявление параметров и возвращаемого значения замыкания закончено, и тело замыкания вот-вот начнется.

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

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })

Это показывает, что общий вызов метода sorted остался прежним. Пара скобок по-прежнему обособляют весь набор параметров метода.

Определение типа из контекста

Поскольку сортирующее замыкание передается как аргумент метода, Swift может вывести типы его параметров и тип возвращаемого значения, через тип параметра метода sorted(by:). Этот параметр ожидает функцию имеющую тип (String, String) -> Bool. Это означает что типы (String, String) и Bool не нужно писать в объявлении замыкающего выражения. Поскольку все типы могут быть выведены, стрелка результата ( -> ) и скобки вокруг имен параметров также могут быть опущены:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

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

Тем не менее, вы всё равно можете явно указать типы, если хотите. И делать это предполагается, если это поможет избежать двусмысленности для читателей вашего кода. В случае с методом sorted(by:), цель замыкания понятна из того факта, что сортировка происходит, и она безопасна для читателя, который может предположить, что замыкание, вероятно, будет работать со значениями String, поскольку оно помогает сортировать массив из строк.

Неявные возвращаемые значения из замыканий с одним выражением

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

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

Здесь, функциональный тип аргумента метода sorted(by:)дает понять, что замыкание вернет Bool значение. Поскольку тело замыкания содержит одно выражение (s1 > s2), которое возвращает Bool значение, то нет никакой двусмысленности, и ключевое слово return можно опустить.

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

Swift автоматически предоставляет сокращённые имена для однострочных замыканий, которые могут быть использованы для обращения к значениям параметров замыкания через имена $0, $1, $2, и так далее.

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

reversedNames = names.sorted(by: { $0 > $1 })

Здесь, $0 и $1 обращаются к первому и второму String параметру замыкания.

Операторные функции

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

reversedNames = names.sorted(by: >)

Более подробную информацию о операторных функциях смотрите в разделе Операторные функции.

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

func someFunctionThatTakesAClosure(closure: () -> Void) {
   // тело функции
}
 
// Вот как вы вызываете эту функцию без использования последующего замыкания:
 
someFunctionThatTakesAClosure(closure: {
   // тело замыкания
})
 
// Вот как вы вызываете эту функцию с использованием последующего замыкания:
 
someFunctionThatTakesAClosure() {
   // тело последующего замыкания
}

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

reversedNames = names.sorted() { $0 > $1 }

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

reversedNames = names.sorted { $0 > $1 }

Последующие замыкания полезны в случаях, когда само замыкание достаточно длинное, и его невозможно записать в одну строку. В качестве примера приведем вам метод map(_:) типа Array в языке Swift, который принимает выражение замыкания как его единственный аргумент. Замыкание вызывается по одному разу для каждого элемента массива и возвращает альтернативную отображаемую величину (возможно другого типа) для этого элемента. Природа отображения и тип возвращаемого значения определяется замыканием.

После применения замыкания к каждому элементу массива, метод map(_:) возвращает новый массив, содержащий новые преобразованные величины, в том же порядке, что и в исходном массиве.

Вот как вы можете использовать метод map(_:) вместе с последующим замыканием для превращения массива значений типа Int в массив типа String. Массив [16, 58, 510] используется для создания нового массива [«OneSix», «FiveEight», «FiveOneZero»] :

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

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

Вы можете использовать массив numbers для создания значений типа String, передав замыкающее выражение в метод map(_:) массива в качестве последующего замыкания. Обратите внимание, что вызов number.map не включает в себя скобки после map, потому что метод map(_:) имеет только один параметр, который мы имеем в виде последующего замыкания:

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}

//тип строк был выведен как [String]
//значения ["OneSix", "FiveEight", "FiveOneZero"]

Метод map(_:) вызывает замыкание один раз для каждого элемента массива. Вам не нужно указывать тип входного параметра замыкания, number, так как тип может быть выведен из значений массива, который применяет метод map.

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

Замыкающее выражение строит строку, названную output, каждый раз, когда оно вызывается. Оно рассчитывает последнюю цифру number, используя оператор деления с остатком ( number % 10 ) и использует затем эту получившуюся цифру, чтобы найти соответствующую строку в словаре digitNames. Это замыкание может быть использовано для создания строкового представления любого целого числа, большего чем 0.

Заметка

Вызов словаря digitNames синтаксисом сабскрипта сопровождается знаком (!), потому что сабскрипт словаря возвращает опциональное значение, так как есть такая вероятность, что такого ключа в словаре может и не быть. В примере выше мы точно знаем, что number % 10 всегда вернет существующий ключ словаря digitNames, так что восклицательный знак используется для принудительного извлечения значения типа String в возвращаемом опциональном значении сабскрипта.

Строка, полученная из словаря digitNames, добавляется в начало переменной output, путем правильного формирования строковой версии числа наоборот.(Выражение number % 10 дает нам 6 для 16, 8 для 58 и 0 для 510).

Переменная number после вычисления остатка делится на 10. Так как тип значения Int, то наше число округляется вниз, таким образом 16 превращается в 1, 58 в 5, 510 в 51.

Процесс повторяется пока number /= 10 не станет равным 0, после чего строка output возвращается замыканием и добавляется к выходному массиву функции map(_:).

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

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

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

Вот пример функции makeIncrementer, которая содержит вложенную функцию incrementer. Вложенная функция incrementer() захватывает два значения runningTotal и amount из окружающего контекста. После захвата этих значений incrementer возвращается функцией makeIncrementer как замыкание, которое увеличивает runningTotal на amount каждый раз как вызывается.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
   var runningTotal = 0
   func incrementer() -> Int {
      runningTotal += amount
      return runningTotal
   }
   return incrementer
}

Возвращаемый тип makeIncrementer Void -> Int. Это значит, что он возвращает функцию, а не простое значение. Возвращенная функция не имеет параметров и возвращает Int каждый раз как ее вызывают. Узнать как функции могут возвращать другие функции можно в главе «Функциональные типы как возвращаемые типы».

Функция makeIncrementer(forIncrement:) объявляет целочисленную переменную runningTotal, для хранения текущего значения инкрементора, которое будет возвращено. Переменная инициализируется значением 0.

Функция makeIncrementer(forIncrement:) имеет единственный параметр Int с внешним именем forIncrement и локальным именем amount. Значение аргумента передается этому параметру, определяя на сколько должно быть увеличено значение runningTotal каждый раз при вызове функции.

Функция makeIncrementer объявляет вложенную функцию incrementer, которая непосредственно и занимается увеличением значения. Эта функция просто добавляет amount к runningTotal и возвращает результат.

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

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

Функция incrementer() не имеет ни одного параметра и она ссылается на runningTotal и amount внутри тела функции. Она делает это, захватывая существующие значения от runningTotal и amount из окружающей функции и используя их внутри. Захват ссылки дает гарантию того, что runningTotal не исчезнет при окончании вызова makeIncrementer и гарантирует, что runningTotal останется переменной в следующий раз, когда будет вызвана функция incrementer().

Заметка

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

Приведем пример makeIncrementer в действии:

let incrementByTen = makeIncrementer(forIncrement: 10)

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

incrementByTen()
// возвращает 10
incrementByTen()
// возвращает 20
incrementByTen()
// возвращает 30

Если вы создаете второй инкрементор, он будет иметь свою собственную ссылку на новую отдельную переменную runningTotal :

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
//возвращает значение 7

Повторный вызов первоначального инкрементора ( incrementByTen ) заставит увеличиваться его собственную переменную runningTotal и никак не повлияет на переменную, захваченную в incrementBySeven :

incrementByTen()
//возвращает 40
Заметка

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

В примере выше incrementBySeven и incrementByTen константы, но замыкания, на которые ссылаются эти константы имеют возможность увеличивать значение переменных runningTotal, которые они захватили. Это из-за того, что функции и замыкания являются ссылочными типами.

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

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

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
//возвращает 50

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

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

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
  completionHandlers.append(completionHandler)
}

Функция someFunctionWithEscapingClosure(_:) принимает и добавляет в массив замыкание, объявленное за пределами функции. Если вы не поставите маркировку @escaping, то получите ошибку компиляции.

Определение замыкания через @escaping означает, что вы должны сослаться на self явно внутри самого замыкания. Например, в коде ниже, замыкание передается функции someFunctionWithEscapingClosure(_:) в виде сбегающего замыкания, так что вам нужно ссылаться на self внутри него явно. С другой стороны, замыкание, передаваемое в someFunctionWithNonescapingClosure(_:) является несбегающим, значит вы можете ссылаться на self неявно.

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}
 
class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}
 
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Выведет "200"
 
completionHandlers.first?()
print(instance.x)
// Выведет "100"

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

Нет ничего необычного в вызове функций, которые принимают автозамыкания, но необычным является реализовывать такие функции. Например, функция assert(condition:message:file:line:) принимает автозамыкания на место condition и message параметров. Ее параметр condition вычисляется только в сборке дебаггера, а параметр message вычисляется, если только condition равен false.

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

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Выведет "5"
 
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Выведет "5"
 
print("Now serving \(customerProvider())!")
// Выведет "Now serving Chris!"
print(customersInLine.count)
// Выведет "4"

Даже если первый элемент массива customersInLine удаляется кодом внутри замыкания, элемент массива фактически не удаляется до тех пор пока само замыкание не будет вызвано. Если замыкание так и не вызывается, то выражение внутри него никогда не выполнится и, соответственно, элемент не будет удален из массива. Обратите внимание, что customerProvider является не String, а () -> String, то есть функция не принимает аргументов, но возвращает строку. Вы получите то же самое поведение, когда сделаете это внутри функции:

// customersInLine равен ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Выведет "Now serving Alex!"

Функция serve(customer:) описанная выше принимает явное замыкание, которое возвращает имя клиента. Версия функции serve(customer:) ниже выполняет ту же самую операцию, но вместо использования явного замыкания, она использует автозамыкание, поставив маркировку при помощи атрибута @autoclosure. Теперь вы можете вызывать функцию, как будто бы она принимает аргумент String вместо замыкания. Аргумент автоматически преобразуется в замыкание, потому что тип параметра customerProvider имеет атрибут @autoclosure.

// customersInLine равен ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Выведет "Now serving Ewa!"
Заметка

Слишком частое использование автозамыканий может сделать ваш код сложным для чтения. Контекст и имя функции должны обеспечивать ясность отложенности исполнения кода.

Если вы хотите чтобы автозамыкание могло сбежать, то вам нужно использовать оба атрибута и @autoclosure, и @escaping. Атрибут @escaping подробнее описан в главе Сбегающие замыкания.

// customersInLine равен ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
 
print("Collected \(customerProviders.count) closures.")
// Выведет "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Выведет "Now serving Barry!"
// Выведет "Now serving Daniella!"

В коде выше, вместо того, чтобы вызывать переданное замыкание в качестве аргумента customer, функция collectCustomerProviders(_:) добавляет замыкание к массиву customerProviders. Массив объявлен за пределами функции, что означает, что замыкание в массиве может быть исполнено после того, как функция вернет значение. В результате значение аргумента customerProvider должен иметь “разрешение” на “побег” из зоны видимости функции.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Используем замыкания в Swift по полной / Habr

Несмотря на то, что в Objective-C 2.0 присутствуют замыкания (известные как блоки), ранее эппловский API использовал их неохотно. Возможно, отчасти поэтому многие программисты под iOS с удовольствием эксплуатировали сторонние библиотеки, вроде AFNetworking, где блоки применяются повсеместно. С выходом Swift, а также добавлением новой функциональности в API, работать с замыканиями стало чрезвычайно удобно. Давайте рассмотрим, какими особенностями обладает их синтаксис в Swift, и какие трюки можно с ними «вытворять».

Продвигаться будем от простого к сложному, от скучного к веселому. Заранее приношу извинения за обильное использование мантр «функция», «параметр» и «Double», но из песни слов не выкинешь.


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) В теле замыкания указывается, какая операция будет выполняться над операндами.
Авторы 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.
___________________________________________________________________

UPD.: Реальное применение

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

Если вам нужно создать очередь с приоритетом, можно использовать двоичную кучу (binary heap). Как известно, это может быть как MinHeap, так и MaxHeap, т.е. кучи, где в корне дерева находится минимальный или максимальный элемент соотвественно. Базовая реализация MinHeap от MaxHeap будет отличаться по сути только проверочными сравнениями при восстановлении инварианта двоичной кучи после добавления/удаления элемента.

Таким образом, мы могли создать базовый класс BinaryHeap, который будет содержать свойство comparison типа (T, T) -> Bool. А конструктор этого класса будет принимать способ сравнения и затем использовать его в методах heapify. Прототип базового класса выглядел бы так:

class BinaryHeap<T: Comparable>: DebugPrintable {
    private var array: Array<T?>
    private var comparison: (T, T) -> Bool
    private var used: Int = 0

   // Бла-бла-бла

    // Internal Methods
    internal func removeTop() -> T? { //... }
    internal func getTop() -> T? { //... }
    
    // Public Methods:
    func addValue(value: T) {
        if used == self.array.count {
            self.grow()
        }
        
        self.array[used] = value
        heapifyToTop(used, comparison) // Одно из мест, где используется функция сравнения
        self.used++
    }

    init(size newSize: Int, comparison newComparison: (T, T) -> Bool) {
        array = [T?](count: newSize, repeatedValue: nil)
        comparison = newComparison
    }
}

Теперь для того, чтобы создать классы MinHeap и MaxHeap нам достаточно унаследоваться от BinaryHeap, а в их конструкторах просто явно указать, какое сравнение применять. Вот так будет выглядеть наши классы:
class MaxHeap<T: Comparable>: BinaryHeap<T> {
    func getMax() -> T? {
        return self.getTop()
    }
    
    func removeMax() -> T? {
        return self.removeTop()
    }
    
    init(size newSize: Int) {
        super.init(size: newSize, {$0 > $1})
    }
}
class MinHeap<T: Comparable>: BinaryHeap<T> {
    func getMin() -> T? {
        return self.getTop()
    }
    
    func removeMin() -> T? {
        return self.removeTop()
    }
    
    init(size newSize: Int) {
        super.init(size: newSize, {$0 <= $1})
    }
}

Swift | Замыкания

Замыкания

Последнее обновление: 01.01.2018

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

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

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

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

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

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

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


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

    инструкции
}

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

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


let hello = { print("Hello world")}
hello()
hello()

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

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

let hello: ()->Void = { print("Hello world")}

Дополнительно можно определить список параметров с помощью ключевого слова in:


let hello = {
    (message: String) in
    print(message)
}
hello("Hello")
hello("Salut")
hello("Ni hao")

В данном случае замыкание принимает один параметр — message, который представляет тип String. Список параметров указывается до ключевого слова in, а после идут инструкции функции.

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


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

Замыкания как аргументы функций

Как правило, анонимные функции используются в том месте, где они определены. Нередко анонимные функции передаются в другие функции в качестве параметра, если параметр представляет функцию:


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 мы можем передать замыкающее выражение.

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

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


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.

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


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 — второй параметр. Система автоматически распознает их и поймет, что они представляют числа.

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


let x = 10
let y = 12

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

let result2 = operation(x, y, -)
print(result2)    // -2

Система автоматически распознает,что должны выполняться операции сложения и вычитания двух переданных параметров, и поэтому результат будет тот же.

Доступ к контексту

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


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

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

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


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. Однако мы можем зафиксировать начальные значения переменных:


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], мы тем самым фиксируем их начальные значения. И даже если значения этих переменных в какой-то момент изменятся, замыкание будет оперировать прежними значениями.

Swift — Замыкания

Уважаемый пользователь! Реклама помогает поддерживать и развивать наш проект, делая его простым и удобным специально для Вас. Если проект интересный и важный для Вас, то отключите на нем блокировщик рекламы. Спасибо, что читаете сайт!

Замыкания — это автономные блоки функциональности, которые можно передавать и использовать в вашем коде. Замыкания в Swift похожи на блоки в C и Objective-C и на лямбды в других языках программирования.

Замыкания могут захватывать и хранить ссылки на любые константы и переменные из контекста, в котором они определены. Это называется закрытием этих констант и переменных. Swift обрабатывает все управление памятью захвата для вас.

ЗАМЕТКА

Не беспокойтесь, если вы не знакомы с концепцией захвата. Это подробно объясняется ниже в разделе «Захват ценностей» .

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

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

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

  • Вывод параметров и типов возвращаемых значений из контекста
  • Неявные возвраты от замыканий с одним выражением
  • Сокращенные имена аргументов
  • Синтаксис замыкающего замыкания

Закрытие выражений

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

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

Сортированный метод

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

Приведенные ниже примеры выражений замыкания используют sorted(by:)метод для сортировки массива Stringзначений в обратном алфавитном порядке. Вот начальный массив для сортировки:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:)Метод принимает замыкание , которое принимает два аргумента одного и того же типа, что и содержимое массива и возвращает Boolзначение сказать , должно ли первое значение перед или после второго значения как значения сортируются. Закрытие сортировки должно возвращаться, trueесли первое значение должно стоять перед вторым значением, и в falseпротивном случае.

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

Один из способов обеспечить закрытие сортировки — написать нормальную функцию правильного типа и передать ее в качестве аргумента sorted(by:)методу:

func backward(_ s1: String, _ s2: String) -> Bool {

return s1 > s2

}

var reversedNames = names.sorted(by: backward)

// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

Если первая строка ( s1) больше, чем вторая строка ( s2), backward(_:_:)функция вернется true, указывая, что s1должно быть раньше s2в отсортированном массиве. Для символов в строках «больше чем» означает «появляется позже в алфавите, чем». Это означает, что буква "B"«больше, чем» буква "A", а строка "Tom"больше, чем строка "Tim". Это дает обратный алфавитный вид с "Barry"размещением до "Alex"и так далее.

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

Синтаксис выражения закрытия

Синтаксис выражения замыкания имеет следующую общую форму:

{ (parameters) -> return type in

statements

}

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

Пример ниже показывает версию выражения замыкания backward(_:_:)функции сверху:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in

return s1 > s2

})

Обратите внимание, что объявление параметров и возвращаемого типа для этого встроенного замыкания идентично объявлению из backward(_:_:)функции. В обоих случаях это написано как . Однако для встроенного выражения замыкания параметры и возвращаемый тип записываются внутри фигурных скобок, а не вне их.(s1: String, s2: String) -> Bool

Начало тела замыкания вводится по inключевому слову. Это ключевое слово указывает на то, что определение параметров и типа возврата замыкания завершено, и тело замыкания должно начаться.

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

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

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

Вывод типа из контекста

Поскольку закрытие сортировки передается методу в качестве аргумента, Swift может определить типы его параметров и тип возвращаемого значения. sorted(by:)Метод вызывается на массив строк, поэтому его аргумент должен быть функцией типа . Это означает , что и типам не должны быть написано как часть определения Выражения закупоривающего. Поскольку все типы могут быть выведены, стрелка возврата ( ) и круглые скобки вокруг имен параметров также могут быть опущены:(String, String) -> Bool(String, String)Bool->

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

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

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

Неявные возвраты из замыканий с одним выражением

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

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

Здесь тип функции sorted(by:)аргумента метода проясняет, что Boolзначение должно быть возвращено замыканием. Поскольку тело замыкания содержит единственное выражение ( ), которое возвращает значение, двусмысленности нет, и ключевое слово может быть опущено.s1 > s2Boolreturn

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

Swift автоматически предоставляет сокращенные имена аргументов встраивать затворы, которые могут быть использованы для обозначения значений аргументов замыкания по именам $0$1$2и так далее.

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

reversedNames = names.sorted(by: { $0 > $1 } )

Здесь $0и $1ссылаются на первый и второй Stringаргументы замыкания .

Операторские методы

На самом деле есть еще более короткий способ написания выражения замыкания выше. StringТип Swift определяет свою специфическую для строки реализацию оператора more-than ( >) как метод, который имеет два параметра типа Stringи возвращает значение типа Bool. Это точно соответствует типу метода, необходимому для sorted(by:)метода. Следовательно, вы можете просто передать оператор «больше, чем», и Swift определит, что вы хотите использовать его специфическую для строки реализацию:

reversedNames = names.sorted(by: >)

Для получения дополнительной информации о методе оператора см. Методы оператора .

Замыкающие затворы

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

func someFunctionThatTakesAClosure(closure: () -> Void) {

// function body goes here

}



// Here's how you call this function without using a trailing closure:



someFunctionThatTakesAClosure(closure: {

// closure's body goes here

})



// Here's how you call this function with a trailing closure instead:



someFunctionThatTakesAClosure() {

// trailing closure's body goes here

}

Закрытие с сортировкой строк из приведенного выше раздела Синтаксис выражения закрытия может быть записано вне sorted(by:)скобок метода как завершающее замыкание:

reversedNames = names.sorted() { $0 > $1 }

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

reversedNames = names.sorted { $0 > $1 }

Конечные замыкания наиболее полезны, когда замыкание достаточно длинное, и его невозможно записать в одной строке. Например, у Arrayтипа Swift есть map(_:)метод, который принимает выражение замыкания в качестве единственного аргумента. Замыкание вызывается один раз для каждого элемента в массиве и возвращает альтернативное сопоставленное значение (возможно, другого типа) для этого элемента. Характер сопоставления и тип возвращаемого значения оставляется на усмотрение замыкания.

После применения предоставленного замыкания к каждому элементу массива map(_:)метод возвращает новый массив, содержащий все новые сопоставленные значения, в том же порядке, что и их соответствующие значения в исходном массиве.

Вот как вы можете использовать map(_:)метод с замыкающим замыканием для преобразования массива Intзначений в массив Stringзначений. Массив используется для создания нового массива :[16, 58, 510]["OneSix", "FiveEight", "FiveOneZero"]

let digitNames = [

0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",

5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"

]

let numbers = [16, 58, 510]

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

Теперь вы можете использовать numbersмассив для создания массива Stringзначений, передав выражение закрытия в метод массива map(_:)в качестве завершающего замыкания:

let strings = numbers.map { (number) -> String in

var number = number

var output = ""

repeat {

output = digitNames[number % 10]! + output

number /= 10

} while number > 0

return output

}

// strings is inferred to be of type [String]

// its value is ["OneSix", "FiveEight", "FiveOneZero"]

map(_:)Метод вызывает выражение закрывающего один раз для каждого элемента массива. Вам не нужно указывать тип входного параметра замыкания number, потому что тип может быть выведен из значений в массиве, который нужно отобразить.

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

Выражение закрытия создает строку, вызываемую outputкаждый раз, когда она вызывается. Он вычисляет последнюю цифру number, используя оператор остатка ( ), и использует эту цифру для поиска соответствующей строки в словаре. Замыкание может использоваться для создания строкового представления любого целого числа, большего нуля.number % 10digitNames

ЗАМЕТКА

За вызовом digitNamesнижнего индекса словаря следует восклицательный знак ( !), поскольку нижние индексы словаря возвращают необязательное значение, указывающее, что поиск в словаре может завершиться неудачно, если ключ не существует. В приведенном выше примере гарантируется, что для словаря всегда будет допустимый ключ индекса , и поэтому для принудительного развертывания значения, хранящегося в необязательном возвращаемом значении индекса, используется восклицательный знак .number % 10digitNamesString

Строка извлекается из digitNamesсловаря добавляется в передней части output, эффективно строить строковую версию числа в обратном порядке . (Выражение дает значение для , для и для .)number % 106168580510

numberПеременная затем делится 10. Поскольку это целое число, оно округляется во время деления, поэтому 16становится 158становится 5и 510становится 51.

Процесс повторяется до тех пор, пока он не numberстанет равным 0; в этот момент outputстрока возвращается закрытием и добавляется в выходной массив map(_:)методом.

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

Захват ценностей

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

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

Вот пример вызываемой функции makeIncrementer, которая содержит вложенную вызываемую функцию incrementer. Вложенная incrementer()функция захватывает два значения runningTotalи amountиз окружающего контекста. После захвата этих значений incrementerвозвращается makeIncrementerкак замыкание, которое увеличивается runningTotalпри amountкаждом вызове.

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

var runningTotal = 0

func incrementer() -> Int {

runningTotal += amount

return runningTotal

}

return incrementer

}

Тип возврата makeIncrementeris . Это означает, что он возвращает функцию , а не простое значение. Функция, которую она возвращает, не имеет параметров и возвращает значение каждый раз, когда она вызывается. Чтобы узнать, как функции могут возвращать другие функции, см. Типы функций как Типы возврата .() -> IntInt

makeIncrementer(forIncrement:)Функция определяет целое переменное runningTotal, чтобы сохранить текущую текущую сумму инкрементор , которые будут возвращены. Эта переменная инициализируется значением 0.

makeIncrementer(forIncrement:)Функция имеет один Intпараметр с аргументом меткой forIncrement, и имя параметра amount. Значение аргумента, передаваемое этому параметру, указывает, сколько runningTotalнужно увеличивать при каждом вызове возвращаемой функции инкремента. makeIncrementerФункция определяет вложенную функцию с именем incrementer, которая выполняет фактическое приращение. Эта функция просто добавляет amountк runningTotal, и возвращает результат.

При рассмотрении в отдельности вложенная incrementer()функция может показаться необычной:

func incrementer() -> Int {

runningTotal += amount

return runningTotal

}

incrementer()Функция не имеет параметров, и все же это относится к runningTotalи amountвнутри его тела функции. Это делается путем захвата ссылки на runningTotalи amountиз окружающей функции и использования их в своем собственном теле функции. Захватив по ссылке гарантирует , что runningTotalи amountне исчезает , когда вызов makeIncrementerконцов, а также гарантирует , что runningTotalдоступно в следующий раз incrementerфункция вызывается.

ЗАМЕТКА

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

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

Вот пример makeIncrementerв действии:

let incrementByTen = makeIncrementer(forIncrement: 10)

В этом примере устанавливается константа, вызываемая incrementByTenдля ссылки на функцию инкремента, которая добавляет 10к своей runningTotalпеременной каждый раз, когда она вызывается. Многократный вызов функции показывает это поведение в действии:

incrementByTen()

// returns a value of 10

incrementByTen()

// returns a value of 20

incrementByTen()

// returns a value of 30

Если вы создадите второй инкремент, он будет иметь собственную сохраненную ссылку на новую отдельную runningTotalпеременную:

let incrementBySeven = makeIncrementer(forIncrement: 7)

incrementBySeven()

// returns a value of 7

Повторный вызов исходного incrementer ( incrementByTen) продолжает увеличивать собственную runningTotalпеременную и не влияет на переменную, захваченную incrementBySeven:

incrementByTen()

// returns a value of 40

ЗАМЕТКА

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

Замыкания являются ссылочными типами

В приведенном выше примере, incrementBySevenи incrementByTenпостоянные, но замыкания эти константы относятся к еще способны увеличивать те runningTotalпеременные , которые они захватили. Это потому, что функции и замыкания являются ссылочными типами .

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

Это также означает, что если вы назначаете замыкание двум различным константам или переменным, обе эти константы или переменные ссылаются на одно и то же замыкание.

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen()

// returns a value of 50



incrementByTen()

// returns a value of 60

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

Экранирование

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

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

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {

completionHandlers.append(completionHandler)

}

someFunctionWithEscapingClosure(_:)Функция принимает замыкание в качестве аргумента и добавляет его в массив , который объявлен вне функции. Если вы не пометили параметр этой функции @escaping, вы получите ошибку во время компиляции.

Маркировка закрытия @escapingозначает, что вы должны selfявно ссылаться на него. Например, в приведенном ниже коде переданное закрытие someFunctionWithEscapingClosure(_:)является экранирующим закрытием, что означает, что оно должно ссылаться selfявно. Напротив, переданное закрытие someFunctionWithNonescapingClosure(_:)является неэскапирующим закрытием, что означает, что оно может ссылаться selfнеявно.

func someFunctionWithNonescapingClosure(closure: () -> Void) {

closure()

}



class SomeClass {

var x = 10

func doSomething() {

someFunctionWithEscapingClosure { self.x = 100 }

someFunctionWithNonescapingClosure { x = 200 }

}

}



let instance = SomeClass()

instance.doSomething()

print(instance.x)

// Prints "200"



completionHandlers.first?()

print(instance.x)

// Prints "100"

Autoclosures

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

Распространено вызывать функции, которые принимают автозамены, но это не распространено для реализации такого рода функции. Например, assert(condition:message:file:line:)функция принимает autoclosure для своих conditionи messageпараметров; его conditionпараметр оценивается только в отладочных сборках, а его messageпараметр оценивается только в том случае, если он conditionесть false.

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

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

print(customersInLine.count)

// Prints "5"



let customerProvider = { customersInLine.remove(at: 0) }

print(customersInLine.count)

// Prints "5"



print("Now serving \(customerProvider())!")

// Prints "Now serving Chris!"

print(customersInLine.count)

// Prints "4"

Даже если первый элемент customersInLineмассива удаляется кодом внутри замыкания, элемент массива не удаляется до тех пор, пока замыкание не будет фактически вызвано. Если замыкание никогда не вызывается, выражение внутри замыкания никогда не вычисляется, что означает, что элемент массива никогда не удаляется. Обратите внимание, что тип customerProvider— это не Stringпросто функция без параметров, которая возвращает строку.() -> String

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

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]

func serve(customer customerProvider: () -> String) {

print("Now serving \(customerProvider())!")

}

serve(customer: { customersInLine.remove(at: 0) } )

// Prints "Now serving Alex!"

serve(customer:)Функция в листинге выше требуется явное замыкание , которое возвращает имя клиента. Версия serve(customer:)ниже выполняет ту же операцию, но вместо явного закрытия она выполняет автозаполнение, помечая тип своего параметра @autoclosureатрибутом. Теперь вы можете вызывать функцию, как если бы она взяла Stringаргумент вместо замыкания. Аргумент автоматически преобразуется в замыкание, поскольку customerProviderтип параметра помечается @autoclosureатрибутом.

// customersInLine is ["Ewa", "Barry", "Daniella"]

func serve(customer customerProvider: @autoclosure () -> String) {

print("Now serving \(customerProvider())!")

}

serve(customer: customersInLine.remove(at: 0))

// Prints "Now serving Ewa!"

ЗАМЕТКА

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

Если вы хотите autoclosure , что позволило избежать, использовать как @autoclosureи @escapingатрибуты. @escapingАтрибут описано выше в Экранирование Closures .

// customersInLine is ["Barry", "Daniella"]

var customerProviders: [() -> String] = []

func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {

customerProviders.append(customerProvider)

}

collectCustomerProviders(customersInLine.remove(at: 0))

collectCustomerProviders(customersInLine.remove(at: 0))



print("Collected \(customerProviders.count) closures.")

// Prints "Collected 2 closures."

for customerProvider in customerProviders {

print("Now serving \(customerProvider())!")

}

// Prints "Now serving Barry!"

// Prints "Now serving Daniella!"

В приведенном выше коде вместо вызова замыкания, переданного ему в качестве customerProviderаргумента, collectCustomerProviders(_:)функция добавляет замыкание к customerProvidersмассиву. Массив объявляется вне области действия функции, что означает, что замыкания в массиве могут быть выполнены после возврата из функции. В результате значение customerProviderаргумента должно быть разрешено за пределами области действия функции.

Уважаемый пользователь! Реклама помогает поддерживать и развивать наш проект, делая его простым и удобным специально для Вас. Если проект интересный и важный для Вас, то отключите на нем блокировщик рекламы. Спасибо, что читаете сайт!

9.1. Замыкания — Основы | Swift World

let strings = numbers.map{ (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings выведено в тип [String]
// его значение равно ["OneSix", "FiveEight", "FiveOneZero"]
Теперь Вы можете использовать массив numbers для создания массива типа String, передав в метод массива map(_:) волочащееся замыкание.

Метод map(_:) вызывает замыкание единожды для каждого элемента в массиве. Вам не нужно явно указывать тип входного параметра замыкания, number, так как этот тип может быть выведен из значений отображаемого массива.

В этом примере переменная number инициализируется значением параметра замыкания number, так что это значение может быть изменено внутри тела замыкания. (Параметры функций и замыканий всегда константы.) Замыкание так же специфицирует возвращаемый тип String, чтобы указать, какиой тип будет храниться в отображённом выходном массиве.

Замыкание строит строку с названием output всякий раз при своём вызове. Оно вычисляет последнюю цифру number с использованием оператора остатка от деления (number % 10) и использует эту цифру для выбора подходящей строки в словаре digitNames. Это замыкание может быть использовано для создания строки, отображающей любое целое число, большее 0.

Вызов сабскрипта словаря digitNames сопровождается восклицательным знаком (!), так как сабскрипты словарей возвращают опциональное значение для индикации того, что выбор значения из словаря может провалиться, если указанный ключ не существует. В примере Выше гарантировано, что number % 10 всегда будет корректным ключом сабскрита для словаря digitNames, так что восклицательный знак используется для принудительной распаковки значения типа String, хранимого в опциональном возвращаемом значении сабскрипта.

Строка, полученная из словаря digitNames прибавляется в переднюю часть output, эффективно создавая строковые представление числа в обратном порядке. (Выражение number % 10 даёт число 6 для 16, 8 для 58 и 0 для 510.)

Переменная number затем делится на 10. Так как это целое число, то оо будет округлено вниз при делении, так что 16 станет 1, 58 станет 5, а 510 станет 51.

Этот процесс повторяется, пока number не станет равен 0, в этот момент строка output будет возвращена замыканием и добавлена в выходной массив метода map(_:).

Использование синтаксиса trailing-замыкания в примере выше позволяет инкаспулировать функциональность замыкания сразу после функции, поддерживающей это замыкание без необходимости оборачивать всё замыкание во внешнюю круглую скобку метода map(_:).

Замыкания (closure) в Swift – Swiftme.ru

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

Задание 1

1)Напишите замыкание, которое производит вывод на консоль сообщения “Hello, World!”.
2)Вызовите данное замыкание
3)Какой тип данных у данного замыкания?

Правильный ответ

//1)
// для того, чтобы замыкание могло быть вызвано, оно должно быть проинициализировано параметру
let closureHello = { print("Hello, World!") }
//2)
closureHello()
//3)
//Тип данных замыкания: () -> ()
type(of:closureHello) // (() -> ()).Type

 

Задание 2

Из представленных ниже функциональных типов укажите те, которые указывают на то:

1) что функция/замыкание не принимает входных аргументов и ничего не возвращает
2) что функция принимает один входной аргумент
3) что функция возвращает значение

() -> (:)
(_) -> ()
(Int) -> () -> ()
() -> Void
() -> (String)
() -> ()

Правильный ответ

1)
4 и 6
2)
3, т.к. функция принимает входной аргумент типа Int, требований к возвращаемому значению в данного подзадании нет)
3)
3, т.к. функция возвращает значение типа () -> ()
5, т.к. функция возвращает значение типа String

 

Задание 3

1) Напишите замыкание, которое принимает на вход параметр типа String и выводит его значение на консоль.
2) Вызовите данное замыкание
3) Какой тип данных у данного замыкания?

Правильный ответ

//1)
let closurePrintMessage = { (message: String) in
    print(message)
}
//2)
closurePrintMessage("Этот текст будет выведен на консоль")

// вариант решения, предложенный Тимуром @magic1620
let printString: (String) -> () = { print($0) }


 

Задание 4

1) Напишите замыкание, которое принимает на вход два целочисленных параметра и возвращает их сумму.
2) Протестируйте работу данного замыкания
3) Какой тип данных будет у данного замыкания?

Правильный ответ

//1)
let closureSumOfTwoInt = { (a: Int, b: Int) in
    return a + b
}
//2)
closureSumOfTwoInt(4,7) //11
//3)
// тип данных - (Int, Int) -> Int
type(of: closureSumOfTwoInt) // ((Int, Int) -> Int).Type

 

Доступ закрыт

Захват контекста замыканиями вместо делегирования в iOS 8 Swift / Habr

При проектировании iOS приложений со многими MVC приходится решать вопросы передачи информации от одного MVC к другому как в прямом, так и в обратном направлении. Передача информации в прямом направлении при переходе от одного MVC к последующему осуществляется обычно установкой Mодели того MVC, куда мы переходим, а вот передача информации «назад» из текущего MVC в предшествующий осуществляется с помощью делегирования как в Objective-C, так и в Swift.

Кроме того, делегирование используется внутри одного MVC между View и Controller для их «слепого взаимодействия».

Дело в том, что Views — слишком обощенные (generic) стандартизованные строительные блоки, они не могут что-то знать ни о классе, ни о Controller, который их использует. Views не могут владеть своими собственными данными, данные принадлежат Controller. В действительности, данные могут находиться в Mодели, но Controller является ответственным за их предоставление. Тогда как же  View может общаться с Controller? С помощью делегирования.

Нужно выполнить 6 шагов, чтобы внедрить делегирование во взаимодействие View и Controller:

  1. Создаем протокол делегирования (определяем то, о чем View хочет, чтобы Controller позаботился)
  2. Создаем в View weak свойство delegate, типом которого будет протокол делегирования
  3. Используем в View свойство delegate, чтобы получать данные/ делать вещи, которыми View  не может владеть или управлять
  4. Controller объявляет, что он реализует протокол
  5. Controller устанавливает self (самого себя) как делегата View путем установки свойства в пункте #2, приведенном выше
  6. Реализуем протокол в Controller

Мы видим, что делегирование — не простой процесс.
Как в Swift, так и в Objective-C, процесс делегирования можно заменить использованием замыканий (блоков), принимая во внимание их способность захватывать любые переменные из окружающего контекста для внутреннего использования. Однако в Swift реализация этой идеи существенно упрощается и выглядит более лаконичной, так как  функции (замыкания) в Swift являются «гражданами первого сорта», то есть могут объявляться переменными и передаваться как параметры функций. Простота и абсолютная ясность кода в Swift позволят более широко использовать замыкания (closures), захватывающие контекст, для взаимодействия двух MVC или взаимодействия Controller и View без применения делегирования.

Я хочу показать использование захвата контекста замыканиями на двух примерах, взятых из стэнфордского курса 2015 «Developing iOS 8 Apps with Swift» (русский эквивалент находится на сайте «Разработка iOS+Swift+Objective-C приложений»).

Один пример будет касаться взаимодействия View  и Controller в пределах одного MVC, а другой — двух различных MVC. В обоих случаях  захват контекста замыканиями позволит нам заменить делегирование более простым и элегантным кодом, не требующим вспомогательных протоколов и делегатов.

В Заданиях стэнфордского курса предлагается разработать Графический калькулятор,

который на iPad выглядит состоящим из двух частей: в левой части находится RPN (обратная польская запись) калькулятор, позволяющий не только проводить вычисления, но и, используя переменную M, задавать выражение для функции, которая при нажатии кнопки «График» графически воспроизводится в правой части экрана. Эти выражения можно запоминать в списке функций нажатием кнопки «Add to Favorites» и воспроизводить весь список запомненных функций с помощью кнопки «Show Favorites«. В списке вы можете выбрать любую функцию (рисунок в заголовке), и она будет построена в графической части. Имея набор некоторых функций, вы можете производить их графическое построение, не прибегая к RPN калькулятору.
Кроме того, вы можете удалить ненужную функцию из списка, используя жест Swipe ( смахивания) справа налево.

Я не буду останавливаться на реализации RPN калькулятора, процесс построения его изложен на сайте «Разработка iOS+Swift+Objective-C приложений». Нас будет интересовать графическая часть, и в частности, как пользовательский UIView получает информацию о координате y= f(x) от своего Controller, и как стандартный Table View, появляющийся в окошке Popover, заставляет Controller другого MVC рисовать нужный график и поддерживать синхронный список функций.
Все MVC, участвующие в приложении «Графический калькулятор», представлены ниже

Мы видим, что используется Split View Controller, в котором роль Master стороны играет калькулятор, способный формировать функциональные зависимости типа y= f(x), а роль Detail играет График, представляющий зависимость y= f(x). Нас будет интересовать Detail сторона Split View Controller, а именно MVC «График», на котором мы отработаем взаимодействие View и Controller в пределах одного MVC, и MVC «Список функций», на котором мы отработаем его взаимодействие с MVC «График».

Захват контекста замыканием при взаимодействии View и Controller в одном MVC.


Посмотрим на MVC «График», которое управляется классом FavoritesGraphViewController.

При внимательном рассмотрении мы обнаружим, что класс FavoritesGraphViewController наследует от базового класса GraphViewController и содержит только то, что связано со списком функций, представленном переменной favoritePrograms, которая является массивом программ для RPN калькулятора. Вся графическая часть скрыта в базовом классе GraphViewController. С точки зрения поставленной в статье задачи, нам интересен именно базовый класс GraphViewController, а к классу FavoritesGraphViewController мы вернемся в следующем разделе. Это общий прием в iOS программировании, когда более обобщенный класс остается нетронутым, а все «частности» вносятся в его subclass. В данном разделе мы можем считать, что схема нашего пользовательского интерфейса имеет более упрощенный вид:

То есть MVC «График» управляется классом GraphViewController, в который передается программа program RPN калькулятора для построения графика ( это Mодель MVC «График»).

View этого MVC представляет собой обычный UIView, управляемый классом GraphView.

Перед нами поставлена задача создать абсолютно обобщенный класс GraphView, способный строить зависимости y = f(x). Этот класс ничего не должен знать о калькуляторе, он должен получать информацию о графике в виде общей зависимости y = f(x) и не хранить никаких данных. С другой стороны, в нашем Controller, представленным классом GraphViewController, как раз и содержится информация о графике y = f(x), но не в явном виде, а в виде программы program, которая может интерпретироваться экземпляром brain RPN калькулятора.

Имея произвольное значение x можно вычислить y c помощью калькулятора brain для установленной программы program

Как связать эти два класса — GraphView и GraphViewController, когда у одно из них есть информация, в которой нуждается другой? Традиционный и универсальный способ выполнения этого как в Objective-C, так и в Swift — это делегирование. Об этом способе для данного конкретного примера на Swift рассказано в посте «Задание 3. Решение -Обязательные задания».

Мы избрали другой путь — использование замыкания (closures), захватывающего переменные из внешнего контекста, для взаимодействия двух классов, в нашем случае GraphView и GraphViewController.

Добавляем в класс GrapherView переменную-замыкание yForX как public (not private), чтобы ее можно было устанавливать в GrapherViewController

Используя Optional переменную yForX, нарисуем график в классе GrapView:

Заметьте, что для задания цепочки Optionals в случае, когда сама функция является Optional, функцию нужно взять в круглые скобки, поставить знак ? вопроса, а затем написать ее аргументы.
В GraphViewController в Наблюдателе didSet { } Свойства GraphView! , которое является @IBOutlet, мы установим замыкание yForX так, чтобы оно захватило ссылку на экземпляр моего калькулятор self.brain, в котором уже установлена нужная программа program для построения графика. Каждый раз при обращении к yForX будет использоваться один и тот же «захваченный» калькулятор, а это то, что нам нужно.

Все. Никаких делегатов, никаких протоколов, никаких подтверждений протоколов. Единственное — добавляем в так называемый список «захвата» [unowned self ] для исключения циклических ссылок в памяти (об этом рассказывается в Лекции 9 курса «Developing iOS 8 Apps with Swift»).

Код на Github.

Захват контекста замыканием при взаимодействии двух MVC.


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

Как было указано выше, для этого нам пришлось создать subclass класса GraphViewController, который мы назвали FavoritesGraphViewController. И теперь MVC «График», управляется классом FavoritesGraphViewController.
В этом новом классе FavoritesGraphViewController для списка программ мы разместим вычисляемую переменную favoritePrograms, которая является массивом программ для RPN калькулятора и связана с постоянным хранилищем NSUserDefaults. Пополнение списка программ осуществляется с помощью кнопки «Add to Favorites«. К массиву favoritePrograms добавляется текущая программа program

Для отображения списка программ используется другой MVC — MVC «Список функций». Это обычный Table View Controller, которым управляет класс FavoriteTableViewController. «Переезд» на MVC «Список функций» осуществляется при нажатии кнопки «Show Favorites«, которая находится на MVC «График», с помощью segue типа «Present as Popover».

Моделью для класса FavoriteTableViewController является массив программ для RPN калькулятора, который нужно отобразить в таблице.

Выполняем методы Table View DataSource


И сразу же сталкиваемся с тем, что нам нужно отображать в строке таблицы не программу для RPN калькулятора, а ее описание в «цивилизованном инфиксном» виде, ведь наш MVC называется MVC «Список функций». Для этого надо запрашивать калькулятор, который находится в MVC «График».

Добавляем в класс FavoriteTableViewController переменную-замыкание descriptionProgram, тип которой — функция, имеющая на входе два параметра:

  • FavoriteTableViewController — класс, который запрашивает этот метод
  • index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание

На выходе получается Optional строка c описанием:

Это замыкание мы будем устанавливать в MVC «График» в процессе подготовки к «переезду» на MVC «Список функций» в методе prepareForSegue

Замыкание descriptionProgram захватит в MVC «График» программу калькулятора и массив программ и будет их использовать при каждом вызове.

Вернемся к нашей таблице и классу FavoriteTableViewController. Нам нужно обеспечить рисование соответствующего графика при выборе определенной функции в таблице и синхронизовать удаление строки в списке функций с массивом программ, находящемся в постоянном хранилище NSUserDefaults. Все это требует взаимодействия с MVC «График» . Поэтому добавляем в класс FavoriteTableViewController две переменные-замыкания didSelect и didDelete, тип которых — функции с одинаковой сигнатурой, имеющие на входе, как и предыдущая переменная-замыкание descriptionProgram, два параметра:

  • FavoriteTableViewController — класс, который запрашивает этот метод
  • index — индекс программы в списке программ favoritePrograms, которой нужно инфиксное описание

Эти функции ничего не возвращают, так как все действия производятся внутри замыканий:

Будем использовать методы делегата didSelectRowAtIndexPath и commitEditingStyle… и только что объявленные переменные-замыкания для выполнения поставленных задач:

Замыкания didSelect и didDelete мы будем устанавливать в MVC «График» в процессе подготовки к переезду на MVC «Список функций» в методе prepareForSegue:

Замыкание didSelect захватит в MVC «График» программу program, которая устанавливается для калькулятора извне, и переустановит ее, что заставит MVC «График» перерисовать нужный нам график. В этом же замыкании вы можете убрать Popover окно со списком функций с экрана (достаточно убрать комментарий со строки controller.dismissControlerAnimated…) или оставить его для последующего выбора пользователем.

Замыкание didDelete захватит массив программ favoritePrograms, связанный с постоянным хранилищем NSUserDefaults, и удаляет соответствующую программу.
Итак, мы рассмотрели как MVC «Список функций» взаимодействует с вызвавшим его MVC «График» в обратном направлении с помощью замыканий.

Теперь рассмотрим прямое взаимодействие. Где же устанавливается Модель programs для MVC «Список функций»? Мы будем устанавливать ее в MVC «График» в процессе подготовки к переезду на MVC «Список функций» в том же методе prepareForSegue

Итак, схема использования замыканий для обмена информацией между различными MVC очень простая.
Она состоит из 3-х шагов:

  • В MVC, требующим взаимодействия, создаете public переменную — замыкание
  • Используете ее в том же MVC
  • В другом MVC устанавливаете это замыкание либо в Наблюдателе Свойств didSet {}, либо в методе prepareForSegue, либо еще где-то так, чтобы замыкание «захватило» нужные переменные и константы

Все.
Никаких вспомогательных элементов — протоколов и делегатов.

Код на Github.

На iPhone использование Графического калькулятора еще эффективнее, так как там работает не Split View Controller, а Navigation Controller, и вы остаетесь один на один со списком функций на экране.

Заключение


Мы рассмотрели передачи информации от одного MVC к другому MVC как в прямом, так и в обратном направлении. Передача информации в прямом направлении при переходе от одного MVC к последующему, осуществляется установкой Mодели того MVC, куда мы переходим. Передачу информации «назад» из текущего MVC в предшествующий MVC очень удобно и легко осуществлять в Swift с помощью замыканий.

Этот прием можно используется также и внутри одного MVC для “слепого взаимодействия” между View и Controller. Представлен демонстрационный пример Графический Калькулятор, который показывает все эти возможности.

Обращаю ваше внимание, что условием разработки Графического калькулятора в стэнфордских курсах было создание классов, поддерживающих построения графика и вывод списка функций в табличном виде, как можно более обобщенными (generic), не знающими ничего о существовании RPN калькулятора. Поэтому все переменные — замыкания во всех представленных примерах имеют очень обобщенный (generic) вид, связанный исключительно с семантикой соответствующих классов GraphView и FavoriteTableViewController.

Ссылки


Стэнфордский курс 2015 «Developing iOS 8 Apps with Swift» 
Русский неавторизованный конспект лекций и решения Заданий находятся на сайте «Разработка iOS+Swift+Objective-C приложений»
Текст Задания 3 на английском языке доступен на iTunes в пункте “Developing iOS 8 app: Programming: Project 3″.
Текст Задания 3 на русском языке доступен на «Задание 3 iOS 8.pdf»

Решение Задания 3 «Графический калькулятор» с нуля.
Задание 3 cs193p Зима 2015 Графический Калькулятор. Решение — обязательные пункты
Задание 3 cs193p Зима 2015 Графический Калькулятор. Решение — дополнительные пункты 1, 2 и 3
Задание 3. Решение — дополнительные пункты 4, 5 и 6. Окончание.
Код на Github.
Примечание. Если будете экспериментировать с Графическим калькулятором, то помните, что это RPN калькулятор, поэтому сначала вводятся операнды, а потом операция. Чтобы получить функцию sin (1/M) нужно ввести на калькуляторе следующую последовательность символов
1 M ÷ sin кнопка «График» дает sin (1/M)
M cos M × кнопка «График» дает cos(M)*M
M1M sin + × кнопка «График» дает M * ( 1 +sin (M))

Leave a comment