1. Замикання можна скорочувати
Коли ви вперше бачите замикання в повній формі, здається, що Swift змушує вас дуже докладно пояснювати компілятору очевидні речі: «ось параметр x, ось його тип Int, ось ми повертаємо Int…». Це нормально на етапі навчання: повна форма — як навчальні коліщата на велосипеді. Їде повільно, зате ви не падаєте.
Але в реальному Swift-коді — і в стандартній бібліотеці — замикання пишуть компактно. Причина проста: типи часто вже відомі з контексту. Якщо функція очікує замикання типу (Int) -> Bool, то компілятор і так розуміє, що всередині { ... } параметр — це Int, а результат — Bool. Ми можемо прибрати зайві слова й залишити зміст.
У цій лекції ми вчимося робити код коротшим без втрати читабельності. Мета не в тому, щоб написати якнайменше символів, а в тому, щоб текст читався легко й без зайвого напруження.
Контекст і виведення типів
Ключова ідея така: типи в замиканні часто можна не писати, бо їх підказує оточувальний код.
Якщо у вас є змінна з типом замикання, усе всередині можна сильно спростити:
import Foundation
let makeLoud: (String) -> String = { (text: String) -> String in
return text.uppercased()
}
print(makeLoud("Swift")) // SWIFT
Зараз це максимально «офіційна» версія. Але тип makeLoud уже відомий: (String) -> String. Отже, всередині замикання ми можемо прибрати типи параметрів і результату:
import Foundation
let makeLoud: (String) -> String = { text in
return text.uppercased()
}
print(makeLoud("Swift")) // SWIFT
Зверніть увагу, що слово in залишилося. Воно відокремлює «заголовок» замикання — тобто параметри — від тіла. Навіть коли ми прибрали типи, in усе ще потрібне, тому що компілятору треба зрозуміти: «ось закінчилися параметри, далі — код».
Якщо хочете уявити це образно, думайте так: тип замикання — це контракт, і оточувальний код часто цей контракт уже підписав. Ми просто перестаємо повторювати його вголос.
Варто окремо памʼятати: виведення типів у замиканнях — це не магія, а цілком конкретні правила типізації, які розвивалися разом із мовою. Зокрема, Swift намагається виводити параметри й результат замикання з контексту, а для багаторядкових замикань є окремі нюанси.
2. Сходинки скорочень замикань
Важливо не переходити одразу з повної форми в ультракоротку. У мозку — і в колег у команді також — є межа швидкості адаптації. Тому давайте розберемо «сходинки скорочень» крок за кроком, щоб було зрозуміло, що саме ми прибрали й чому.
Уявімо, що ми створюємо мінізастосунок «кишенькова бібліотека»: у нас є список книжок, і ми хочемо гарно виводити його в консоль. Поки що без struct — до них ми ще повернемося — використаємо кортежі, з якими ви вже знайомі.
import Foundation
let books: [(title: String, year: Int)] = [
(title: "Clean Code", year: 2008),
(title: "The Pragmatic Programmer", year: 1999)
]
Створімо функцію, яка виводить список книжок, а форматування передає назовні через замикання. Тобто: ось вам книга — а ви поверніть рядок для виводу.
import Foundation
func printBooks(
_ books: [(title: String, year: Int)],
formatter: ((String, Int) -> String)
) {
for book in books {
let line = formatter(book.title, book.year)
print(line)
}
}
Тепер виклик у повній формі:
import Foundation
let books: [(title: String, year: Int)] = [
(title: "Clean Code", year: 2008),
(title: "The Pragmatic Programmer", year: 1999)
]
func printBooks(
_ books: [(title: String, year: Int)],
formatter: ((String, Int) -> String)
) {
for book in books {
let line = formatter(book.title, book.year)
print(line)
}
}
printBooks(books, formatter: { (title: String, year: Int) -> String in
return "\(title) (\(year))"
})
// Clean Code (2008)
// The Pragmatic Programmer (1999)
Це працює, але виглядає громіздко. І саме тут починається наше сьогоднішнє свято скорочень.
Неявний return
Коли тіло замикання складається з одного виразу, Swift дозволяє не писати return: результат виразу і є значенням, що повертається. Це не «магія», а просто синтаксичний цукор, щоб код був чистішим.
Зробімо перший крок: приберемо return.
import Foundation
printBooks(books, formatter: { (title: String, year: Int) -> String in
"\(title) (\(year))"
})
// Clean Code (2008)
// The Pragmatic Programmer (1999)
Код став легшим, і головне — зміст читається швидше.
Тут є важлива практична думка: неявний return доречний тоді, коли вираз короткий і зрозумілий. Якщо всередині пʼять умов, кілька тимчасових змінних і складна логіка, краще писати явно або взагалі винести її в окрему функцію. Замикання має бути маленьким, інакше воно перетворюється на «міні-роман у дужках».
Виведення типів параметрів
Наступний крок — прибрати типи параметрів і результату із заголовка замикання. Чому це можна зробити? Тому що printBooks уже сказав: formatter має бути ((String, Int) -> String). Компілятор знає, що перший аргумент — рядок, другий — ціле число, а результат — рядок.
import Foundation
printBooks(books, formatter: { title, year in
"\(title) (\(year))"
})
// Clean Code (2008)
// The Pragmatic Programmer (1999)
Це один із найуживаніших нормальних стилів у Swift: коротко, але імена параметрів залишаються людяними. Часто на цьому рівні вже можна зупинитися — код компактний і читабельний.
Зверніть увагу: ми не просто стерли типи. Ми поклалися на тип, який задає контекст. Якщо контексту немає, Swift може вимагати типи явно. Це нормально: компілятор не телепат.
$0 і $1: ультракоротко
Тепер те, що часто люблять — або ненавидять: скорочені імена аргументів $0, $1, $2… Вони доступні, якщо ви не вказали імена параметрів явно.
Зробімо ще коротше:
import Foundation
printBooks(books, formatter: {
"\($0) (\($1))"
})
// Clean Code (2008)
// The Pragmatic Programmer (1999)
Виглядає компактно, але є нюанс: коли ви читаєте "\($0) (\($1))", вам треба памʼятати, що $0 — title, а $1 — year. Для дуже коротких виразів це нормально. Для всього, що довше за один рядок або містить кілька операцій, починається питання: «що таке $1 і чому він тут головний?»
Гарне правило стилю, яке рятує нерви: якщо замикання довше за один рядок або якщо $0/$1 не читаються миттєво, краще повернути імена параметрів.
Щоб закріпити, порівняймо дві версії:
| Стиль | Приклад | Коли доречно |
|---|---|---|
| Іменовані параметри | |
Майже завжди — читабельно й зрозуміло |
| $0/$1 | |
Коли вираз дуже короткий і очевидний |
3. Trailing closure
Головна тема лекції — trailing closure. Це синтаксис, за якого останній аргумент функції, якщо він є замиканням, можна винести за круглі дужки.
Наче й небагато, але на практиці це сильно змінює читабельність: виклик починає виглядати як маленька інструкція.
Перепишімо наш виклик printBooks через trailing closure:
import Foundation
printBooks(books) { title, year in
"\(title) (\(year))"
}
// Clean Code (2008)
// The Pragmatic Programmer (1999)
Зверніть увагу: ми навіть не написали formatter:. І це не тому, що «ми забули», а тому, що trailing closure у Swift має особливе правило: його можна передати як останній параметр-замикання, навіть якщо в нього є мітка.
Якщо хочеться запамʼятати на побутовому рівні: trailing closure робить виклик функції схожим на конструкцію керування, майже як if { ... } або while { ... }, хоча це звичайна функція.
Коли дужки можна прибрати повністю
Якщо єдиний аргумент функції — замикання, круглі дужки можна прибрати повністю.
Створімо мініфункцію «повтори дію N разів».
import Foundation
func repeatAction(times: Int, action: () -> Void) {
for _ in 0..<times {
action()
}
}
repeatAction(times: 2) {
print("Додаємо книжку…")
}
// Додаємо книжку…
// Додаємо книжку…
А тепер приклад, де замикання — єдиний аргумент:
import Foundation
func run(_ action: () -> Void) {
action()
}
run {
print("Запуск без дужок — звучить підозріло, але це Swift")
}
// Запуск без дужок — звучить підозріло, але це Swift
Так, виглядає незвично, але це нормальний стиль. Головне — не зловживати: якщо у функції вже є інші аргументи, дужки краще лишити, так читати простіше.
Trailing closure і читабельність
Trailing closure — потужна річ, але в неї є пастка: можна написати так «красиво», що потім ніхто не зрозуміє, що куди передається. Особливо це помітно, коли у функції є кілька параметрів-замикань, а ще один із них може мати значення за замовчуванням — або коли в проєкті є перевантаження.
Останніми роками Swift спеціально вдосконалював правила зіставлення trailing closure з параметрами, бо історично це місце могло підкидати сюрпризи. У Swift 6 активно просувається більш передбачуване зіставлення, а в документах з еволюції мови це називають переходом до «forward scan». Компілятор навіть може попереджати про неоднозначні випадки.
У нашому навчальному коді ми поки що не будемо будувати такі «пастки», але важливу звичку закладемо вже зараз: якщо ви відчуваєте, що trailing closure робить виклик неоднозначним, краще повернути мітку або записати замикання як звичайний аргумент.
Наприклад, ось так — цілком читабельно й безпечно:
import Foundation
printBooks(books, formatter: { title, year in
"\(title) (\(year))"
})
А ось так — теж читабельно, і часто навіть краще:
import Foundation
printBooks(books) { title, year in
"\(title) (\(year))"
}
Сенс у тому, що trailing closure — це інструмент для читача, а не для того, щоб ви виграли олімпіаду з економії символів.
4. Схема: як компілятор виводить типи
Щоб закріпити інтуїцію, корисно побачити процес у вигляді простої блок-схеми. Не бійтеся, це не «теорія компіляторів», а звичайна логіка.
flowchart TD
A["Функція очікує параметр-замикання (наприклад, (String, Int) -> String)"] --> B["Ви пишете { title, year in ... }"]
B --> C["Компілятор бере тип із контексту виклику"]
C --> D["Розуміє: title = String, year = Int"]
D --> E["Перевіряє тіло замикання: чи повертається String?"]
E --> F["Якщо все збігається — код компілюється"]
Саме тому сьогодні ми так багато говоримо про контекст. Типи ми не «втрачаємо» — ми просто перестаємо їх дублювати.
5. Типові помилки
Помилка №1: використовувати $0/$1 у замиканні, яке вже перестало бути коротким.
Це часта історія: ви почали з { $0 + $1 }, потім додали кілька умов, потім ще одну перевірку — і раптом код починає нагадувати шифр. У такий момент краще не геройствувати, а повернути імена параметрів: { left, right in ... }. У вас одразу зʼявляться слова, а не номери.
Помилка №2: намагатися прибрати типи там, де контекст не задає тип замикання.
Якщо ви пишете замикання «саме по собі», без явного типу змінної й без передавання у функцію, компілятор може не зрозуміти, які це параметри. Тоді доведеться або вказати тип змінної (наприклад, let f: (Int) -> Int = { ... }), або написати повну форму в заголовку замикання. Це не слабкість мови — це чесність: компілятор не зобовʼязаний вгадувати те, чого ви не сказали.
Помилка №3: забути, що trailing closure працює лише для останнього аргументу.
Іноді новачки намагаються винести замикання будь-куди. У базовому варіанті trailing closure працює лише для останнього аргументу, якщо він є замиканням. Саме тому функції часто проєктують так, щоб потрібне замикання було останнім. Ця ідея лежить в основі багатьох API Swift і обговорюється під час розвитку синтаксису trailing closures.
Помилка №4: робити виклик «красивим», але неоднозначним.
Trailing closure покращує читабельність, коли все однозначно. Але якщо у функції кілька параметрів-замикань, є перевантаження або аргументи за замовчуванням, можна отримати неочікувані прив’язки trailing closure і навіть попередження компілятора в Swift 6. У таких випадках краще явно написати мітку або передати замикання звичайним аргументом.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ