Зачем функцию хранить в переменной
Представьте калькулятор, где кнопка «применить операцию» одна, а сама операция — сложение, вычитание или умножение — выбирается отдельно. Вместо трёх разных кнопок с зашитой логикой удобнее одна кнопка, которой просто передают нужную операцию. В программировании то же самое: часто нужно не вызвать конкретную функцию прямо сейчас, а передать её как параметр — пусть решает, когда и с какими данными её вызвать, кто-то другой.
В Swift это возможно, потому что функция — не что-то особенное, привязанное только к своему имени. Функцию можно положить в переменную, передать в другую функцию как аргумент и даже вернуть из функции как результат — точно так же, как Int или String.
Тип функции
У каждой функции есть тип, и он описывается через типы её параметров и возвращаемого значения: (Int) -> Int означает «функция, принимающая один Int и возвращающая Int». Это официальный термин — тип функции (function type). Если параметров несколько, они перечисляются через запятую: (Int, Int) -> Int — функция от двух чисел, возвращающая число. Если функция ничего не возвращает, справа пишут Void — так же, как в объявлении обычной функции.
Присваивание функции переменной
Раз у функции есть тип, её можно положить в константу или переменную этого типа — без круглых скобок после имени, иначе Swift подумает, что вы хотите её вызвать.
func square(_ n: Int) -> Int {
return n * n
}
let operation: (Int) -> Int = square
print(operation(5))
Здесь operation — не число, а сама функция square, только под другим именем. Строка operation(5) вызывает ровно тот код, что и square(5). Тип аннотации (Int) -> Int можно опустить — компилятор выведет его сам из square, но с аннотацией понятнее, что перед вами именно функция, а не число.
Функция как параметр другой функции
Самое полезное применение — передавать функцию внутрь другой функции, чтобы та решала, что делать с данными, не зная заранее, какая именно операция понадобится.
func apply(_ n: Int, _ transform: (Int) -> Int) -> Int {
return transform(n)
}
func doubleIt(_ n: Int) -> Int {
return n * 2
}
print(apply(10, doubleIt))
Функция apply ничего не знает про удвоение — она просто вызывает то, что ей передали, параметром transform. Сегодня это doubleIt, завтра можно передать любую другую функцию с тем же типом (Int) -> Int, и apply менять не придётся. Обратите внимание: в вызове apply(10, doubleIt) имя функции указано без скобок — скобки означали бы «вызови её прямо сейчас», а нужно передать саму функцию как значение.
typealias для типа функции
Тип вроде (Int, Int) -> Int приходится писать снова и снова, если операций такого вида в коде много, и это быстро становится нечитаемым. Для этого есть typealias — способ дать типу короткое собственное имя.
typealias BinaryOperation = (Int, Int) -> Int
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
let sum: BinaryOperation = add
print(sum(2, 3))
typealias не создаёт новый тип — это просто псевдоним, ярлык для уже существующего. После объявления BinaryOperation и (Int, Int) -> Int в объявлениях полностью взаимозаменяемы: компилятору всё равно, каким из двух способов вы написали тип.
Функция как возвращаемое значение
Функция может не только принимать другую функцию, но и возвращать её — это удобно, когда нужный алгоритм зависит от условия, известного заранее, а сами вычисления происходят позже.
func subtract(_ a: Int, _ b: Int) -> Int {
return a - b
}
func chooseOperation(isAddition: Bool) -> BinaryOperation {
if isAddition {
return add
} else {
return subtract
}
}
let operation = chooseOperation(isAddition: false)
print(operation(10, 4))
Тип, указанный после стрелки в заголовке chooseOperation, — это не число и не строка, а тип функции BinaryOperation. Внутри тела мы возвращаем не результат вычисления, а саму функцию — add или subtract, без вызова. Вызов происходит позже, отдельной строкой, когда операция уже выбрана.
Частые ошибки
Забыть, что имя функции без скобок — это значение, а со скобками — вызов. apply(10, doubleIt()) не скомпилируется: doubleIt() без аргумента ждёт число там, где функция уже ожидает результат вызова, а не саму функцию.
Перепутать тип функции с обычным типом в объявлении параметра. Если написать func run(_ f: Int) вместо func run(_ f: (Int) -> Int), компилятор честно скажет, что вы пытаетесь передать функцию туда, где ожидается число — это не ошибка компилятора, а несовпадение объявленного типа с тем, что вы передаёте на самом деле.
Пытаться вернуть из функции результат вызова там, где ожидается сама функция. В примере с chooseOperation строка return add(1, 2) вернула бы Int, а объявленный тип возврата — функция; компилятор такое не пропустит.
Кратко
- У функции есть тип:
(Int) -> Int,(Int, Int) -> Intи так далее — по типам параметров и результата. - Функцию можно положить в константу или переменную нужного типа функции, указав имя без круглых скобок.
- Функцию можно передать в другую функцию параметром — так функция-получатель работает с любой операцией того же типа, не зная заранее, какой именно.
typealiasдаёт типу функции короткое имя и не создаёт новый тип, только псевдоним.- Функцию можно вернуть из функции — тип возврата в этом случае сам является типом функции.
Проверьте себя
3 вопроса
Дважды применить функцию
Реализуйте функцию applyTwice(_:_:), которая принимает число n и функцию operation типа (Int) -> Int, а возвращает результат применения этой функции к n ДВАЖДЫ подряд: сначала к самому n, а затем ещё раз — уже к полученному результату.
Например, если operation возводит число в квадрат, а n равно 3, результат должен быть 81: сначала 3 в квадрате даёт 9, а затем 9 в квадрате даёт 81.
Выбор операции по символу
Реализуйте функцию chooseOperation(_:), которая принимает строку-символ операции и возвращает функцию типа (Int, Int) -> Int, выполняющую эту операцию:
"+"— сложение;"-"— вычитание (первое число минус второе);"*"— умножение;- 0, независимо от аргументов.
Саму операцию вызывать внутри chooseOperation не нужно — функция только выбирает и возвращает нужную функцию, а вызывают её уже после.