1. Значення за замовчуванням
Коли ви лише починаєте, може здаватися, що проблема «переплутав параметри місцями» — це щось зі світу великих проєктів. Та на практиці вона виникає дуже рано: щойно зʼявляється функція з 3–5 параметрами одного типу (наприклад, кілька Int або кілька Boolean). Мозок одразу починає «підкладати свиню». І річ не в тому, що ви неуважні, — це звичайна людська особливість: у нас немає вбудованого аналізатора сигнатур.
Рішення в Kotlin просте й дієве: частину параметрів можна зробити необовʼязковими, задавши значення за замовчуванням. А під час виклику — підписувати аргументи їхніми іменами. Так виклик функції перетворюється з магічного заклинання на зрозумілу фразу, яку легко прочитати як звичайний текст.
Робимо параметр необовʼязковим
Почнімо з простого. Часто функцію викликають «типово», і лише зрідка потрібно уточнити якусь деталь. Наприклад, «надрукуй заголовок» майже завжди друкує лінію = однакової ширини — але інколи хочеться зробити її ширшою або використати інший символ. У таких випадках значення за замовчуванням — це як налаштування, яке зазвичай не потрібно чіпати.
Синтаксис елементарний: в оголошенні параметра пишемо = … Kotlin підставить це значення, якщо під час виклику ви не передасте аргумент для цього параметра.
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printHeader("Гаманець") // друкує ширину 30 і '='
printHeader("Гаманець", width = 10) // друкує ширину 10 і '='
}
Зверніть увагу на важливий момент: ми не робимо «дві версії» функції. Функція одна, але має «налаштування за замовчуванням». Майже завжди для проєкту це краще, ніж плодити багато схожих функцій із трохи різними іменами.
Значення за замовчуванням — це не лише константа
Може здаватися, що значення за замовчуванням має бути чимось простим на кшталт 0, true або "text". Але Kotlin дозволяє використовувати будь-який вираз — зокрема й такий, що залежить від інших параметрів. Це дуже зручно: наприклад, «довжина за замовчуванням дорівнює розміру масиву» або «ліміт за замовчуванням дорівнює довжині рядка».
Є лише одне важливе правило: якщо значення за замовчуванням залежить від іншого параметра, то цей «інший параметр» має стояти раніше у списку параметрів. Це логічно: не можна посилатися на те, чого ще «немає».
Невеликий приклад на нашому проєкті «Гаманець»: хочемо надрукувати список витрат і за замовчуванням виводити його повністю.
fun printExpenses(amounts: IntArray, size: Int, limit: Int = size) {
var i = 0
while (i < size && i < limit) {
println("Витрата #${i + 1}: ${amounts[i]}") // наприклад: Витрата #1: 150
i++
}
}
fun main() {
val amounts = intArrayOf(150, 40, 999)
val size = 3
printExpenses(amounts, size) // друкує 3 витрати
printExpenses(amounts, size, limit = 1) // друкує 1 витрату
}
Порядок параметрів: обовʼязкові — спереду, із дефолтами — позаду
А тепер — важлива, але цілком логічна думка. Значення за замовчуванням робить параметр необовʼязковим — тобто його можна не вказувати. Тому такі параметри зручніше ставити ближче до кінця списку, а обовʼязкові — ближче до початку. Так компілятору (і вам) простіше однозначно зрозуміти, що саме передано.
У Kotlin є спеціальне правило: якщо параметр із дефолтом стоїть перед параметром без дефолту, то під час виклику ви зможете опустити цей параметр лише тоді, коли використаєте іменований аргумент.
Зробімо приклад «як краще не робити»:
fun greeting(userId: Int = 0, message: String) {
println("[$userId] $message")
}
fun main() {
greeting(message = "Привіт!") // [0] Привіт!
greeting("Привіт!") // так не можна: компілятор буде лаятися
}
Такий порядок іноді справді потрібен (таке трапляється), але частіше це просто невдалий дизайн. У навчальних завданнях і в нашому застосунку краще дотримуватися простого правила: «найважливіше — спочатку, налаштування — потім».
Наприклад, ось приємніший варіант:
fun greeting(message: String, userId: Int = 0) {
println("[$userId] $message")
}
fun main() {
greeting("Привіт!") // [0] Привіт!
greeting("Привіт!", 42) // [42] Привіт!
}
2. Іменовані аргументи
Підписуємо аргументи й перестаємо боятися однакових типів
Іменовані аргументи потрібні не тому, що Kotlin «любить багатослівність», а тому що вони різко знижують імовірність помилки. Особливо коли параметри мають однаковий тип або коли саме значення нічого не пояснює. Класика: набір аргументів true, false, true — і думка «що це взагалі було?».
Синтаксис такий: під час виклику пишемо імʼяПараметра = значення. Kotlin дозволяє перелічувати іменовані аргументи у будь-якому порядку — саме заради читабельності.
Зробімо функцію для додавання витрати в масив (поки без категорій, щоб не забігати наперед). Нехай у нас є «дата» як день і місяць — примітивно, зате для прикладу ідеально.
fun addExpense(
amounts: IntArray,
days: IntArray,
months: IntArray,
size: Int,
amount: Int,
day: Int,
month: Int
): Int {
amounts[size] = amount
days[size] = day
months[size] = month
return size + 1
}
fun main() {
val amounts = IntArray(10)
val days = IntArray(10)
val months = IntArray(10)
var size = 0
size = addExpense(amounts, days, months, size, amount = 150, day = 14, month = 1)
println(size) // 1
}
Якби ми викликали це позиційно як addExpense(…), воно виглядало б як «телефонний номер». А з іменами сенс видно відразу: amount, day, month.
Як змінювати налаштування й не передавати зайвого
Поширений сценарій: у функції є кілька параметрів зі значеннями за замовчуванням, і ви хочете змінити не перший із них, а якийсь інший. Kotlin це дозволяє. Тут корисно запамʼятати просте правило: якщо ви пропускаєте параметри зі значеннями за замовчуванням, то все, що задаєте далі, зазвичай простіше (і безпечніше) передавати іменовано.
Офіційне формулювання таке: ви можете пропускати частину аргументів зі значеннями за замовчуванням, але після першого пропущеного аргументу потрібно іменувати всі наступні. Інакше незрозуміло, що до чого належить.
Приклад: функція друку звіту. За замовчуванням виводимо і список, і суму, а валюта — "EUR". Інколи хочеться змінити лише валюту, не зачіпаючи решту налаштувань.
fun printReport(
title: String,
showList: Boolean = true,
showTotal: Boolean = true,
currency: String = "EUR"
) {
println("== $title ==")
println("showList=$showList, showTotal=$showTotal, currency=$currency")
}
fun main() {
printReport("Звіт") // == Звіт == / showList=true, showTotal=true, currency=EUR
printReport("Звіт", currency = "USD") // == Звіт == / showList=true, showTotal=true, currency=USD
}
Якби іменованих аргументів не було, нам довелося б писати щось на кшталт printReport("Звіт", true, true, "USD") — і знову вгадувати, що означають ті два true.
Змішуємо позиційні та іменовані аргументи
На практиці часто виходить «гібридний стиль»: перші 1–2 параметри дуже зрозумілі за змістом (зазвичай title, text, amount), і їх зручно передавати позиційно. Далі зазвичай починаються налаштування — і їх краще задавати іменовано. Kotlin це підтримує.
Для читабельності зазвичай дотримуються простого правила: спочатку йдуть позиційні аргументи, а потім — іменовані. Такий виклик виглядає охайно: «основна думка + уточнення».
Застосуймо це до нашого заголовка:
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printHeader("Гаманець", border = '#') // спочатку title, потім налаштування
printHeader("Гаманець", width = 10, border = '*')
}
Такий стиль особливо корисний, коли ви проєктуєте «міні-API» для себе: базові параметри читаються швидко, а рідкісні налаштування не заважають. Водночас вони завжди залишаються доступними.
3. Практика: меню «Гаманець»
Міні-рефакторинг: робимо меню розширюваним
Тепер зберемо невеликий, але цілісний фрагмент нашого консольного застосунку «Гаманець». Ми не додаємо нічого принципово нового в логіці — натомість покращуємо інтерфейс функцій: задаємо значення за замовчуванням і використовуємо іменовані аргументи, щоб main() читався як сценарій.
У нас буде стандартне меню: показати команди, додати витрату, вивести список, вивести звіт. Ми ще не робимо «ідеальний розбір» введення (це буде на наступних рівнях), тож обмежимося простим читанням рядка та базовими перевірками.
fun printMenu(appName: String = "Гаманець", showHint: Boolean = true) {
printHeader(appName, width = 25)
println("1) Додати витрату")
println("2) Показати витрати")
println("3) Показати суму")
println("0) Вихід")
if (showHint) println("Введіть номер команди та натисніть Enter") // Введіть номер команди...
}
fun printHeader(title: String, width: Int = 30, border: Char = '=') {
val line = border.toString().repeat(width)
println(line)
println(title)
println(line)
}
fun main() {
printMenu() // дефолтна назва та підказка
printMenu(showHint = false) // той самий екран, але без підказки
printMenu(appName = "Budget v0") // кастомна назва
}
Тут важлива сама ідея. Ми зробили поведінку за замовчуванням типовою для більшості викликів, а рідкісні налаштування винесли в параметри. А завдяки іменованим аргументам printMenu(showHint = false) виглядає як читабельна фраза.
Памʼятка: значення за замовчуванням та іменування
На цьому етапі зазвичай хочеться запитати: «Гаразд, а як зрозуміти, що краще — значення за замовчуванням чи іменування?» Гарна новина: є практичні критерії. Значення за замовчуванням допомагають зробити «звичайний шлях» коротшим, а іменовані аргументи — зробити «незвичайний шлях» безпечнішим і зрозумілішим.
| Ситуація | Що краще використовувати | Чому |
|---|---|---|
| Параметр майже завжди однаковий | Значення за замовчуванням | Прибираємо «шум» у більшості викликів |
| Кілька параметрів одного типу (Int, Boolean, Char) | Іменовані аргументи | Зменшуємо шанс переплутати порядок |
| Потрібно змінити «налаштування в кінці», не чіпаючи інші | Значення за замовчуванням + іменування потрібного параметра | Не доводиться передавати «зайві» аргументи |
| Виклик погано читається без розшифровки | Іменовані аргументи | Виклик стає самодокументованим |
І так, іноді правильна відповідь — «функція перевантажена параметрами, і її треба спростити». Але це вже відчуття, яке прийде трохи пізніше, коли функцій стане більше.
Схема рішення: чи робити параметр із дефолтом
Щоб закріпити підхід, ось проста блок-схема, за якою зручно ухвалювати рішення у своїх проєктах:
flowchart TD
A[Є функція з параметром P] --> B{P обовʼязковий завжди?}
B -->|Так| C[Залишаємо без дефолту]
B -->|Ні| D{Є безпечне очікуване значення?}
D -->|Так| E[Робимо дефолт: P: Type = ...]
D -->|Ні| F[Залишаємо обовʼязковим, але викликаємо іменовано]
E --> G{Параметри одного типу й легко переплутати?}
F --> G
G -->|Так| H[Рекомендуємо іменовані аргументи]
G -->|Ні| I[Можна позиційно, якщо читається]
Слово «безпечне» тут ключове. Якщо значення за замовчуванням може призвести до дивної поведінки (наприклад, типове month = 0, а в нас місяці починаються з 1), то краще змусити себе явно передавати значення.
5. Типові помилки
Помилка № 1: значення за замовчуванням «на авось» — виглядає зручно, але ламає сенс.
Іноді хочеться поставити значення за замовчуванням, аби параметр став необовʼязковим. Наприклад, зробити day: Int = 0. Але якщо 0 — невалідний день, ви закладаєте «міну сповільненої дії»: програма ніби працюватиме, але дані поступово перетворяться на сміття. Гарне значення за замовчуванням — це не «будь-яке», а очікуване й безпечне.
Помилка № 2: обовʼязкові параметри стоять після дефолтних, і виклики перетворюються на загадки.
Такий дизайн змушує постійно використовувати іменовані аргументи навіть там, де цього не хочеться. Kotlin прямо підсвічує цю ситуацію: якщо параметр із дефолтом стоїть перед обовʼязковим, то опустити його можна лише через іменований виклик. У навчальних проєктах майже завжди простіше просто поміняти параметри місцями.
Помилка № 3: кілька однакових за типом аргументів передаються позиційно, і порядок плутається.
Три числа підряд або три булеві прапорці підряд виглядають однаково, і помилку в одному місці дуже складно помітити очима. У таких викликах іменовані аргументи — це не «балаканина», а страховка.
Помилка № 4: хаотичне змішування позиційних та іменованих аргументів.
Коли частину аргументів передають позиційно, частину — іменовано, а потім знову позиційно (або просто в дивному порядку), читання перетворюється на муку. Kotlin дозволяє іменовані аргументи в будь-якому порядку, але мета не в тому, щоб здивувати колегу, а в тому, щоб зробити код очевидним: спочатку основне, потім налаштування.
Помилка № 5: надто багато параметрів із дефолтами як спроба замінити нормальний дизайн.
Якщо у функції 8 параметрів, із них 6 мають значення за замовчуванням, і ви самі не памʼятаєте, що означає кожен, — це сигнал, що функція робить забагато. У межах цього курсу ми поки не заглиблюватимемося в просунуті конструкції, але щонайменше варто зупинитися й запитати себе: «А чи не можна розділити дію на дві простіші функції?»
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ