JavaRush /Курси /Kotlin SELF /Правила сигнатури vararg: порядок, іменовані аргументи й ...

Правила сигнатури vararg: порядок, іменовані аргументи й значення за замовчуванням

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

1. vararg як частина дизайну сигнатури

Коли ви вперше бачите vararg, здається, ніби це просто «параметр, який можна передати багато разів». Але на рівні API це сильне рішення. Воно змінює стиль виклику функції й змушує компілятор визначати, де закінчуються «численні аргументи», а де починаються «налаштування».

Тому важливо проєктувати сигнатуру так, щоб її було легко і читати, і викликати — особливо новачкам, які ще не вміють «вгадувати наміри» автора функції.

Технічно Kotlin говорить дві ключові речі: vararg усередині функції сприймається як масив, а vararg зазвичай ставлять останнім. Якщо він стоїть не наприкінці, то все, що йде після нього, під час виклику доведеться задавати іменовано.

vararg усередині функції: мислимо як про масив

Щоб упевнено читати сигнатури, важливо «приземлити магію». Усередині тіла функції vararg — це звичайний масив елементів відповідного типу. Тобто ви можете взяти size, звернутися за індексом xs[0] (якщо масив не порожній), пройтися циклом for (x in xs) тощо.

Зробімо невеличкий «мікроскоп» — функцію, яка показує, що ми насправді отримали:

fun debugInts(vararg xs: Int) {
    println("кількість = ${xs.size}")     // кількість = ...
    if (xs.size > 0) {
        println("перший = ${xs[0]}")   // перший = ...
    }
}

fun main() {
    debugInts(10, 20, 30)             // кількість = 3, перший = 10
}

Тут немає жодної «таємної» колекції чи особливого контейнера: xs — це саме те, що зручно перебирати як масив.

Чому vararg зазвичай ставлять останнім

Є просте правило з «дизайну читабельного коду»: коли людина бачить виклик f(…, …, …), вона з першого погляду має розуміти, що означає «хвіст» аргументів.

vararg логічно майже завжди є таким хвостом: «передайте список повідомлень», «передайте набір чисел», «передайте рядки для друку». Тому найчастіше vararg ставлять наприкінці сигнатури. Так виклик виглядає природно, і компілятору не доводиться вгадувати, де закінчується набір.

У Kotlin це ще й практично: якщо vararg стоїть наприкінці, ви можете передавати значення просто переліком — і жодних додаткових вимог до стилю виклику не зʼявляється.

Приклад «добре читається»: спочатку один обовʼязковий параметр, потім vararg.

fun log(tag: String, vararg messages: String) {
    for (m in messages) {
        println("[$tag] $m")
    }
}

fun main() {
    log("AUTH", "старт", "перевірка пароля", "ок")
    // [AUTH] старт
    // [AUTH] перевірка пароля
    // [AUTH] ок
}

Обмеження: лише один vararg у функції

Сигнатура має бути однозначною. Якби Kotlin дозволив два vararg, то виклик f(1, 2, 3) був би загадкою: що належить до першого vararg, а що — до другого?

Тому в Kotlin дозволено рівно один vararg на функцію. Це пряме мовне правило.

Якщо вам раптом захотілося написати так:

// Так не можна: два vararg в одній функції
// fun broken(vararg a: Int, vararg b: Int) { }

Тоді компілятор справедливо скаже «ні».

2. vararg не наприкінці: іменовані аргументи й значення за замовчуванням

Чому після vararg майже завжди потрібні іменовані аргументи

Іноді хочеться зробити сигнатуру на кшталт «спочатку набір, потім налаштування». Наприклад: «роздрукуйте елементи, але з таким-то роздільником». І тут виникає спокуса написати fun printAll(vararg items: String, separator: String).

Це допустимо, але Kotlin попереджає нас: якщо vararg не останній, то значення для параметрів після нього потрібно передавати іменовано. Інакше компілятор не зможе зрозуміти, де закінчуються items, а де починається separator.

Зробімо приклад на маленькій утиліті, яку ми використовуватимемо в нашому навчальному консольному застосунку (умовно назвімо його GuessGame, «Вгадай число»). Нам часто потрібно друкувати список рядків в один рядок.

fun printAll(vararg items: String, separator: String) {
    var first = true
    var out = ""

    for (item in items) {
        if (!first) out += separator
        out += item
        first = false
    }
    println(out)
}

fun main() {
    printAll("a", "b", "c", separator = " | ") // a | b | c
}

Зверніть увагу: separator ми передали лише як separator = " | ". Якщо спробувати зробити printAll("a", "b", "c", " | "), компілятор не погодиться, бо це виглядає як «ще один елемент vararg».

vararg і значення за замовчуванням

Параметр після vararg часто є «опцією», і для неї зазвичай існує розумне значення за замовчуванням. Тоді сигнатура стає дружнішою: у простих випадках ви можете не вказувати опцію, а вказувати її лише тоді, коли потрібно.

Однак через те, що параметр іде після vararg, у реальних викликах ви майже завжди використовуватимете іменовані аргументи — навіть якщо параметр має значення за замовчуванням. Це не «обовʼязковість заради бюрократії», а спосіб зробити виклик читабельним. Людині простіше побачити separator = " | ", ніж гадати, що означає останній рядок.

Зробімо нашу утиліту printAll трохи зручнішою:

fun printAll(vararg items: String, separator: String = ", ") {
    var first = true
    var out = ""

    for (item in items) {
        if (!first) out += separator
        out += item
        first = false
    }
    println(out)
}

fun main() {
    printAll("kotlin", "java", "swift")                 // kotlin, java, swift
    printAll("kotlin", "java", "swift", separator = " / ")
    // kotlin / java / swift
}

Тут важливий момент: параметр separator зі значенням за замовчуванням робить сигнатуру кращою, бо типовий виклик лишається коротким. Але щойно ми хочемо змінити separator, ми все одно явно пишемо імʼя параметра. Так код читабельніший, і ми не сперечаємося з компілятором.

Правило: «пропустили один — називайте далі»

Переходимо до місця, де початківці найчастіше дивуються: «Чому я не можу просто пропустити один аргумент посередині, а решту передати позиційно?» Бо позиційний виклик тримається на порядку. А якщо ви зробили «дірку», компілятору потрібно зрозуміти, що саме ви пропустили.

У Kotlin є правило: ви можете пропускати аргументи зі значеннями за замовчуванням, але після першого пропущеного усі наступні аргументи мають бути іменованими.

Покажемо це на маленькому прикладі, схожому на реальне «налаштування виводу» в нашому консольному застосунку:

fun showAttempt(
    attempt: Int,
    guess: Int,
    hint: String = "без підказки",
    prefix: String = "Спроба"
) {
    println("$prefix #$attempt: здогад=$guess, підказка=$hint")
}

fun main() {
    showAttempt(1, 50) // Спроба #1: здогад=50, підказка=без підказки
    showAttempt(2, 75, prefix = "Спроб.") // Спроб. #2: здогад=75, підказка=без підказки
}

Зауважте: у другому виклику ми хочемо змінити prefix, але не хочемо змінювати hint. Ми «пропускаємо» hint, отже prefix зобовʼязані передавати іменовано (prefix = "Спроб."). Інакше це було б незрозуміло.

3. Шпаргалка щодо порядку параметрів і приклад

Порядок параметрів: мінішпаргалка

Тепер зберемо все в одну картинку — не як «правила заради правил», а як підхід, що робить виклик функції передбачуваним.

Нижче — таблиця з двома типовими формами сигнатури. Вона не замінює розуміння, але добре допомагає, коли ви проєктуєте API й обираєте, де поставити vararg.

Сигнатура Як читається Як зазвичай викликається Де менше сюрпризів
fun f(a: A, vararg xs: X)
«спочатку обовʼязкове, потім набір»
f(a, x1, x2, x3)
Зазвичай тут
fun f(vararg xs: X, opt: O = ...)
«набір, потім налаштування»
f(x1, x2, opt = ...)
Лише якщо справді потрібно

Сенс такий: vararg наприкінці — це майже завжди най«дружніший» дизайн. Якщо ви ставите vararg не наприкінці, будьте готові до того, що користувачі вашої функції (зокрема й ви самі за тиждень) писатимуть іменовані аргументи після vararg.

Приклад із навчального застосунку: функція логування

Щоб не залишати тему «у вакуумі», додамо в наш консольний застосунок маленький логер. Ми не робимо нічого «промислового» — нам потрібна лише одна річ: друкувати кілька повідомлень з однаковим тегом. Це ідеально лягає на vararg.

Сигнатуру оберемо максимально дружню: спочатку тег, потім vararg. Так виклик читається природно.

fun log(tag: String, vararg messages: String) {
    for (m in messages) {
        println("[$tag] $m")
    }
}

fun main() {
    log("GAME", "старт", "секрет згенеровано", "очікуємо введення")
}

Якщо згодом нам захочеться додати «налаштування» (наприклад, вмикати або вимикати лог), ми, найімовірніше, додамо звичайний параметр до vararg або доберемо значення за замовчуванням так, щоб виклик залишався читабельним. На цьому етапі курсу головне — відчути: порядок параметрів є частиною дизайну вашого коду.

4. Типові помилки

Помилка № 1: параметр після vararg намагаються передати позиційно.
Зазвичай це виглядає так: ви написали fun f(vararg xs: String, separator: String) і потім інтуїтивно викликаєте f("a", "b", " | "). Але компілятор не зобовʼязаний вгадувати, що останній рядок — це «налаштування», а не черговий елемент набору. У Kotlin, якщо vararg не останній, параметри після нього потрібно передавати іменовано.

Помилка № 2: роблять vararg не останнім «просто так», а потім дивуються, що всі виклики стали громіздкими.
Іноді здається, що «гарно» поставити налаштування наприкінці, але ціна — постійні іменовані аргументи й «шумніший» код. Якщо параметр після vararg не додає справді важливої читабельності (або не трапляється в кожному другому виклику), краще перебудувати сигнатуру так, щоб vararg був останнім.

Помилка № 3: забувають, що vararg може бути порожнім, і звертаються до xs[0] без перевірки.
Для vararg легальний виклик f(), і це не помилка компіляції. Тому будь-який код, який звертається до xs[0], має заздалегідь визначити контракт: або акуратно перевірити xs.size > 0, або заборонити порожній набір через require. Інакше ви отримаєте помилку вже під час виконання.

Помилка № 4: пропускають параметр зі значенням за замовчуванням, але продовжують передавати наступні позиційно.
Це часта «логічна помилка читання»: хочеться написати щось на кшталт showAttempt(2, 75, "Спроб."), маючи на увазі prefix, але позиційно третім аргументом іде hint. Kotlin спеціально підштовхує вас до явності: якщо ви пропустили один параметр зі значенням за замовчуванням, далі використовуйте іменовані аргументи.

Помилка № 5: намагаються додати другий vararg «раптом знадобиться».
Навіть якщо ви могли б вигадати, «як це має працювати», компілятор не зможе однозначно розкласти аргументи за двома змінними наборами. Тому в Kotlin один vararg на функцію — це суворе обмеження, і це нормально.

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