1. Передаём функции в свои функции
Когда вы только начинали программировать, функция казалась чем-то вроде кнопки: нажали — она сработала. Но в Swift (как и во многих современных языках) функцию можно воспринимать ещё и как обычное значение, почти как число или строку. Это резко повышает выразительность кода: вы начинаете передавать «правила» туда, где раньше приходилось копировать однотипные циклы и if.
Представьте, что у нас есть список книг, и мы то ищем по слову, то показываем только короткие названия, то печатаем «красиво», то «как есть». Если каждый раз писать новый цикл, код разрастается, как снеговик в январе. Если же мы передаём функцию «как обрабатывать элемент» — мы пишем одну универсальную заготовку и подставляем разные правила.
До этого момента вы в основном передавали замыкания в готовые методы стандартной библиотеки: map, filter, compactMap, reduce. Сейчас сделаем шаг взросления: напишем свою функцию, которая принимает замыкание и вызывает его. Только посложнее. Это момент, когда начинаешь чувствовать, что стандартная библиотека — не магия, а набор очень умных, но вполне повторяемых идей.
Тип функции в Swift: (A) -> B
Если коротко, тип функции в Swift выглядит так: «принимает A, возвращает B», и записывается как (A) -> B. Это не комментарий и не подсказка для человека — это реальный тип, который компилятор проверяет так же строго, как Int или String.
Ниже — маленькая табличка, чтобы мозг быстрее привык, что «функция» может лежать в переменной и передаваться как аргумент:
| Что это | Пример значения | Тип |
|---|---|---|
| «Берём число, возвращаем число» | |
|
| «Берём строку, возвращаем Bool» | |
|
| «Берём два числа, возвращаем число» | |
|
Обратите внимание: оператор + тоже можно воспринимать как значение нужного типа, если контекст ожидает функцию «два Int → Int». Именно поэтому работает запись reduce(0, +) (вы это уже видели). А чуть позже мы сделаем похожий трюк в своих функциях.
Базовый шаблон: параметр-функция и её вызов
Самый базовый шаблон выглядит так: у функции есть параметр‑функция, и внутри мы её вызываем как обычно — через круглые скобки. Вот мини‑пример «применить правило к значению»:
import Foundation
func apply(_ x: Int, using transform: (Int) -> Int) -> Int {
transform(x)
}
let doubled = apply(10, using: { $0 * 2 })
print(doubled) // 20
Здесь transform — это параметр типа (Int) -> Int. Он ничем не хуже Int-параметра, просто «значение» другое: не число, а правило.
Теперь сделаем пример ближе к реальности: «посчитать количество элементов, которые подходят условию». Мы сознательно пишем это вручную, хотя у массива есть filter/count, потому что сейчас мы учимся механике передачи правила.
import Foundation
func countBooks(_ books: [String], where predicate: (String) -> Bool) -> Int {
var total = 0
for title in books {
if predicate(title) { total += 1 }
}
return total
}
Здесь главное — увидеть, что predicate(title) выглядит абсолютно нормально: predicate — это «переменная, в которой лежит функция», и мы её вызываем.
Передать vs вызвать: частая путаница
В этом месте почти все новички делают одну и ту же ошибку: путают передачу функции и вызов функции. Визуально разница небольшая — наличие () — но по смыслу это «передал правило» против «выполнил прямо сейчас». Это как принести рецепт на кухню или сразу приготовить борщ (а потом удивляться, почему кухня занята).
Посмотрим на контраст. Допустим, есть функция:
import Foundation
func isShortTitle(_ s: String) -> Bool {
s.count <= 10
}
Тогда передать её как значение — это написать без скобок:
import Foundation
let books = ["Swift", "The Swift Programming Language", "Dune"]
let shortCount = countBooks(books, where: isShortTitle)
print(shortCount) // 1
А вот так писать нельзя (или будет не то, что вы ожидаете), потому что isShortTitle() — это вызов, и ему нужен аргумент:
import Foundation
countBooks(books, where: isShortTitle()) // ❌ так нельзя: вы пытаетесь вызвать функцию
В голове полезно держать простую формулу: имя функции без () — это значение, имя функции с () — это действие.
3. Улучшаем читаемость кода
Когда в коде появляются функции, принимающие функции, типы начинают выглядеть «страшнее»: (String) -> Bool, (String) -> String, (Int, Int) -> Int и так далее. Это нормально, но иногда хочется сделать код дружелюбнее для человека.
typealias для типов замыканий
Для этого мы используем typealias — просто «псевдоним типа».
Важно: это не новая сущность, не класс и не структура. Это буквально «второе имя» для типа, чтобы сигнатуры читались легче.
import Foundation
typealias BookPredicate = (String) -> Bool
typealias BookFormatter = (String) -> String
func printBooks(_ books: [String], format: BookFormatter) {
for title in books {
print(format(title))
}
}
Теперь сигнатуры начинают читаться почти как русский текст: «печатаем книги, форматируя их правилом BookFormatter».
Когда лучше замыкание, а когда именованная функция
На этом этапе легко впасть в крайность: либо писать всё замыканиями (и получить «лес фигурных скобок»), либо наоборот — плодить именованные функции по поводу и без. Нормальный стиль где-то посередине: маленькое одноразовое правило удобно держать рядом с местом использования, а правило со смыслом лучше назвать.
Например, форматирование вывода часто одноразовое:
import Foundation
printBooks(library) { title in
"\"\(title)\" (\(title.count) chars)"
}
А вот предикаты «книга короткая», «книга содержит слово», «книга начинается с буквы» — обычно хочется вынести и назвать, потому что это уже бизнес‑логика мини‑приложения, а не деталь печати.
import Foundation
func startsWithLetter(_ letter: Character) -> (String) -> Bool {
let l = String(letter).lowercased()
return { normalize($0).hasPrefix(l) }
}
Читаемость — главный критерий. Если вы через неделю открываете файл и понимаете код без расшифровки «что имел в виду автор» — значит, вы всё сделали правильно.
4. Мини-каталог книг: фильтрация и форматирование
Чтобы примеры не были «в вакууме», продолжим развивать наше учебное консольное приложение. Пока оно простое: есть список названий книг (строки), а мы умеем их печатать и фильтровать. Позже курс будет усложнять проект, но сейчас наша цель — научиться передавать правила как функции.
Начнём с данных и пары утилит:
import Foundation
let library = [
"Swift",
"Clean Code",
"The Pragmatic Programmer",
"Dune",
"1984"
]
func normalize(_ s: String) -> String {
s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
normalize — обычная функция. Но сейчас она для нас ещё и «значение», которое можно передать в map, или в наш собственный помощник.
Теперь сделаем две универсальные функции: одна фильтрует массив книг по правилу, другая печатает книги с форматированием. Это две самые частые операции в консольных программах: отобрать данные и красиво их показать.
Сначала — фильтрация:
import Foundation
typealias BookPredicate = (String) -> Bool
func filterBooks(_ books: [String], where predicate: BookPredicate) -> [String] {
var result: [String] = []
for title in books {
if predicate(title) { result.append(title) }
}
return result
}
А теперь — печать с форматированием:
import Foundation
typealias BookFormatter = (String) -> String
func printBooks(_ books: [String], format: BookFormatter) {
for title in books {
print(format(title))
}
}
И вот ради чего всё это. Мы можем подставлять разные правила, не переписывая циклы:
import Foundation
func isShort(_ title: String) -> Bool { title.count <= 6 }
let shortOnes = filterBooks(library, where: isShort)
printBooks(shortOnes) { "- \($0)" }
// - Swift
// - Dune
// - 1984
Обратите внимание на стиль: isShort — именованная функция (её удобно переиспользовать), а форматирование выводим замыканием «на месте», потому что оно маленькое и одноразовое.
5. Функции, которые строят правила
После того как вы приняли мысль «функция — это значение», начинают работать очень красивые приёмы: операторы можно передавать как функции, инициализаторы — тоже, а ещё можно писать функции, которые возвращают новые «правила».
Операторы как значения
Сделаем свою функцию «посчитать что-то по двум числам» и передадим туда + и *:
import Foundation
func combine(_ a: Int, _ b: Int, using op: (Int, Int) -> Int) -> Int {
op(a, b)
}
print(combine(3, 4, using: +)) // 7
print(combine(3, 4, using: *)) // 12
Это тот же принцип, что и reduce(0, +), только мы сами создали «место», куда можно подставлять оператор.
Инициализаторы как значения
Теперь про Int.init. Представим, что пользователь ввёл оценки книг как строки, и мы хотим превратить их в числа. Int.init имеет подходящую форму для compactMap: «строка → Int?».
import Foundation
let rawRatings = ["5", "x", "4", "10", "nope"]
let ratings = rawRatings.compactMap(Int.init)
print(ratings) // [5, 4, 10]
Смысл простой: вы не пишете { Int($0) }, а передаёте готовое правило «как пробовать сделать Int».
Возвращаем функцию из функции
Иногда удобнее не только передавать функции в функции, но и писать функцию, которая создаёт и возвращает новое правило, например «искать книги, содержащие слово». Это похоже на изготовление формочки для печенья: вы один раз сделали форму «звёздочка», а потом штампуете сколько угодно печенек.
Сделаем «конструктор предиката» для поиска подстроки:
import Foundation
func makeTitleContainsPredicate(_ query: String) -> (String) -> Bool {
let q = normalize(query)
return { title in
normalize(title).contains(q)
}
}
Теперь используем:
import Foundation
let containsSwift = makeTitleContainsPredicate("swift")
let found = filterBooks(library, where: containsSwift)
printBooks(found) { "FOUND: \($0)" }
// FOUND: Swift
// FOUND: The Swift Programming Language (если бы была в списке)
Здесь происходит тихая магия без мистики: внутренняя функция (замыкание) «помнит» q. Вы это уже встречали в теме про захват переменных в замыканиях, просто теперь это стало практическим инструментом.
6. Типичные ошибки
Когда функции начинают путешествовать по коду как обычные значения, ошибки становятся немного «философскими»: программа компилируется, но вы передали не то, вызвали не там или слишком усложнили чтение. Это нормально, через это проходят все. Главное — научиться быстро узнавать эти ситуации по симптомам и править без боли.
Ошибка №1: функция случайно вызывается вместо передачи.
Самый частый случай — написать where: isShort() вместо where: isShort. Скобки превращают «правило» в «действие», а действие требует аргументов. Если компилятор ругается на "missing argument" или "cannot convert value … to expected argument type", первым делом проверьте, не прилипли ли ().
Ошибка №2: у передаваемой функции не совпадает тип.
Например, вы хотите передать в фильтр правило (String) -> Bool, а у вас функция (String) -> Int. В быту это звучит как «я принёс молоток туда, где нужна отвёртка». Лечится просто: смотрите на ожидаемый тип параметра и сверяйте «что принимает» и «что возвращает» ваша функция.
Ошибка №3: замыкание становится слишком большим и превращается в мини-роман.
Если внутри передаваемого замыкания 10–15 строк, несколько if/else и переменные, это почти всегда сигнал вынести логику в именованную функцию. Так проще читать, проще переиспользовать и проще отлаживать (а ещё проще объяснять одногруппникам, что вы вообще написали).
Ошибка №4: захват переменных в замыкании используется неосознанно.
Замыкание может «помнить» внешние значения (как в примере с q), и это удобно. Но если вы начинаете мутировать внешнюю var внутри правила, код становится менее предсказуемым. На этом дне курса мы ещё не обсуждаем все последствия глубоко, но полезная привычка уже сейчас: стараться, чтобы «правила» для filter/map/reduce были про вычисление, а не про изменение внешнего мира.
Ошибка №5: переусложнение интерфейса своей функции.
Иногда новичок так вдохновляется, что делает функцию с тремя замыканиями, двумя массивами и пятью typealias — и потом сам же не может её вызвать. Начинайте с минимального: один массив + одно правило. Как только это стало ясно и удобно — добавляйте следующий уровень, но только если он реально нужен для читаемости.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ