1. Навіщо потрібен spread *
Коли ви оголошуєте параметр vararg, виклик функції виглядає приємно: sumAll(1, 2, 3). Але на практиці часто трапляються ситуації, коли числа (або рядки) уже лежать у масиві. Наприклад, ви зібрали «хвіст» параметрів, прочитали їх із вводу або зберегли результати кількох раундів гри. І тут зʼявляється класичне запитання новачка: «чому не можна просто передати масив?»
Зафіксуймо словами, що саме відбувається. vararg — це форма виклику, коли ви передаєте 0..N окремих аргументів. А масив — це один обʼєкт (одна змінна), усередині якого лежать N елементів. Тож компілятор цілком коректно сприймає масив як один аргумент, а не як «контейнер, який треба розгорнути».
Наочна схема (спрощено):
flowchart LR
A["vararg очікує: a, b, c (кілька аргументів)"] --> B["виклик f(1, 2, 3)"]
C["у вас є: arr = [1, 2, 3] (одна змінна)"] --> D["виклик f(arr) — це один аргумент"]
E["spread: *arr"] --> F["виклик f(*arr) — це f(1, 2, 3)"]
У Kotlin для розвʼязання цієї проблеми й існує spread-оператор *: він каже компілятору: «візьми елементи масиву й передай їх як окремі аргументи».
2. Spread для Array<T>
Почнімо з найпростішого випадку: звичайний Array<String> або Array<Int>, тобто «обʼєктний» масив. Його створюють через arrayOf(...). Саме з таким масивом spread читається максимально природно: f(*arr).
Мінімальний приклад: друк рядків
fun printLines(vararg lines: String) {
for (line in lines) {
println(line)
}
}
fun main() {
val tail: Array<String> = arrayOf("два", "три")
printLines("один", *tail)
// один
// два
// три
}
Зверніть увагу на важливу деталь: *tail можна використовувати лише в позиції аргументу під час виклику функції. Це не «операція над масивом», а саме синтаксис виклику.
Поганий виклик (і чому він поганий)
Якщо спробувати зробити так (залишимо це як коментар, щоб ваш проєкт компілювався):
fun main() {
val tail = arrayOf("два", "три")
printLines("один", tail)
// Помилка компіляції: tail — це один аргумент типу Array<String>,
// а функція очікує String, String, String... (vararg)
}
Компілятор не «здогадається», що ви хотіли «вставити елементи масиву всередину списку аргументів». Тут треба сказати явно: *tail.
Змішаний виклик: частина аргументів вручну, частина — з масиву
Після того як ви побачили f(*arr), майже одразу виникає наступний практичний сценарій: «а можна я кілька значень задам вручну, а решту візьму з масиву?» Так: до і після розпаковування можна передавати звичайні аргументи.
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
val mid: Array<Int> = arrayOf(2, 3, 4)
println(sumAll(1, *mid, 10)) // 20
}
Тут важливо вловити думку: *mid не замінює весь список аргументів, а «вклеюється» всередину нього.
Якщо вам здається, що це схоже на «вставити пачку аркушів у середину зошита», — так, аналогія майже чесна. Щоправда, зошит не свариться через типи, а Kotlin свариться — і це навіть корисно.
3. Примітивні масиви та перетворення
Тепер переходимо до місця, де новачки починають підозрювати, що Kotlin — живий. Річ у тім, що в Kotlin є дві «родини» масивів: Array<T> (для обʼєктів) і окремі типи для примітивів: IntArray, DoubleArray, CharArray тощо. Їх зроблено для того, щоб числа зберігалися ефективно — без «пакування в обʼєкти».
Практичний наслідок: не будь-який масив можна «розпакувати» в будь-який vararg. Потрібно, щоб тип масиву збігався з очікуваним типом vararg-параметра.
vararg Int і IntArray: хороший сценарій
Якщо вашу функцію оголошено як fun sumAll(vararg xs: Int), то всередині xs — це, по суті, IntArray. І передавати IntArray у такий vararg можна через spread:
fun sumAll(vararg xs: Int): Int {
var sum = 0
for (x in xs) sum += x
return sum
}
fun main() {
val attempts: IntArray = intArrayOf(3, 5, 2)
println(sumAll(*attempts)) // 10
}
Це відчувається «логічно»: IntArray містить Int, vararg xs: Int приймає Int.
IntArray — не Array<Int> (й інколи це важливо)
А от Array<Int> — це інший тип. Його створюють через arrayOf(1, 2, 3). Між ними не можна просто поставити знак рівності, бо це справді різні структури.
Практичний висновок: інколи вам доведеться робити перетворення, щоб «перейти» з однієї родини масивів в іншу.
Коли потрібен toTypedArray()
Найчастіше «не компілюється» трапляється в такій ситуації: у вас є IntArray, але функція приймає vararg не Int, а, наприклад, Any? (так часто роблять для «універсального логера») або якийсь узагальнений тип. У такому разі IntArray уже не підходить, бо це не Array<...>.
Приклад: логер приймає Any?, а в нас IntArray
fun log(tag: String, vararg items: Any?) {
print("[$tag] ")
for (item in items) {
print("$item ")
}
println()
}
fun main() {
val attempts: IntArray = intArrayOf(3, 5, 2)
val boxed: Array<Int> = attempts.toTypedArray()
log("GAME", *boxed)
// [GAME] 3 5 2
}
Тут два кроки, і обидва важливі:
1) attempts.toTypedArray() перетворює IntArray на Array<Int> (тобто на «обʼєктний» масив).
2) *boxed розпаковує Array<Int> у vararg items.
Чому не можна просто написати log("GAME", *attempts)?
Тому що attempts — це IntArray, а vararg items: Any? усередині функції — це Array<out Any?>. Ці типи мають різну природу. Kotlin не може автоматично «зробити вигляд», що IntArray — це масив обʼєктів: там інший формат зберігання.
Якщо говорити по-людськи: IntArray — це «коробка з цифрами», а Array<Int> — «коробка з маленькими обʼєктами-числами». Spread уміє «витрушувати» коробку потрібного типу, але сама коробка має підходити за формою.
Памʼятка: що в що розпаковуємо
| Що у вас у змінній | Що очікує vararg | Що писати у виклику |
|---|---|---|
|
|
|
|
|
|
|
|
|
Ця таблиця не «про всі випадки життя», але для поточного рівня — це приблизно 90% практики.
4. Передавання vararg в іншу vararg-функцію
Частий сценарій виникає, коли ви пишете утиліти. Ви створили одну зручну функцію з vararg, а потім хочете додати іншу, яка підставляє «шапку», «префікс», «підпис» — і всередині викликає першу. Тут легко забути, що всередині другої функції vararg уже перетворився на масив. А отже, передавати його далі потрібно через *.
Це безпосередньо випливає з визначення: усередині функції параметр vararg використовується як масив.
Приклад: друк із заголовком
fun printLines(vararg lines: String) {
for (line in lines) println(line)
}
fun printWithHeader(header: String, vararg lines: String) {
println(header)
printLines(*lines) // важливо: *lines
}
fun main() {
printWithHeader("== МЕНЮ ==", "Грати", "Налаштування", "Вихід")
// == МЕНЮ ==
// Грати
// Налаштування
// Вихід
}
Якщо прибрати *, вийде спроба передати Array<String> туди, де очікують String, String, String... (vararg). Це той самий типовий конфлікт, просто «схований» усередині вашої утиліти.
5. Приклад: статистика для гри «Вгадай число»
Щоб не складалося враження, що spread існує лише для абстрактних прикладів із підручника, давайте вбудуємо його в зрозумілий шматочок консольного застосунку. Ми вже робили гру «Вгадай число»: там є раунди, спроби, підсумкові повідомлення. Логічний наступний крок — навчитися гарно друкувати статистику за кількома раундами, не копіюючи елементи вручну.
Уявімо, що ми зіграли кілька раундів і зберегли кількість спроб у IntArray. Тепер хочемо роздрукувати їх через функцію, яка приймає vararg Int.
Утиліта: друк статистики
fun printAttempts(vararg attempts: Int) {
print("Спроби: ")
for (a in attempts) print("$a ")
println()
}
fun main() {
val attemptsByRound: IntArray = intArrayOf(4, 2, 6)
printAttempts(*attemptsByRound) // розпакували IntArray
// Спроби: 4 2 6
}
Важливий момент: ви не змінювали printAttempts, щоб вона «знала про масиви». Вона лишається зручною для викликів виду printAttempts(3, 5, 1), але водночас ви можете «підсунути» їй масив через *.
Той самий вивід, але універсальним логером
Якщо ви хочете зробити «одну функцію логування на все», вона часто приймає Any?, щоб туди можна було передати і рядки, і числа, і null.
fun log(tag: String, vararg items: Any?) {
print("[$tag] ")
for (item in items) print("$item ")
println()
}
fun main() {
val attemptsByRound: IntArray = intArrayOf(4, 2, 6)
log("GAME", *attemptsByRound.toTypedArray())
// [GAME] 4 2 6
}
Звʼязка toTypedArray() + * — це як «перепакувати речі у валізу потрібного формату, а потім відкрити блискавку». Вона виглядає трохи довшою, зате прозора за змістом.
6. Типові помилки під час роботи зі spread * і vararg
Помилка №1: передати масив у vararg без * і дивуватися, «чому Kotlin вередує».
Зазвичай це виглядає так: printLines(linesArray) замість printLines(*linesArray). Компілятор тут має рацію: ви передали один аргумент типу Array<String>, а функція очікує багато аргументів типу String. Звичка «якщо потрібен vararg — згадуємо про *» економить купу часу.
Помилка №2: намагатися використовувати * не під час виклику функції.
Spread-оператор — це не «операція над масивом», не «особливий цикл» і не «магія розпаковування взагалі всюди». Він працює в конкретній позиції: перед іменем масиву в списку аргументів функції. Якщо спробувати застосувати * в іншому контексті, ви отримаєте помилку синтаксису — і це нормально.
Помилка №3: переплутати Array<Int> і IntArray та застрягти в перетвореннях.
На практиці це проявляється так: у вас IntArray, а ви намагаєтеся передати його туди, де очікують Array<...> (наприклад, vararg Any?), або навпаки. Корисно памʼятати, що примітивні масиви — окрема «лінійка типів» і не успадковуються від Array, хоча зовні схожі. Якщо типи не збіглися, ви або обираєте правильний вид масиву під час створення, або робите явне перетворення.
Помилка №4: забути * під час передавання vararg в іншу vararg-функцію.
Дуже частий баг в утилітах: ви пишете fun a(vararg xs: String), потім fun b(vararg xs: String) { a(xs) } — і ловите помилку типів. Причина проста: усередині b параметр xs уже є масивом, і під час передавання «як набору аргументів» його потрібно розпакувати: a(*xs). Це прямий наслідок того, що vararg усередині функції використовується як масив.
Помилка №5: використовувати toTypedArray() «на автоматі» й не розуміти, навіщо він.
toTypedArray() — корисний інструмент, але він потрібен не завжди. Якщо ваша функція приймає vararg Int, і у вас уже є IntArray, найчастіше достатньо f(*ints). Перетворення стає потрібним, коли ви переходите між «примітивним» масивом і «обʼєктним» масивом, або коли vararg приймає тип на кшталт Any?.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ