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() просто чтобы понять “что происходит”. Это нормально. Более того: это один из самых честных способов отладки на раннем этапе (пока вы не дружите с debugger).
Но если 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‑функции обычно делают одну маленькую работу (печать, подсчёт, простая обработка набора), а не заменяют собой архитектуру программы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ