JavaRush /Курси /Swift SELF /Trailing closures та виведення типів

Trailing closures та виведення типів

Swift SELF
Рівень 16 , Лекція 1
Відкрита

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))", вам треба памʼятати, що $0title, а $1year. Для дуже коротких виразів це нормально. Для всього, що довше за один рядок або містить кілька операцій, починається питання: «що таке $1 і чому він тут головний?»

Гарне правило стилю, яке рятує нерви: якщо замикання довше за один рядок або якщо $0/$1 не читаються миттєво, краще повернути імена параметрів.

Щоб закріпити, порівняймо дві версії:

Стиль Приклад Коли доречно
Іменовані параметри
{ title, year in "\(title) (\(year))" }
Майже завжди — читабельно й зрозуміло
$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. У таких випадках краще явно написати мітку або передати замикання звичайним аргументом.

1
Задача
Swift SELF, 16 рівень, 1 лекція
Недоступна
Привітання голосно
Привітання голосно
1
Задача
Swift SELF, 16 рівень, 1 лекція
Недоступна
Друк за шаблоном
Друк за шаблоном
1
Задача
Swift SELF, 16 рівень, 1 лекція
Недоступна
Два формати
Два формати
1
Задача
Swift SELF, 16 рівень, 1 лекція
Недоступна
Команда операції
Команда операції
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ