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("two", "three")
    printLines("one", *tail)
    // one
    // two
    // three
}

Обратите внимание на важную деталь: *tail можно использовать только в позиции аргумента при вызове функции. Это не «операция над массивом», а именно синтаксис вызова.

Плохой вызов (и почему он плохой)

Если попытаться сделать так (оставлю как комментарий, чтобы ваш проект компилировался):

fun main() {
    val tail = arrayOf("two", "three")

    printLines("one", 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("== MENU ==", "Play", "Settings", "Exit")
    // == MENU ==
    // Play
    // Settings
    // Exit
}

Если убрать *, получится попытка передать Array<String> туда, где ждут String, String, String... (vararg). А это тот же самый типовой конфликт, только «спрятанный» внутри вашей же утилиты.

5. Пример: статистика для игры «Угадай число»

Чтобы не было ощущения, что spread существует только для абстрактных примеров из учебника, давайте встроим его в понятный кусочек консольного приложения. Мы уже делали игру «Угадай число»: там есть раунды, попытки, итоговые сообщения. Логичный следующий шаг — научиться красиво печатать статистику по нескольким раундам, не копируя элементы вручную.

Представим, что мы сыграли несколько раундов и сохранили число попыток в IntArray. Теперь хотим распечатать их через функцию, которая принимает vararg Int.

Утилита: печать статистики

fun printAttempts(vararg attempts: Int) {
    print("Attempts: ")
    for (a in attempts) print("$a ")
    println()
}

fun main() {
    val attemptsByRound: IntArray = intArrayOf(4, 2, 6)

    printAttempts(*attemptsByRound) // распаковали IntArray
    // Attempts: 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?.

1
Задача
Kotlin SELF, 16 уровень, 2 лекция
Недоступна
Приветствие команды
Приветствие команды
1
Задача
Kotlin SELF, 16 уровень, 2 лекция
Недоступна
Средняя оценка
Средняя оценка
1
Задача
Kotlin SELF, 16 уровень, 2 лекция
Недоступна
Лог попыток
Лог попыток
1
Задача
Kotlin SELF, 16 уровень, 2 лекция
Недоступна
Меню с шапкой
Меню с шапкой
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ