1. Тип замикання: входи й виходи
Якби Swift був кухнею, то функції були б рецептами з назвою, а замикання — рецептами на серветці: без назви, зате з чіткими кроками. Іноді саме така серветка рятує ситуацію, бо вам не хочеться оголошувати окрему функцію заради одного маленького правила — «перевірити число» або «перетворити рядок». Замикання (closure) — це блок коду, який можна зберігати у змінній і передавати як значення.
Головна ідея: замикання — це «функція без імені», але з входом і виходом. Тому замикання має тип, і цей тип виглядає так само, як тип функції: наприклад, (Int) -> Int означає «отримую Int, повертаю Int».
Тип замикання — це, по суті, «паспорт»: що приймаємо, що повертаємо. Цей запис спершу здається дивним, але до нього швидко звикаєте, особливо якщо читати його вголос. Наприклад, (Int) -> Int можна читати як «з Int у Int», а () -> Void — як «нічого не приймає і нічого не повертає, а просто виконує свою роботу». Важливо навчитися бачити саме вхід і вихід.
Ось три найчастіші форми, з якими ви будете зустрічатися постійно:
- () -> Void — дія без параметрів (наприклад, «показати привітання»).
- (Int) -> Int — перетворення одного значення на інше.
- (String, Int) -> Bool — перевірка: за рядком і числом визначити true/false.
Створімо замикання, яке просто друкує рядок. Це дія: воно нічого не повертає.
import Foundation
let sayHi: () -> Void = {
print("Привіт") // Привіт
}
sayHi()
Зверніть увагу на дві речі. По-перше, тип () -> Void стоїть ліворуч і допомагає компілятору зрозуміти, чого ми хочемо. По-друге, замикання не виконується під час оголошення — воно виконується лише тоді, коли ви його викликаєте: sayHi().
Повна форма: in і явні типи
Повна форма замикання — це «найчесніший» запис: ми явно пишемо параметри, явно вказуємо тип результату і ставимо in, яке відокремлює «шапку» від тіла. in тут як дверний проріз: усе, що ліворуч, — опис входу й виходу, а все, що праворуч, — код, який буде виконано. Якщо in забути, компілятор буде сваритися, а ви — читати повідомлення про помилку. Це теж корисно, але сьогодні це не головний герой.
Зробімо замикання, яке множить число на 3. Це простий приклад, але він ідеально показує структуру.
import Foundation
let triple: (Int) -> Int = { (x: Int) -> Int in // підпис замикання
return x * 3
}
print(triple(4)) // 12
Тут усе максимально явно: параметр x типу Int, результат теж Int, усередині — звичайний return. Так, це багатослівно. Зате саме такий запис ідеально підходить для першого знайомства, коли важливо спочатку побачити форму.
Замикання як значення: змінна й виклик
Найбільш незвична частина для новачків полягає в тому, що функцію можна викликати, а замикання… теж можна викликати, хоча воно лежить у змінній. У цьому місці корисно заспокоїтися: замикання справді є значенням, але таким значенням може бути й функціональний тип. Тому синтаксис виклику однаковий: дужки після імені.
Покажу типову ситуацію, де новачки плутаються: «Я написав замикання, чому воно не спрацювало?» Тому що ви його не викликали. Це як купити чайник і чекати, що він сам закипʼятить воду, просто тому, що стоїть на столі.
import Foundation
let isEven: (Int) -> Bool = { (n: Int) -> Bool in
return n % 2 == 0
}
print(isEven(10)) // true
print(isEven(7)) // false
З погляду читання коду це виглядає як звичайна функція. І це добре: Swift спеціально робить замикання «функціональними за відчуттям», щоб ви могли думати про них як про маленькі правила.
2. Замикання як параметри функцій
Тепер важливий крок: навіщо взагалі зберігати «функцію у змінній»? Одна з головних відповідей — щоб передавати поведінку в іншу функцію. Тобто ми пишемо функцію, яка виконує загальну роботу, а замиканням передаємо конкретне правило. Це схоже на ситуацію: є співробітник (функція), який уміє обробляти документи, але ви даєте йому інструкцію (замикання), які саме документи вважати «придатними».
Зробімо функцію apply, яка бере число й застосовує до нього правило перетворення. Зверніть увагу: у цій лекції ми не використовуємо trailing closure, щоб не забігати наперед, — передамо замикання як звичайний аргумент. Усьому свій час.
import Foundation
func apply(_ value: Int, transform: (Int) -> Int) -> Int {
return transform(value)
}
let result = apply(10, transform: { (x: Int) -> Int in
return x + 5
})
print(result) // 15
Зміст дуже простий: apply не знає, що саме робити з числом, але вміє викликати правило transform. І це основа величезної кількості API у Swift, які ви надалі будете читати й використовувати.
4. Скорочення в замиканнях
Після повної форми хочеться запитати: «А можна коротше, а то моя клавіатура втомилася?» Можна. Swift уміє виводити типи з контексту, а ще дозволяє не писати return, якщо замикання складається з одного виразу. Але тут важливо не впасти в надмірно стислий код: занадто коротко — і через тиждень ви самі не зрозумієте, що написали.
Спочатку приберемо типи параметрів і результату — вони вже відомі з типу змінної ліворуч.
import Foundation
let triple: (Int) -> Int = { x in
x * 3
}
print(triple(4)) // 12
Що сталося? Тип (Int) -> Int уже задано, отже x точно Int, а x * 3 теж Int. return можна не писати, бо тіло складається з одного виразу.
Автоматичні імена аргументів: $0, $1
Тепер найвпізнаваніше: $0, $1. Новачки часто сприймають це як «якусь секретну мову старих розробників». Насправді все простіше: якщо ви не написали імена параметрів явно, Swift дає вам автоматичні імена за порядком. $0 — перший параметр, $1 — другий, і так далі.
Це зручно в коротких замиканнях, де правило очевидне з першого погляду. Але якщо правило складніше, $0 перетворюється на «хто всі ці люди?» — і краще дати параметрам нормальні імена.
import Foundation
let sumVerbose: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
let sumShort: (Int, Int) -> Int = { $0 + $1 }
print(sumVerbose(2, 3)) // 5
print(sumShort(2, 3)) // 5
Зверніть увагу: sumShort виглядає класно, але це працює саме тому, що вираз дуже простий. Якби всередині було 2–3 умови й обчислення, читабельність швидко б випарувалася.
5. Кортежі як результат замикання
Іноді замикання не зобовʼязане повертати одне значення: воно може повертати кортеж (tuple). Кортежі ви вже бачили раніше як «легку структуру без оголошення struct». І звʼязка $0/$1 + кортеж дає дуже наочний прийом: взяти два входи й повернути їх як пару.
Зробімо замикання, яке приймає два числа й повертає їх як пару (first: Int, second: Int). Так, звучить безглуздо, але це чудова демонстрація синтаксису, а пізніше такий прийом буває корисним під час перегрупування даних.
import Foundation
let makePair: (Int, Int) -> (first: Int, second: Int) = { ($0, $1) }
let pair = makePair(10, 20)
print(pair.first) // 10
print(pair.second) // 20
Тут важливо, що ($0, $1) — це просто літерал кортежу. Він створює новий кортеж, де перший елемент — це перший аргумент замикання, а другий — другий аргумент замикання.
6. Шпаргалка зі стилів запису
Коли синтаксис гнучкий, виникає проблема: «А як правильно писати?» Відповідь: правильно — це коли код читається без болю. Щоб легше було обирати форму, тримайте в голові таку шкалу: чим більша команда й чим складніша логіка, тим частіше виграє явніший стиль. Чим коротше правило, тим більше сенсу в скороченнях.
Нижче — маленька табличка-шпаргалка, яка допомагає «перекладати» замикання з одного виду в інший:
| Рівень «детальності» | Приклад | Коли доречно |
|---|---|---|
| Повна форма | |
Під час навчання, налагодження або коли логіка складна |
| Тип виводиться, імена є | |
Звичний повсякденний варіант |
| $0/$1 | |
Для дуже коротких правил в один рядок |
| Повернення tuple | |
Потрібно повернути пару або кілька значень |
Ця табличка не «закон», а компас. Код пишеться не заради компілятора — він і так упорається, — а заради людей, які читатимуть цей код. І так, цією людиною за місяць будете ви.
7. Мініприклад: валідація введення через замикання
Щоб замикання не лишилися на папері, додамо їх у маленький консольний застосунок, який ми поступово розвиваємо. Нехай це буде StudyBuddy — трекер навчання: користувач вводить тему й кількість хвилин, а ми зберігаємо записи в масив. Наразі наша мета скромна: зробити функцію читання числа, але з налаштованим правилом перевірки.
Почнемо з функції readInt, яка читає рядок, перетворює його на Int і перевіряє через validator. Якщо перевірка не проходить — повертаємо nil. Це дуже практичний шаблон: «заготовка + зовнішнє правило».
import Foundation
func readInt(_ prompt: String, validator: (Int) -> Bool) -> Int? {
print(prompt, terminator: " ")
guard let line = readLine(), let value = Int(line) else { return nil }
return validator(value) ? value : nil
}
Тепер створимо пару «правил» як замикання. Одне правило — «хвилини мають бути більшими за нуль», друге — «хвилини мають бути від 5 до 600» (щоб ніхто не ввів 999999 і не оголосив себе богом продуктивності).
import Foundation
let isPositive: (Int) -> Bool = { $0 > 0 }
let isReasonableStudyTime: (Int) -> Bool = { minutes in
minutes >= 5 && minutes <= 600
}
І використаємо це в блоці введення застосунку. Поки що без складних меню та команд: лише одне введення.
import Foundation
if let minutes = readInt("Скільки хвилин ви навчалися?", validator: isReasonableStudyTime) {
print("Гаразд, записали \(minutes) хвилин.") // наприклад: Гаразд, записали 45 хвилин.
} else {
print("Це не схоже на коректне число хвилин.") // якщо ввели "abc" або "2"
}
Зверніть увагу на стиль: validator: приймає той самий тип, що й функція, — (Int) -> Bool. Тому ми можемо підставити туди змінну із замиканням. І так, це саме той випадок, коли замикання — «правило», а не «випадковий шматок коду».
8. Типові помилки під час роботи з замиканнями
Помилка № 1: забувають, що замикання не виконується само по собі.
Дуже частий сценарій: студент написав let f = { print("Привіт") }, побачив, що застосунок мовчить, і запідозрив, що Swift зламаний. Насправді ви створили значення — замикання — і поклали його у змінну, а запускати його потрібно явно: f(). Це нормальна плутанина, бо мозок ще не звик до ідеї «код як дані».
Помилка № 2: плутають тип замикання та його виклик.
Тип (Int) -> Int — це не «як викликати», а «що воно приймає і що повертає». Виклик виглядає як f(10). Через схожість дужок легко переплутати, особливо коли ви щойно вивчали функції. Хороший прийом — подумки додавати слова: «тип: з Int у Int», «виклик: передали 10».
Помилка № 3: забувають in у повній формі.
Коли ви пишете повну форму { (x: Int) -> Int in return x + 1 }, слово in обовʼязкове. Воно відокремлює опис параметрів і типу від тіла. Без нього компілятор не розуміє, де закінчилася «шапка». Якщо in дратує — це нормально. Скорочені форми існують саме тому, що в більшості випадків повна форма надмірна.
Помилка № 4: зловживають $0 у складній логіці.
$0 — чудовий, коли вираз короткий: { $0 + 1 }. Але якщо всередині починаються if, кілька обчислень і guard, то $0 перетворює код на ребус. У таких випадках краще написати { value in ... } і дати аргументу нормальне імʼя. Читабельність важливіша за економію трьох символів — особливо коли баги коштують дорожче за клавіатуру.
Помилка № 5: повертають не той тип або забувають повернути значення.
Якщо тип указано як (Int) -> Bool, замикання зобовʼязане повернути Bool на кожному шляху виконання. Новачки іноді пишуть print(...) усередині й забувають повернути true/false, або повертають String за звичкою. Тут допомагає правило: спочатку визначити тип, а потім написати тіло так, щоб воно йому відповідало.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ