Функции в Swift: полное руководство по использованию

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

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

Параметры и возвращемое значение

Функция работает следующим образом: вы вводите некоторые данные, они обрабатываются в соответствии с тем, что делает функция, и затем выводится какой-то результат.

Вот пример функции, которая принимает два параметра типа Int, складывает их вместе и возвращает их сумму:

func sum (_ x: Int, _ y: Int) -> Int { 
  let result = x + y
  return result
}

Давайте разберем синтаксис функций во всех деталях.

функции в Swift

  1. Объявление функции начинается с ключевого слова func, за которым следует имя этой функции. Это имя должно использоваться для вызова функции, то есть для запуска кода, который содержит функция.
  2. За именем функции следует список ее параметров. Он состоит, как минимум, из пустых скобок, если параметры отсутствуют. Если функция принимает параметры, они перечисляются в скобках и разделяются запятой. Каждый параметр имеет строгий формат: имя параметра, двоеточие и тип параметра.
  3. Объявление функции также имеет подчеркивание (_) и пробел перед каждым именем параметра. Этим мы указываем, что при вызове функции не обязательно указывать имена ее параметров.
  4. Если функция должна возвращать значение, то после скобок указывается оператор стрелки (->), за которым следует тип значения, которое будет возвращать функция.
  5. В фигурных скобках заключается код функции.
  6. Внутри фигурных скобок в теле функции мы можем использовать константы, определенные как имена параметров.
  7. Если функция возвращает какое-либо значение, она делает это с помощью ключевого словом return, за которым следует возвращемое значение. Тип этого значения должен соответствовать типу, объявленному ранее для возвращаемого значения (после оператора стрелки).

Работа с функциями в Swift состоит из двух частей:

  1. Определение функции.
  2. Вызов функции.

Параметры

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

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

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

Возвращаемое значение

Функция sum возвращает значение константы с именем result. Эта константа была создана путем сложения двух значений типа Int. Если попытаться вернуть значение типа String (к примеру, return «Привет»), компилятор выдаст ошибку.

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

Начиная с Swift 5.1 ключевое слово return также может быть опущено.

Вот код, который вызывает функцию sum:

let z = sum(4, 5)

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

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

    let x = 4
    let y = 5
    let z = sum(x, y)

При этом не важно, что имена аргументов совпадают с именами параметров. Их значения — это все, что нам нужно.

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

sum(4, 5)

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

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

_ = sum(4, 5)

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

Также мы можем вызывать функцию sum внутри функции sum:

let z = sum(4, sum(5, 6))

Функция без параметров и без возвращаемго значения

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

Функция без возвращаемого значения

Функция не обязательно должна возвращать какое-либо значение. В данном случае существует три способа объявления данной функции:

  1. Вы можете определить тип возвращаемого значения как Void.
  2. Вы можете написать его как пустую парку скобок — ().
  3. Вы можете полностью опустить оператор стрелки и тип возвращаемого значения.
func say1(_ s: String) -> Void { 
print(s) 
}
 
func say2(_ s: String) -> () { 
print(s) 
}
 
func say3(_ s: String) { 
print(s) 
}

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

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

say3("Привет")

Функция без параметров

Функция не обязательно должна принимать какие-либо параметры. Список параметров в объявлении функции может быть пустым. Однако вы не можете опустить скобки вызова параметров. Они всегда должны присутствовать в объявлении функции после ее имени:

func greet() -> String {
return "Как дела?"
}

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

let greeting = greet()

У функции может отсутствовать как возвращаемое значение, так и параметры:

func greet1() -> Void { 
print("Как дела?") 
}
 
func greet2() -> () { 
print("Как дела?") 
}
 
func greet3() { 
print("Как дела?") 
}

Типы функций

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

(Int, Int) -> Int

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

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

Тип функции, которая не принимает параметров и не возвращает никакого значения, может быть записан как:

() -> Void
() -> ()

Внешние имена параметров

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

  • Это разъясняет цель каждого аргумента. Метка аргумента может дать подсказку о том, как этот аргумент влияет на поведение функции.
  • Это отличает одну функцию от другой. Две функции с одинаковым именем и одинаковым типом, но с разными именами внешних параметров, являются двумя разными функциями.
  • Это помогает Swift взаимодействовать с Objective-C и Cocoa, где параметры метода почти всегда имеют внешние имена.
func echo(string s: String, times n: Int) -> String { 
   var result = ""
   for _ in 1...n { 
       result += s 
      }
        return result
    }

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

let s = echo(string: "Привет", times: 3)

Перезагрузка функций

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

func say (_ what: String) { 
}
 
func say (_ what: Int) {
}

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

Swift однозначно знает, какую функцию следует вызывать в данный момент:

say("Привет")
say(1)

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

func say() -> String { 
return "Привет"
}
 
func say() -> Int {
return 1 
}

Однако вы не можете просто вызвать функцию:

let result = say() // ошибка компиляции

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

let result1: String = say()
let result2 = say() + "two"

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

Значения параметров по умолчанию

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

class Dog {
func say(_ s: String, times: Int = 1) {
for _ in 1...times { 
  print(s)
  } 
 }
}

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

let d = Dog()
d.say("Гав!")
 
let d = Dog()
d.say("Гав!", times: 3)

Произвольные параметры

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

Чтобы указать в объявлении функции, что параметр является произвольным, добавьте к нему три точки:

func sayStrings(_ arrayOfStrings: String ...) { 
    for s in arrayOfStrings { 
       print(s) 
     }
}
sayStrings ("Эй", "Привет", "Спасибо")

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

print("Manny", 3, true) // Manny 3 true

Функция может объявлять максимум один произвольный параметр (потому что в противном случае было бы невозможно определить, где заканчивается список значений).

Изменяемые параметры

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

func say(_ s: String, times: Int, loudly: Bool) { 
loudly = true // ошибка
}

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

func say(_ s: String, times: Int, loudly: Bool) { 
var loudly = loudly
loudly = true
}

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

  • Тип параметра, который мы собираемся изменить, должен быть объявлен через inout.
  • Когда мы вызываем функцию, переменная, содержащая значение, которое нужно изменить, должна быть объявлено с помощью var, а не let.
  • Вместо передачи переменной в качестве аргумента, мы должны передать на нее ссылку. Это может быть сделано с помощью знака амперсанда — &.
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
 
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt равен \(someInt), anotherInt равен \(anotherInt)")
// Выводит "someInt равен 107, anotherInt равен 3"

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

Для примера объявим класс Dog со свойством name:

class Dog {
 var name = ""
}

Вот функция, которая принимает параметр экземпляра Dog а также параметр типа String и устанавливает имя экземпляра собаки в String. Обратите внимание, что никакой inout здесь не используется:

func changeName(of d: Dog, to newName: String) { 
 d.name = newName
}

Далее мы создаем экземпляр класса Dog:

let d = Dog()
d.name = "Фидо"
print(d.name) // "Фидо"
changeName(of: d, to: "Ровер")
print(d.name) // "Ровер"

Мы смогли изменить свойство экземпляра Dog, хотя оно не было передано в качестве параметра inout и даже было объявлено с помощью let, а не var. Похоже, что это исключение из правил об изменении параметров, но это не так. Это особенность экземпляров классов, а именно то, что они сами по себе являются ссылочными типами.

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

Вложенные функции

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

Рекурсия

Функция может вызывать сама себя. Это называется рекурсией.

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

func countDownFrom(_ x: Int) { 
print(x)
if x > 0 { // остановка
   countDownFrom(x - 1) // рекурсия
   } 
}
    countDownFrom(5) // 5, 4, 3, 2, 1, 0

Функция как значение

В Swift функция может использоваться везде, где может использоваться значение:

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

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

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

func doThis (_ f: () -> ()) {
f()
}

Функция принимает один параметр и не возвращает значения. В данном случае f сам по себе является функцией.

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

func doThis(_ f: () -> ()) { 
 f()
 }
 
func whatToDo() {
  print("I did it")
 }
 
doThis(whatToDo)

Сначала мы объявляем функцию whatToDo нужного типа (функцию, которая не принимает параметров и не возвращает значения). Затем мы вызываем функцию doThis, передавая whatToDo в качестве аргумента.

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

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

let size = CGSize(width: 45, height: 20)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
let p = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 45, height: 20), cornerRadius: 8)
p.stroke()
let result = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()

Однако этот код можно упростить, если создать служебную функцию:

func imageOfSize(_ size: CGSize, _ whatToDraw: () -> ()) -> UIImage { 
UIGraphicsBeginImageContextWithOptions(size, false, 0)
whatToDraw()
let result = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return result
}

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

func drawing() {
let p = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 45, height: 20, cornerRadius: 8)
p.stroke()
}
 
let image = imageOfSize(CGSize(width:45, height:20), drawing)

Анонимные функции

Рассмотрим следующий пример:

func whatToAnimate() {
self.myButton.frame.origin.y += 20
}
 
func whatToDoLater(finished: Bool) {
print("finished: \(finished)")
}
 
UIView.animate(withDuration:0.4, animations: whatToAnimate, completion: whatToDoLater)

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

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

Тело безымянной функции называется анонимной функцией. Чтобы создать анонимную функцию, вы делаете две вещи:

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

Мы можем переписать функцию whatToAnimate следующим образом:

func whatToAnimate() {
self.myButton.frame.origin.y += 20
}
 
{ () -> () in self.myButton.frame.origin.y + = 20 }

Еще один пример:

func whatToDoLater(finished: Bool) {
        print("finished: \(finished)")
}
 
{ (finished: Bool) -> () in print("finished: \(finished)") }

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

UIView.animate(
withDuration:0.4,
animations: { () -> () in self.myButton.frame.origin.y += 20 },
completion: { (finished: Bool) -> () in print("finished: \(finished)") }
)

Синтаксис анонимных функций

Анонимные функции очень распространены в Swift, поэтому уместно будет подробно разобрать их синтаксис.

Отсутствие возвращаемого значения

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

UIView.animate(
withDuration: 0.4,
animations: { () in self.myButton.frame.origin.y += 20 },
completion: { (finished: Bool) in print("finished: \(finished)")
})

Отсутствие ключевого слова in, когда нет передаваемых параметров

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

UIView.animate(
withDuration: 0.4, 
animations: { self.myButton.frame.origin.y += 20 },
completion: { (finished: Bool) in print("finished: \(finished)") }
)

Отсутствие типов параметров

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

UIView.animate(
withDuration: 0.4, 
animations: { self.myButton.frame.origin.y += 20 }, 
completion: { (finished) in print("finished: \(finished)") }
)

При этом круглые скобки вокруг списка параметров также могут быть опущены:

UIView.animate(
withDuration: 0.4, 
animations: { self.myButton.frame.origin.y += 20 }, 
completion: { finished in print("finished: \(finished)") }
)

Отсутствие in в выражении, в котором присутствуют параметры

Если возвращаемый тип может быть опущен, и если типы параметров уже известны компилятору, вы можете опустить выражение in и обратиться к параметрам непосредственно в теле анонимной функции, используя имена $0, $1 и т. д.:

UIView.animate(
withDuration: 0.4
animations: { self.myButton.frame.origin.y += 20 }, 
completion: { print("finished: \($0)") }
)

Отсутствие имен параметров

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

UIView.animate(
withDuration: 0.4,
animations: {self.myButton.frame.origin.y += 20 }, 
completion: {_ in print("finished!") }
)

Отсутствие метки аргументов функции

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

UIView.animate(
withDuration: 0.4, 
animations: { self.myButton.frame.origin.y += 20 }
) 
{ _ in print("finished!") }

Пропуск скобок вызывающей функции

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

func doThis (_ f : () -> ()) { 
f()
}
 
doThis { print ("Привет!") }

Пропуск ключевого слова return

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

func greeting() -> String { 
return "Как дела?"
}
 
func performAndPrint(_ f: () -> String) {
let s = f()
print(s) 
}
 
performAndPrint {
greeting()
}

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

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

Данный код можно оформить следующим образом:

let arr = [2, 4, 6, 8]
func doubleMe(i: Int) -> Int {
return i * 2 
}
 
let arr2 = arr.map(doubleMe) // [4, 8, 12, 16]

Однако нам не нужно имя doubleMe, так что это может быть анонимная функция:

let arr = [2, 4, 6, 8]
let arr2 = arr.map({ (i: Int) -> Int in return i*2 })

Сократим нашу анонимную функцию. Его тип параметра известен заранее, поэтому нам не нужно указывать его. У нас есть только один параметр, поэтому нам не нужно выражение in, если мы будем называть этот параметр через $0. Тело нашей функции состоит только из одного оператора, и это оператор return, поэтому мы можем опустить и его. И map (_ :) не принимает никаких других параметров, поэтому мы также можем опустить круглые скобки:

let arr = [2, 4, 6, 8]
let arr2 = arr.map{$0 * 2}

Паттерн опредения и вызова

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

{ }()

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

Используя данный паттерн нужное действие в коде можно выполнить в том месте, где оно необходимо. Вот типичная ситуация, когда мы создаем и настраиваем NSMutableParagraphStyle, а затем используем его в качестве аргумента при вызове метода NSMutableAttributedString addAttribute (_: value: range :):

let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
content.addAttribute(.paragraphStyle, value:para, range: NSRange(location:0, length:1))

Мы можем переписать этот код следующим образом:

content.addAttribute(.paragraphStyle, value: {
let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
} (), 
range: NSRange(location: 0, length: 1))

Замыкания

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

class Dog {
   var whatThisDogSays = "Гав!"
 
   func bark() {
      print(self.whatThisDogSays) }
}

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

func doThis(_ f: () -> ()) {
f()
}
 
let d = Dog()
d.whatThisDogSays = "Аф!"
let barkFunction = d.bark
doThis(barkFunction) // Аф!

В данном случае мы не вызываем напрямую d.bark. Мы создаем экземпляр Dog и передаем функцию bark как значение в функцию doThis. Внутри функции doThis нет экземпляра Dog. Тем не менее, функция d.bark, когда она передается, содержит в себе переменную whatThisDogSays.

Давайте изменим пример, переместив строку d.whatThisDogSays после назначения d.bark в переменную barkFunction:

class Dog {
   var whatThisDogSays = "Гав!"
 
   func bark() {
      print(self.whatThisDogSays) }
}
 
func doThis(_ f: () -> ()) { 
f()
}
 
let d = Dog()
let barkFunction = d.bark
doThis(barkFunction) // Гав!
d.whatThisDogSays = "Аф!"
doThis(barkFunction) // Аф!

Что мы сделали?

  1. Мы присвоили d.bark константе barkFunction.
  2. Результат d.whatThisDogSays был «Гав!».
  3. Мы изменили d.whatThisDogSays на «Аф!».
  4. Мы снова передали функцию barkFunction в doThis, и на этот раз мы получили «Аф!».

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

Как замыкания улучшают код?

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

func imageOfSize(_ size: CGSize, _ whatToDraw: () -> ()) -> UIImage { 
UIGraphicsBeginImageContextWithOptions(size, false, 0) 
whatToDraw()
let result = UIGraphicsGetImageFromCurrentImageContext()! 
UIGraphicsEndImageContext()
return result
}

Как вы знаете, мы можем вызвать imageOfSize с завершающим замыканием:

let image = imageOfSize(CGSize(width: 45, height: 20)) {
let p = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 45, height: 20), cornerRadius: 8)
p.stroke()
}

Этот код, однако, содержит повторение. Мы повторяем размер: пара чисел 45, 20 появляется дважды. Давайте предотвратим это повторение:

let sz = CGSize(width: 45, height: 20)
let image = imageOfSize(sz) {
let p = UIBezierPath(roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: 8)
p.stroke()
}

Переменная sz, объявленная вне нашей анонимной функции на более высоком уровне видна внутри нее. Таким образом, мы можем ссылаться на нее внутри анонимной функции. Анонимная функция — это просто тело функции, которое не будет выполнено, пока imageOfSize не вызовет его.

Когда мы ссылаемся на sz внутри тела функции в выражении CGRect (origin: CGPoint.zero, size: sz), мы фиксируем его значение, потому что тело функции является замыканием. Когда imageOfSize вызывает whatToDraw, и whatToDraw оказывается функцией, тело которой ссылается на переменную sz, проблем не возникает, даже если в области видимости imageOfSize нет sz.

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

func makeRoundedRectangle(_ sz: CGSize) -> UIImage {
let image = imageOfSize(sz) {
   let p = UIBezierPath(
   roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: 8)
   p.stroke() 
  }
return image
}

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

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

self.iv.image = makeRoundedRectangle(CGSize(width: 45, height: 20))

Функция как возвращаемое значение

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

func makeRoundedRectangleMaker(_ sz: CGSize) -> () -> UIImage { 
  func f () -> UIImage {
  let im = imageOfSize(sz) {
  let p = UIBezierPath(roundedRect: CGRect(origin: CGPoint.zero, size: sz), cornerRadius: 8)
  p.stroke()
  }
 return im 
 }
return f 
}

Чем на самом деле является тип функции makeRoundedRectangleMaker? Это (CGSize) -> () -> UIImage. Это выражение имеет две стрелки. Все, что находится после каждого оператора стрелки является типом возвращаемого значения. Так что makeRoundedRectangleMaker — это функция, которая принимает параметр CGSize и возвращает () -> UIImage. А что такое () -> UIImage? Это функция, которая не принимает параметров и возвращает UIImage. Таким образом, makeRoundedRectangleMaker — это функция, которая принимает параметр CGSize и возвращает функцию, которая сама по себе возвращает UIImage.

Теперь мы находимся в теле функции makeRoundedRectangleMaker, и наш первый шаг — объявить локальную функцию именно того типа, который мы намереваемся вернуть, а именно, функцию, которая не принимает параметров и возвращает UIImage. Назовем эту функцию f. Работа этой функции довольно проста: она вызывает imageOfSize, передавая ей анонимную функцию, которая рисует изображение прямоугольника со скругленными углами (im), а затем возвращает полученное изображение.

let maker = makeRoundedRectangleMaker(CGSize(width: 45, height: 20))

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

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

func makeRoundedRectangleMaker(_ sz: CGSize) -> () -> UIImage { 
   func f () -> UIImage {
        return imageOfSize(sz) {
        let p = UIBezierPath(roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: 8) 
        p.stroke()
       } 
   }
return f 
}

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

func makeRoundedRectangleMaker(_ sz: CGSize) -> () -> UIImage { 
return {
        return imageOfSize(sz) {
        let p = UIBezierPath(roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: 8)
        p.stroke()
     } 
  }
}

Замыкания и захват значений

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

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

func pass100 (_ f: (Int) -> ()) {
f (100)
}

Теперь внимательно посмотрите на следующий код и попытайтесь подумать, что произойдет, если мы запустим его:

var x = 0
print (x)
 
func setX (newX: Int) {
x = newX
 }
 
pass100 (Setx)
print (x)

Первый вызов print (x), очевидно, даст нам 0. Второй вызов print(x) выведет 100! Функция pass100 изменила значение переменной x. Это происходит потому, что функция setX, которую я передал pass100, содержит ссылку на x. При этом она захватывает его и устанавливает новое значение.

Сохранение замыканиями захваченных значений

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

func countAdder(_ f: @escaping () -> ()) -> () -> () { 
        var ct = 0
        return {
            ct = ct + 1
            print("count - это \(ct)")
f() }
}

Функция countAdder принимает функцию в качестве параметра и возвращает функцию. Функция, которую она возвращает, вызывает функцию, которую она принимает, с небольшим добавлением: она увеличивает переменную и возвращает результат.

func greet () {
        print("Как дела?")
    }
    let countedGreet = countAdder(greet)
    countedGreet()
    countedGreet()
    countedGreet()

Мы создали функцию, которая выводит на консоль «Как дела», и передает ее countAdder. С другой стороны countAdder выходит новая функция, которую мы назвали counttedGreet. Затем мы вызываем counttedGreet три раза. Вот что появляется в консоли:

    count - это 1
    Как дела?
    count - это 2
    Как дела?
    count - это 3

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

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

Следующая функция получает функцию и вызывает ее напрямую:

func funcCaller(f: () -> ()) { 
f()
}

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

func funcMaker() -> () -> () { 
return { 
 print("hello world") 
 }
}

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

func funcPasser(f: () -> ()) -> () -> () { // ошибка компиляции
return f
}

Решение состоит в том, чтобы пометить тип входящего параметра f как @escaping:

func funcPasser(f: @escaping () -> ()) -> () -> () { 
return f
}

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

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

Вернемся к функции makeRoundedRectangleMaker:

func makeRoundedRectangleMaker(_ sz: CGSize) -> () -> UIImage { 
return {
 imageOfSize(sz) {
    let p = UIBezierPath(roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: 8)
    p.stroke()
  } 
 }
}

Есть что-то, что мне не нравится в этом методе: размер прямоугольника с закругленными углами, который он создает, является параметром (sz), но cornerRadius прямоугольника с закругленными углами жестко задан как 8. Я хотел бы иметь возможность передать значение радиуса угла как часть вызова. Я могу придумать два способа сделать это. Один из них — предоставить makeRoundedRectangleMaker другой параметр:

func makeRoundedRectangleMaker(_ sz: CGSize, _ r: CGFloat) -> () -> UIImage { 
return {
       imageOfSize(sz) {
     let p = UIBezierPath(roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: r)
     p.stroke()
   } 
 }
}

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

let maker = makeRoundedRectangleMaker(CGSize(width: 45, height: 20), 8)

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

func makeRoundedRectangleMaker(_ sz: CGSize) -> (CGFloat) -> UIImage { 
return { 
 r in imageOfSize(sz) {
    let p = UIBezierPath(roundedRect: CGRect(origin:CGPoint.zero, size:sz), cornerRadius: r)
    p.stroke() 
  }
 } 
}

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

let maker = makeRoundedRectangleMaker(CGSize(width: 45, height: 20))
self.iv.image = maker(8)

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

self.iv.image = makeRoundedRectangleMaker(CGSize(width: 45, height: 20))(8)

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

Ссылки на функции и селекторы

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

func whatToAnimate() { 
   self.myButton.frame.origin.y += 20
    }
    func whatToDoLater(finished: Bool) {
        print("finished: \(finished)")
    }
UIView.animate(withDuration:0.4, animations: whatToAnimate, completion: whatToDoLater)

Имя типа whatToAnimate или whatToDoLater является ссылкой на функцию. Использование имени в качестве ссылки на функцию допустимо, если оно однозначно: в данном конкретном контексте есть только одна функция с именем whatToDoLater, и я использую ее имя в качестве аргумента при вызове функции, где заранее известен тип параметра (а именно (Bool) -> ()).

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

class Dog {
 
   func bark() {
      print("woof")
       }
 
   func bark(_ loudly: Bool) { 
   if loudly {
    print("WOOF")
           } else {
      self.bark() }
        }
 
        func test() {
            let barkFunction = bark // ошибка компиляции
 }
}

Этот код не будет компилироваться, потому что в данном контексте имя bark неоднозначно: к какому методу она относится? Чтобы решить эту проблему, Swift предоставляет нотацию, позволяющую более точно ссылаться на функцию. Эта запись имеет две части:

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

Тип функции может быть добавлена к ее имени с ключевым словом as.

func say(_ s: String, times: Int) {

Этот метод имеет полное имя say (_: times :), но на него можно ссылаться с использованием имени
и (String, Int) -> ().

Мы также можем использовать полное имя функции вместе ее параметрами:

  class Dog {
        func bark() {
}
func bark(_ loudly: Bool) {
        }
 
        func test() {
let barkFunction = bark(_:) }
}

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

class Dog {
        func bark() {
     }
       func bark(_ loudly: Bool) {
        }
 
func test() {
      let barkFunction = bark as () -> ()
  }
}

Очевидно, что явный тип функциии необходим и при перегрузке функции:

class Dog {
        func bark() {
}
 
func bark(_ loudly: Bool) { }
 
func bark(_ times:Int) { }
 
func test() {
let barkFunction = bark(_:)
 } // ошибка компиляции
}

Поэтому мы можем использовать:

let barkFunction = bark as (Int) -> ()

Ссылки на функции и область видимости

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

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

class Dog {
 
func bark() {
 }
 
func bark(_ loudly: Bool) { 
 }
 
func test() {
   let f = {
   return self.bark(_:)
 }
} 
 
}

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

 class Cat {
        func purr() {
 } 
}
 class Dog {
let cat = Cat()
func test() {
let purrFunction = cat.purr
 } 
}

Другая возможность — использовать тип с точечной нотацией:

class Cat {
  func purr() {
  } 
}
 
 class Dog {
   func bark() {
    }
 
func test() {
let barkFunction = Dog.bark
let purrFunction = Cat.purr 
 }
}

Селекторы

В Objective-C селектор является своего рода ссылкой на метод. В программировании на iOS вам, возможно, придется вызывать метод Cocoa, в котором потребуется, чтобы селектор был одним из параметров. Swift предоставляет способ сформировать селектор с помощью синтаксиса #selector.

class ViewController: UIViewController {
 
@IBOutlet var button: UIButton!
 
func viewDidLoad() {
 super.viewDidLoad()
 self.button.addTarget(
 self, action: #selector(buttonPressed), for: .touchUpInside)
}
 
@objc func buttonPressed(_ sender: Any) {
 }
}

Когда вы используете эту запись, происходят две вещи:

Компилятор проверяет ссылку на функцию. Если ваша ссылка на функцию недействительна, ваш код даже не скомпилируется. Компилятор также проверяет, доступна ли функция Objective-C. Нет смысла формировать селектор для метода, который Objective-C не может видеть, так как ваше приложение зависнет. Чтобы обеспечить видимость Objective-C, метод должен быть помечен атрибутом @objc.

Компилятор формирует селектор Objective-C для вас. Если ваш код компилируется, селектор, который будет передан в этот параметр, гарантированно будет правильным. Вы можете сформировать селектор неправильно, но тогда компилятор выдаст ошибку.

Читайте также:
Добавить комментарий

Ваш адрес email не будет опубликован.