JavaRush /Курсы /Kotlin SELF /Правила сигнатуры vararg: порядок, named args и default v...

Правила сигнатуры vararg: порядок, named args и default values

Kotlin SELF
16 уровень , 1 лекция
Открыта

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.

Сигнатура Как читается Как обычно вызывается Где меньше сюрпризов
fun f(a: A, vararg xs: X)
«сначала обязательное, потом набор»
f(a, x1, x2, x3)
Обычно здесь
fun f(vararg xs: X, opt: O = ...)
«набор, затем настройки»
f(x1, x2, opt = ...)
Только если очень нужно

Смысл такой: 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 на функцию — это строгое ограничение, и это нормально.

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