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 | Что писать в вызове |
|---|---|---|
|
|
|
|
|
|
|
|
|
Эта таблица не «про все случаи жизни», но для уровня текущего дня — это 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?.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ