JavaRush /Курси /Kotlin SELF /Spread *: передаємо м...

Spread *: передаємо масиви в vararg без помилок

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

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 Що писати у виклику
Array<String>
vararg String
f(*arr)
IntArray
vararg Int
f(*ints)
IntArray
vararg Any?
f(*ints.toTypedArray())

Ця таблиця не «про всі випадки життя», але для поточного рівня — це приблизно 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?.

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