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

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

Здесь мы попытаемся понять концепцию, определенную в приведенном выше определении, на примере:

function startAt(x)
  function incrementBy(y)
    return x + y
  return incrementBy
variable c1 = startAt(1)
variable c2 = startAt(5)
c1(2)   // return 3
c2(2)   // return 7

В этом примере incrementBy (вложенная функция), определенная внутри startAt (функция высшего порядка), является замыканием. startAt принимает один параметр x и возвращает функцию incrementBy. incrementBy также принимает один параметр y и возвращает результат выражения x + y. Хотя x не является локальным для incrementBy, но у него есть доступ x, потому что incrementBy находится в лексической области видимости x. Итак, x - это свободная переменная в incrementBy, которая связана со значением или ссылкой на x из охватывающей области, т.е. startAt. Ниже приведено гипотетическое визуальное представление, чтобы это легко понять.

Согласно документации Apple, замыкания - это автономные функциональные блоки, которые можно передавать и использовать в вашем коде.

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

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

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

1. Глобальные функции - это замыкания, которые имеют имя и не фиксируют никаких значений.

var a = 10 // global variable
// Global function with name globalClosure
func globalClosure() -> Void {   
  print("value of a \(a)")
}
func someFunctionWithGlobalClosure() -> Void {
  globalClosure()
}
a = 14
someFunctionWithClosure() // Prints: value of a 14

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

var a = 10 // global variable
// Global function with name closureToPass
func globalClosure() -> Void {   
  print("value of a \(a)")
}
function someFunctionWithGlobalClosure() { 
  var a = 5
  globalClosure()
}
a = 14
someFunctionWithClosure() // Prints: value of a 14 , not 5

Поскольку мы объявляем a как локальную переменную внутри someFunctionWithGlobalClosure, globalClosure не захватывает эту переменную a, но захватывает a из глобальной области, где globalClosure определяется не из области, в которой она вызывается, то есть внутри someFunctionWithGlobalClosure.

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

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

// This function returns a function of type () -> Void
function someFunctionWithNestedClosure() -> () -> Void { 
  var a = 5
  func nestedFunction() -> Void {   
    print("value of a \(a)")
  }
  return nestedFunction
}
var closure = someFunctionWithNestedClosure()
closure() // Prints: value of a 5

Поскольку nestedFunction определен внутри функции someFunctionWithNestedClosure, nestedFunction может захватывать любую переменную или константу, определенную в области видимости someFunctionWithNestedClosure.

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

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

General form of closure expression syntax is:
 
 { (parameters) -> return_type in
 
   statements 
 }

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

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

var a = 10
var someFunctionWithClosure(closure: () -> Void) {
  closure()
}
someFunctionWithClosure(closure: { () -> Void in
  print("Value of a \(a)")
})  // Prints: Value of a 10

Теперь мы передаем закрытие с использованием синтаксиса Closure Expression, т.е. мы определяем его во время передачи в качестве параметра для вызова функции someFunctionWithClosure(). Он захватывает значение a из глобального контекста, потому что оно определяется в глобальном контексте, когда мы передаем его как параметр.

or

var a = 10
var someFunctionWithClosure(closure: () -> Void) {
  var a = 5
  closure()
}
someFunctionWithClosure(closure: { () -> Void in
  print("Value of a \(a)")
})  // Prints: Value of a 10, not 5

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

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

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

Swift включает в себя различные оптимизации для использования Closure Expressions в своей документации:

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

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

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

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

Что такое избегание замыканий?

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

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

Как объявить закрытие избегающим закрытием?

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

Давайте разберемся на примере:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
 completionHandlers.append(completionHandler)
 somefunctionPerformingAsyncTask()
}
func somefunctionPerformingAsyncTask() {
  // do some task
 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    completionHandlers.first?()
 }  
}
someFunctionWithEscapingClosure(completionHandler: {() -> Void in 
  print("Task completed")
})
print("Task Started")
// In console you will see log in this order
Task Started
Task completed

Здесь происходит то, что мы можем сохранить переданное закрытие в качестве аргумента функции someFunctionWithEscapingClosure в массиве, когда мы отмечаем закрытие @escaping в объявлении функции. После этого выполняется вызов функции, выполняющей некоторую асинхронную задачу, как вы можете видеть в теле функции. В теле somefunctionPerformingAsyncTask, когда задача завершена, мы получаем первый элемент из массива completionHandlers и вызываем это закрытие. Таким образом, когда вы запустите этот код, вы сможете увидеть, что в журнале сначала печатается Задача запущена, а затем эта Задача завершена. Это потому, что, когда мы вызываем someFunctionWithEscapingClosure, функция немедленно вернется и выполнит инструкцию print(“Task Started”). Через некоторое время, когда задача async завершается, она вызывает сохраненное закрытие в массиве completionHandlers, который печатает Task completed.

Вы можете думать об этом как об обратном вызове в Javascript.

Почему вам следует использовать Closure?

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

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

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

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
  var runningTotal = 0
  func incrementor() -> Int {
    runningTotal += amount
    return runningTotal
  }
  return incrementor
}
let incrementByTen = makeIncrementor(forIncrement: 10)
incrementByTen()  // returns a value of 10
incrementByTen()  // returns a value of 20
let incrementByTwo = makeIncrementor(forIncrement: 2)
incrementByTwo()  // returns a value of 2
incrementByTwo()  // returns a value of 4

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

Еще одно использование экранирования закрытия - это асинхронный обратный вызов, такой как Javascript, как мы уже объясняли в разделе выше «Что такое экранирование закрытия?»

Если вам понравился пост, нажмите кнопку ❤️ и распространите слово.

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