1. vararg как часть дизайна сигнатуры
Когда вы впервые видите vararg, кажется, что это просто «параметр, который может быть много раз». Но на уровне API это сильное решение: оно меняет стиль вызова функции и заставляет компилятор решать, где заканчиваются «много аргументов», а где начинаются «настройки».
Поэтому важно проектировать сигнатуру так, чтобы её было легко и читать, и вызывать — особенно новичку, который ещё не умеет «угадывать намерения» автора функции.
Технически Kotlin говорит две ключевые вещи: vararg внутри функции воспринимается как массив, а vararg обычно ставят последним; если он стоит не последним, то всё, что идёт после него, в вызове придётся задавать именованно.
vararg внутри функции: думаем как про массив
Чтобы уверенно читать сигнатуры, важно «приземлить магию»: внутри тела функции vararg — это обычный массив элементов соответствующего типа. То есть вы можете взять size, обратиться по индексу xs[0] (если не пусто), пройтись циклом for (x in xs) и так далее.
Сделаем маленький «микроскоп» — функцию, которая показывает, что мы реально получили:
fun debugInts(vararg xs: Int) {
println("count = ${xs.size}") // count = ...
if (xs.size > 0) {
println("first = ${xs[0]}") // first = ...
}
}
fun main() {
debugInts(10, 20, 30) // count = 3, first = 10
}
Здесь нет никакой тайной коллекции или особого контейнера: xs — это именно то, что удобно перебирать как массив.
Почему vararg обычно ставят последним
Есть простое правило из «дизайна читаемого кода»: когда человек видит вызов f(…, …, …), он должен с первого взгляда понимать, что означает «хвост» аргументов.
vararg логически почти всегда является таким хвостом: «передай список сообщений», «передай набор чисел», «передай строки для печати». Поэтому чаще всего vararg ставят в конец сигнатуры — так вызов выглядит естественно и компилятору не приходится гадать, где заканчивается набор.
В Kotlin это ещё и практично: если vararg последний, вы можете передавать значения просто перечислением, и больше никаких специальных требований к стилю вызова не появляется.
Пример «хорошо читается»: сначала один обязательный параметр, потом vararg.
fun log(tag: String, vararg messages: String) {
for (m in messages) {
println("[$tag] $m")
}
}
fun main() {
log("AUTH", "start", "check password", "ok")
// [AUTH] start
// [AUTH] check password
// [AUTH] ok
}
Ограничение: только один vararg в функции
Сигнатура должна быть однозначной. Если бы Kotlin разрешил два vararg, то вызов f(1, 2, 3) был бы загадкой: что относится к первому vararg, а что — ко второму?
Поэтому в Kotlin разрешён ровно один vararg на функцию. Это прямое языковое правило.
Если вам вдруг захотелось написать так:
// Так нельзя: два vararg в одной функции
// fun broken(vararg a: Int, vararg b: Int) { }
То компилятор справедливо скажет «нет».
2. vararg не в конце: named args и default values
Почему после vararg почти всегда нужны именованные аргументы
Иногда хочется сделать сигнатуру вида «сначала набор, потом настройки». Например: «распечатай элементы, но с таким-то разделителем». И тут появляется соблазн написать fun printAll(vararg items: String, separator: String).
Это допустимо, но Kotlin предупреждает нас: если vararg не последний, то значения для параметров после него нужно передавать именованно, иначе компилятор не сможет понять, где заканчиваются items, а где начинается separator.
Сделаем пример на маленькой утилите, которую мы будем использовать в нашем учебном консольном приложении (условно назовём его GuessGame, «Угадай число»). Нам часто хочется печатать список строк одной строкой.
fun printAll(vararg items: String, separator: String) {
var first = true
var out = ""
for (item in items) {
if (!first) out += separator
out += item
first = false
}
println(out)
}
fun main() {
printAll("a", "b", "c", separator = " | ") // a | b | c
}
Обратите внимание: separator мы передали только как separator = " | ". Если попытаться сделать printAll("a", "b", "c", " | "), компилятор не согласится, потому что это выглядит как «ещё один элемент vararg».
vararg и значения по умолчанию
Параметр после vararg часто является «опцией», и у неё обычно есть разумное значение по умолчанию. Тогда сигнатура становится дружелюбнее: вы можете не указывать опцию в простых случаях, а указывать — только когда надо.
Однако из-за того, что параметр идёт после vararg, в реальных вызовах вы почти всегда будете использовать именованные аргументы, даже если у параметра есть default value. Это не «обязаловка ради бюрократии», а способ сделать вызов читаемым: человеку проще увидеть separator = " | ", чем гадать, что означает последняя строка.
Сделаем нашу утилиту printAll чуть удобнее:
fun printAll(vararg items: String, separator: String = ", ") {
var first = true
var out = ""
for (item in items) {
if (!first) out += separator
out += item
first = false
}
println(out)
}
fun main() {
printAll("kotlin", "java", "swift") // kotlin, java, swift
printAll("kotlin", "java", "swift", separator = " / ")
// kotlin / java / swift
}
Здесь важный момент: параметр separator с дефолтом делает сигнатуру лучше, потому что типичный вызов остаётся коротким. Но как только мы хотим изменить separator, мы всё равно явно пишем имя параметра — так код читабельнее, и мы не спорим с компилятором.
Правило: «пропустил один — называй дальше»
Переходим к месту, где начинающие чаще всего удивляются: «Почему я не могу просто пропустить один аргумент посередине, но остальные передать позиционно?» Потому что позиционный вызов держится на порядке, а если вы «дырку» сделали, компилятору нужно понять, что именно вы пропустили.
В Kotlin есть правило: вы можете пропускать аргументы со значениями по умолчанию, но после первого пропущенного все последующие аргументы должны быть именованными.
Покажем на маленьком примере, который похож на реальную «настройку вывода» в нашем консольном приложении:
fun showAttempt(
attempt: Int,
guess: Int,
hint: String = "no hint",
prefix: String = "Attempt"
) {
println("$prefix #$attempt: guess=$guess, hint=$hint")
}
fun main() {
showAttempt(1, 50) // Attempt #1: guess=50, hint=no hint
showAttempt(2, 75, prefix = "Try") // Try #2: guess=75, hint=no hint
}
Заметьте: во втором вызове мы хотим поменять prefix, но не хотим менять hint. Мы «пропускаем» hint, значит prefix обязаны передавать именованно (prefix = "Try"), иначе это было бы неясно.
3. Шпаргалка по порядку параметров и пример
Порядок параметров: мини-шпаргалка
Сейчас соберём всё в одну картинку — не как «правила ради правил», а как способы сделать вызов функции предсказуемым.
Ниже таблица из двух типовых форм сигнатуры. Она не заменяет понимание, но хорошо помогает, когда вы проектируете API и выбираете, где поставить vararg.
| Сигнатура | Как читается | Как обычно вызывается | Где меньше сюрпризов |
|---|---|---|---|
|
«сначала обязательное, потом набор» | |
Обычно здесь |
|
«набор, затем настройки» | |
Только если очень нужно |
Смысл такой: vararg в конце — это почти всегда самый «дружелюбный» дизайн. Если вы ставите vararg не в конец, вы должны быть готовы к тому, что пользователи вашей функции (включая вас же через неделю) будут писать named arguments после vararg.
Пример из учебного приложения: функция логирования
Чтобы не оставлять тему «в вакууме», добавим в наше консольное приложение маленький логгер. Мы не делаем ничего «промышленного», нам нужна всего одна вещь: печатать несколько сообщений с одинаковым тегом. Это идеально ложится на vararg.
Сигнатуру выберем максимально дружелюбную: сначала тег, потом vararg. Так вызов читается естественно.
fun log(tag: String, vararg messages: String) {
for (m in messages) {
println("[$tag] $m")
}
}
fun main() {
log("GAME", "start", "secret generated", "waiting for input")
}
Если позже нам захочется добавить «настройку» (например, включать/выключать лог), мы, скорее всего, добавим обычный параметр до vararg или сделаем дефолт так, чтобы вызов оставался читабельным. На этом этапе курса главное — почувствовать: порядок параметров — это часть дизайна вашего кода.
4. Типичные ошибки
Ошибка №1: параметр после vararg пытаются передать позиционно.
Обычно это выглядит так: вы написали fun f(vararg xs: String, separator: String) и затем интуитивно вызываете f("a", "b", " | "). Но компилятор не обязан угадывать, что последняя строка — это «настройка», а не очередной элемент набора. В Kotlin, если vararg не последний, параметры после него нужно передавать именованно.
Ошибка №2: делают vararg не последним «просто так», а потом удивляются, что все вызовы стали громоздкими.
Иногда кажется, что «красиво» поставить настройки в конец, но цена — постоянные named arguments и более шумный код. Если параметр после vararg не даёт реально важной читаемости (или не встречается в каждом втором вызове), лучше перестроить сигнатуру так, чтобы vararg был последним.
Ошибка №3: забывают, что vararg может быть пустым, и делают доступ xs[0] без проверки.
С vararg легален вызов f(), и это не ошибка компиляции. Поэтому любой код, который обращается к xs[0], обязан заранее решить контракт: либо аккуратно проверить xs.size > 0, либо запретить пустой набор через require. В противном случае вы получите ошибку уже во время выполнения.
Ошибка №4: пропускают параметр с default value, но продолжают передавать следующие позиционно.
Это частая «логическая ошибка чтения»: хочется написать что-то вроде showAttempt(2, 75, "Try"), имея в виду prefix, но позиционно третьим аргументом идёт hint. Kotlin специально подталкивает вас к явности: если вы пропустили один default-параметр, дальше используйте named arguments.
Ошибка №5: пытаются добавить второй vararg «ну вдруг пригодится».
Даже если вы могли бы придумать «как это должно работать», компилятор не сможет однозначно разложить аргументы по двум переменным наборам. Поэтому в Kotlin один vararg на функцию — это строгое ограничение, и это нормально.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ