Статьи

Swift From Scratch: Закрытие

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

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

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

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

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

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

Даже циклы сохранения , которые не редкость в C или Objective-C, обрабатываются Swift. Это уменьшает трудно обнаруживаемые утечки памяти или сбои, вызванные неверными указателями.

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

1
2
3
{(a: Int) -> Int in
    return a + 1
}

Первое, что вы заметите, это то, что все замыкание заключено в пару фигурных скобок. Параметры замыкания заключены в пару круглых скобок, отделенных от возвращаемого типа символом -> . Вышеупомянутое замыкание принимает один аргумент a типа Int и возвращает Int . Тело замыкания начинается после ключевого слова in .

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

1
2
3
func increment(_ a: Int) -> Int {
    return a + 1
}

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

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

1
2
3
4
5
6
7
8
var states = [«California», «New York», «Texas», «Alaska»]
 
let abbreviatedStates = states.map({ (state: String) -> String in
    let index = state.index(state.startIndex, offsetBy: 2)
    return state.substring(to: index).uppercased()
})
 
print(abbreviatedStates)

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

Ранее в этой серии мы узнали, что Swift довольно умен. Позвольте мне показать вам, насколько умный. Массив состояний — это массив строк. Поскольку мы вызываем функцию map(_:) для массива, Swift знает, что аргумент state имеет тип String . Это означает, что мы можем опустить тип, как показано в обновленном примере ниже.

1
2
3
4
let abbreviatedStates = states.map({ (state) -> String in
    let index = state.index(state.startIndex, offsetBy: 2)
    return state.substring(to: index).uppercased()
})

Есть еще несколько вещей, которые мы можем опустить в приведенном выше примере, что приводит к следующей однострочной.

1
let abbreviatedStates = states.map({ state in state.substring(to: state.index(state.startIndex, offsetBy: 2)).uppercased() })

Позвольте мне объяснить, что происходит.

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

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

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

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

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

1
let abbreviatedStates = states.map({ $0.substring(to: $0.index($0.startIndex, offsetBy: 2)).uppercased() })

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

1
let abbreviatedStates = states.map() { $0.substring(to: $0.index($0.startIndex, offsetBy: 2)).uppercased() }

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

1
let abbreviatedStates = states.map { $0.substring(to: $0.index($0.startIndex, offsetBy: 2)).uppercased() }

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

1
2
3
4
let abbreviatedStates = states.map { (state) -> String in
    let index = state.index(state.startIndex, offsetBy: 2)
    return state.substring(to: index).uppercased()
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func changeCase(uppercase: Bool, ofStrings strings: String…) -> [String] {
    var newStrings = [String]()
 
    func changeToUppercase() {
        for s in strings {
            newStrings.append(s.uppercased())
        }
    }
 
    func changeToLowerCase() {
        for s in strings {
            newStrings.append(s.lowercased())
        }
    }
 
    if uppercase {
        changeToUppercase()
    } else {
        changeToLowerCase()
    }
 
    return newStrings
}
 
let uppercasedStates = changeCase(uppercase: true, ofStrings: «California», «New York»)
let lowercasedStates = changeCase(uppercase: false, ofStrings: «California», «New York»)

Я уверен, что вы согласны с тем, что приведенный выше пример немного надуманный, но он ясно показывает, как в Swift работает захват значений. Вложенные функции, changeToUppercase() и changeToLowercase() , имеют доступ к аргументам, states внешней функции, а также к переменной newStates объявленной во внешней функции.

Позвольте мне объяснить, что происходит.

Функция changeCase(uppercase:ofStrings:) принимает логическое значение в качестве первого аргумента и переменный параметр типа String качестве второго параметра. Функция возвращает массив строк, состоящий из строк, переданных функции в качестве второго аргумента. В теле функции мы создаем изменяемый массив строк newStrings , в котором мы храним измененные строки.

Вложенные функции перебирают строки, которые передаются в changeCase(uppercase:ofStrings:) и изменяют регистр каждой строки. Как видите, они имеют прямой доступ к строкам, передаваемым в changeCase(uppercase:ofStrings:) а также к массиву newStrings , который объявлен в теле функции changeCase(uppercase:ofStrings:) .

Мы проверяем значение в uppercase , вызываем соответствующую функцию и возвращаем массив newStrings . Две строки в конце примера демонстрируют, как работает функция changeCase(uppercase:ofStrings:) .

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

В этой статье упоминалось несколько раз: функции являются замыканиями. Есть три вида закрытий:

  • глобальные функции
  • вложенные функции
  • выражения закрытия

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

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

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

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

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

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

Если вы хотите узнать, как использовать Swift 3 для кодирования расширенных функций для реальных приложений, ознакомьтесь с нашим курсом « Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления» . Следуйте указаниям Маркуса Мюльбергера, который кодирует функциональное приложение погоды для iOS с данными о погоде в реальном времени, пользовательскими компонентами пользовательского интерфейса и анимацией, чтобы оживить все.

  • стриж
    Создание приложений для iOS с Swift 3
    Маркус Мюльбергер
  • стриж
    Запрограммируйте игру с боковой прокруткой с помощью Swift 3 и SpriteKit
    Дерек Дженсен
  • IOS
    Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления
    Маркус Мюльбергер