1. Навіщо потрібен vararg
Іноді в програмуванні трапляється дуже життєва ситуація: ви хочете написати функцію, яка виконує одну й ту саму дію для набору значень, але заздалегідь не знаєте, скільки саме значень буде. Сьогодні їх 2, завтра 5, післязавтра 0. Так, таке теж буває — і це не завжди помилка.
Можна, звісно, змусити код, що викликає, вручну зібрати масив. Можна передавати «порожні» заповнювачі на кшталт 0 або "" (а потім страждати). Можна писати різні функції: «на 2 аргументи», «на 3 аргументи»… Але це швидко перетворюється на цирк.
Для таких випадків у Kotlin є параметр зі змінною кількістю аргументів — vararg.
Що таке vararg
Коли ви бачите в сигнатурі функції слово vararg, це означає: «у цю функцію можна передати 0 або більше аргументів цього типу». Тобто діапазон кількості аргументів виглядає так: 0..N.
Формально це частина мови Kotlin: один параметр у сигнатурі позначають модифікатором vararg, і Kotlin дозволяє викликати функцію, перелічуючи будь-яку кількість значень. Усередині функції цей параметр поводиться як масив — до цього ми підійдемо трохи далі.
Почнімо з най«класичнішого» прикладу — суми.
2. Синтаксис і базові виклики vararg
Мінімальний приклад: підсумовуємо скільки завгодно чисел
Зараз буде код, який виглядає надто просто, — і це нормально. Наша мета — щоб ви побачили, який вигляд має vararg і як виглядає виклик.
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
println(sumAll(1, 2, 3)) // 6
println(sumAll(10, 20)) // 30
}
Зверніть увагу на дві речі. По-перше, vararg xs: Int стоїть на місці звичайного параметра. Тобто це все ще «параметр функції», просто особливий. По-друге, виклик sumAll(1, 2, 3) виглядає як звичайний виклик функції, у якому ви перелічили аргументи через кому.
Виклик без аргументів: vararg допускає f()
З vararg майже завжди трапляється маленький «момент істини»: ви раптово розумієте, що функцію цілком легально можна викликати взагалі без аргументів. Це не помилка компіляції — Kotlin не буде «сваритися». І тут ви, як автор функції, маєте замислитися: «А що моя функція повинна робити в такому разі?»
Продовжімо приклад суми: для суми «порожнього набору» логічно повернути 0.
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
println(sumAll()) // 0
println(sumAll(5)) // 5
}
У цьому прикладі sumAll() повертає 0 не тому, що Kotlin «сам так вирішив», а тому, що ми написали код, який за порожнього набору просто не зайде в цикл і поверне початкове значення sum.
Як vararg виглядає в сигнатурі й як це читати
На цьому етапі важливо навчитися «читати очима» сигнатуру. Коли ви бачите:
fun printLines(vararg lines: String) { ... }
це можна прочитати приблизно так: «printLines приймає будь-яку кількість рядків, а всередині ми обробимо їх по черзі».
У Kotlin зазвичай vararg ставлять ближче до кінця списку параметрів (часто взагалі останнім) — так виклики виглядають природніше.
Зробімо «прикладну» функцію друку:
fun printLines(vararg lines: String) {
for (line in lines) {
println(line)
}
}
fun main() {
printLines("one", "two", "three")
// one
// two
// three
}
І тут vararg справді починає відчуватися як зручність: замість того щоб вручну створювати масив рядків «заради одного друку», ви просто перелічуєте значення.
3. Як vararg працює всередині функції
Що відбувається «за лаштунками»: vararg перетворюється на масив
Зараз буде важлива думка, яка дуже допомагає не плутатися. Коли ви викликаєте sumAll(1, 2, 3), Kotlin усередині компілятора робить приблизно таке: «пакує» ці числа в масив і передає його у функцію.
На практиці це означає: усередині функції вам доступні звичні речі з теми масивів — цикл for (x in xs), розмір xs.size, звертання за індексом xs[i].
Для наочності можна уявити процес так:
flowchart TD
A["Виклик: sumAll(1, 2, 3)"] --> B["Компілятор упакував у набір"]
B --> C["Усередині функції xs поводиться як масив"]
C --> D["Цикл for (x in xs) рахує суму"]
Не потрібно запамʼятовувати, як саме це зберігається в памʼяті. Достатньо тримати в голові практичне правило: усередині функції vararg можна перебирати як масив.
vararg усередині функції: size, індекси й друк
Коли ви трохи освоїлися з for (x in xs), майже одразу хочеться зробити щось розумніше. Наприклад, вивести елементи з номерами або зрозуміти, скільки аргументів передали. І це нормальне бажання: ви починаєте думати як розробник, а не як людина, яка просто «копіює приклад».
Зробімо функцію, яка друкує числа та їхню кількість:
fun printNumbers(vararg xs: Int) {
println("Count = ${xs.size}")
for (i in xs.indices) {
println("xs[$i] = ${xs[i]}")
}
}
fun main() {
printNumbers(7, 8, 9)
// Count = 3
// xs[0] = 7
// xs[1] = 8
// xs[2] = 9
}
Тут ви бачите звʼязок із попередніми темами: indices у масивів та індексацію xs[i]. Це рівно той самий стиль, що був у темі про масиви, просто тепер масив «потрапляє» у функцію через vararg.
Обмеження: в одній функції може бути лише один vararg
У Kotlin є важливе правило: лише один параметр функції може бути позначений як vararg.
Це не «прискіпливість» мови, а спосіб уникнути неоднозначностей. Якби можна було написати fun f(vararg a: Int, vararg b: Int), то під час виклику f(1, 2, 3) було б незрозуміло, де закінчується a і де починається b.
Ось приклад, який не скомпілюється:
fun broken(vararg a: Int, vararg b: Int) { // так не можна
println(a.size + b.size)
}
Компілятор не дасть вам це зібрати — і тим самим убереже від коду, який неможливо нормально викликати.
4. Практика: vararg у «Вгадай число»
Навіщо vararg потрібен у проєкті
vararg — це не «прикольна фіча мови», а інструмент, який робить код зручнішим і чистішим. Особливо тоді, коли ви друкуєте багато повідомлень або збираєте дані в одному місці.
Уявімо, що у вашому мініпроєкті «Вгадай число» ви часто пишете такі фрагменти:
println("Welcome!")
println("I picked a number.")
println("Try to guess it.")
Це працює, але виглядає шумно: багато рядків виводу, і логіка гри тоне в «балаканині».
Зробімо маленьку утиліту: «виведи кілька рядків». І тут vararg підходить ідеально, адже кількість рядків щоразу різна.
fun say(vararg lines: String) {
for (line in lines) {
println(line)
}
}
Тепер у main або у вашому ігровому циклі можна писати компактніше й читабельніше.
Приклад: привітання гри через say(vararg ...)
Важливо: приклади коду будуть короткими, але логічно повʼязаними. Уявіть, що це фрагменти одного файлу GuessNumber.kt, який ви вже робили раніше.
fun say(vararg lines: String) {
for (line in lines) println(line)
}
fun main() {
say(
"Ласкаво просимо до гри!",
"Я загадав число.",
"Спробуйте вгадати :)"
)
}
Зверніть увагу: з погляду синтаксису це звичайний виклик функції. Просто аргументів багато. Kotlin не вимагає жодних дужок, масивів чи додаткових дій. Ви перелічуєте рядки, а функція отримує їх як «набір».
Приклад: повідомлення-підказки як vararg
У «Вгадай число» часто буває розгалуження: ми друкуємо різні підказки залежно від ситуації. Іноді потрібно вивести один рядок, іноді — два. Якщо друк розмазаний по if/else, код швидко стає схожим на новорічну гірлянду: наче світиться, але незрозуміло, де саме дріт.
Зробімо функцію, яка друкує підказки:
fun hint(vararg lines: String) {
for (line in lines) println(line)
}
fun main() {
val secret = 42
val guess = 50
if (guess > secret) {
hint("Занадто багато!", "Спробуйте менше.")
// Занадто багато!
// Спробуйте менше.
} else if (guess < secret) {
hint("Занадто мало!")
// Занадто мало!
} else {
hint("Вгадали!")
// Вгадали!
}
}
Тут ви, по суті, зробили «мікро-API» для повідомлень. І це приємна звичка: ви відокремлюєте «що робимо» (логіка гри) від «як розмовляємо» (повідомлення для користувача).
Приклад: debug(vararg ...) для налагоджувального друку
Коли ви навчаєтеся, ви часто вставляєте println(), просто щоб зрозуміти, «що відбувається». Це нормально. Ба більше: це один із найчесніших способів налагодження на ранньому етапі (поки ви ще не дружите з дебагером).
Але коли println() стає надто багато, ви знову отримуєте шум. Можна зробити маленький «логер» у найпростішому вигляді: функція приймає тег (рядок) і набір повідомлень.
Важливо: сьогодні ми робимо максимально простий варіант, у якому vararg стоїть останнім, щоб виклик виглядав природно.
fun debug(tag: String, vararg messages: String) {
for (m in messages) {
println("[$tag] $m")
}
}
fun main() {
debug("GAME", "Start", "Secret generated", "Waiting for input")
// [GAME] Start
// [GAME] Secret generated
// [GAME] Waiting for input
}
Так ви отримуєте зручний вивід, а vararg дозволяє передати скільки завгодно повідомлень за один виклик.
5. vararg чи масив: що вибрати
До vararg у вас уже був спосіб передати «багато значень»: зробити параметр типу масиву, наприклад IntArray. Це робочий підхід. Але vararg часто робить виклик простішим і приємнішим для читання.
Ось компактне порівняння:
| Підхід | Як виглядає виклик | Що незручно | Коли доречно |
|---|---|---|---|
|
|
потрібно вручну створити масив | коли дані вже й так живуть у масиві |
|
|
потрібно продумати випадок |
коли ви хочете «природний» виклик переліком |
Про ситуацію «дані вже в масиві, але функція — vararg» ми поговоримо окремо в лекції про spread-оператор, бо там є важливі синтаксичні нюанси. Сьогодні ми туди не забігаємо. Важливо просто зрозуміти ідею vararg і звикнути до його виклику.
6. Типові помилки під час роботи з vararg
Помилка №1: «Я думав, vararg — це чарівна колекція, а він як масив».
На старті легко очікувати, що у vararg зʼявляться якісь «суперметоди». Але на практиці простіше й правильніше думати так: усередині функції vararg поводиться як масив, а базові операції — це for, size та індексація.
Помилка №2: забули, що можливий виклик без аргументів, і випадково впали на xs[0].
Щойно ви пишете vararg, ви автоматично дозволяєте виклик f(). Якщо всередині ви одразу звертаєтеся до xs[0], то за порожнього набору отримаєте помилку часу виконання. Навіть якщо «у вашому сценарії так не буває», Kotlin усе одно дозволяє такий виклик. Тож ви маєте або обробити цей випадок, або явно його заборонити.
Помилка №3: спробували оголосити два vararg в одній функції.
Це класика: хочеться зробити «і список чисел, і список рядків», але Kotlin не дасть — vararg може бути лише один. Це обмеження мови, і його спеціально зроблено, щоб виклики залишалися однозначними.
Помилка №4: використовують vararg «про всяк випадок», хоча аргументів завжди рівно два.
vararg — потужний інструмент. Але якщо за змістом у функції завжди два значення (наприклад, ширина й висота), то vararg погіршує читабельність: виклик починає виглядати так, ніби значень може бути скільки завгодно, хоча це не так. На рівні API це вводить в оману. Тому, коли кількість фіксована, краще залишати звичайні параметри.
Помилка №5: перетворюють vararg на спосіб «сховати поганий дизайн».
Іноді трапляється підхід: «а давайте все передавати varargʼом і розберемося всередині». Це майже завжди погано: функція стає непередбачуваною, а помилки проявляються пізно. vararg добрий, коли набір однорідний і логічний (повідомлення, числа, рядки одного сенсу), а не коли це мішок усього підряд.
Помилка №6: роблять занадто багато друку через say(vararg lines) і втрачають структуру програми.
say(vararg lines: String) — корисна штука. Але якщо почати друкувати через неї «все на світі», легко втратити розуміння, де у вас логіка, а де інтерфейс. Гарний орієнтир: vararg-функції зазвичай роблять одну маленьку роботу (друк, підрахунок, просту обробку набору), а не підміняють собою архітектуру програми.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ