JavaRush /Курси /Kotlin SELF /Значення за замовчуванням та іменовані аргументи

Значення за замовчуванням та іменовані аргументи

Kotlin SELF
Рівень 8 , Лекція 3
Відкрита

1. Значення за замовчуванням

Коли ви лише починаєте, може здаватися, що проблема «переплутав параметри місцями» — це щось зі світу великих проєктів. Та на практиці вона виникає дуже рано: щойно зʼявляється функція з 35 параметрами одного типу (наприклад, кілька 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.

Змішуємо позиційні та іменовані аргументи

На практиці часто виходить «гібридний стиль»: перші 12 параметри дуже зрозумілі за змістом (зазвичай 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 мають значення за замовчуванням, і ви самі не памʼятаєте, що означає кожен, — це сигнал, що функція робить забагато. У межах цього курсу ми поки не заглиблюватимемося в просунуті конструкції, але щонайменше варто зупинитися й запитати себе: «А чи не можна розділити дію на дві простіші функції?»

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ