JavaRush /Курсы /Kotlin SELF /Дизайн мини‑утилит на vararg

Дизайн мини‑утилит на vararg

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

1. Мини‑библиотека вывода

Когда вы пишете учебные консольные программы, очень быстро возникает странный эффект: 20% кода решают задачу, а остальные 80% — это “вежливо поговорить с пользователем”. Напечатать заголовок, вывести список, добавить отступы, показать ошибку, красиво оформить результат. И если всё это делать в main, он превращается в сериал на 12 сезонов, который никто не просил.

Мини‑утилиты — это маленькие функции, которые вы пишете “для себя”, чтобы ваш код читался как текст. В идеале main должен быть похож на сценарий: “прочитал → проверил → посчитал → красиво вывел”. А детали вроде “как именно красиво вывести” прячутся в утилитах.

vararg здесь — почти идеальный инструмент, потому что печать и форматирование часто работают с набором строк: заголовок + несколько строк + подвал; или сообщение об ошибке из нескольких частей; или список пунктов, который хочется передавать естественно: printBlock("a", "b", "c").

printHeader(...): простая печать заголовка

Сейчас мы начнём собирать мини‑набор функций для консольного вывода. Это не “фреймворк” и не “архитектура”, а просто аккуратные инструменты. Представьте, что вы делаете себе маленький швейцарский нож: лезвие, отвёртка и штопор. Главное — не пытаться сразу встроить туда микроволновку.

Начнём с функции, которая печатает заголовок. Хочется вызывать её так:


printHeader("Budget Tracker", "версия 0.1")

И получать примерно:

=== Budget Tracker ===
версия 0.1

Реализация может быть максимально прямолинейной:

fun printHeader(title: String, vararg subtitleLines: String) {
    println("=== $title ===")
    for (line in subtitleLines) {
        println(line)
    }
}

fun main() {
    printHeader("Budget Tracker", "версия 0.1", "команды: add/list/exit")
}

Обратите внимание на две важные дизайнерские вещи.

Первая: vararg стоит в конце. Так вызов читается естественно, и вам не нужно включать режим “именованных аргументов” для каждого параметра после vararg. Это прямое следствие правил vararg: если vararg не последний, то параметры после него обычно передаются именованно.

Вторая: пустой вызов тоже корректен:

fun main() {
    printHeader("Budget Tracker")
}

Это нормально: заголовок без подзаголовков — вполне осмысленный сценарий.

printJoined(...): печать значений в одну строку

Когда вы печатаете набор значений, часто хочется вывести их в одну строку с разделителем: a, b, c или a | b | c. В Kotlin есть готовые функции, но в этом курсе нам полезно потренироваться сделать “простую свою”, чтобы лучше чувствовать строки и циклы.

Сделаем утилиту:

  • принимает vararg items: String
  • имеет “опции”: separator, prefix, suffix
  • печатает итог

Важно: “опции” мы ставим после vararg. Тогда их логично передавать именованно, чтобы вызов не выглядел как “угадай параметр по позиции”.

fun printJoined(
    vararg items: String,
    separator: String = ", ",
    prefix: String = "",
    suffix: String = ""
) {
    val sb = StringBuilder()
    sb.append(prefix)

    var first = true
    for (item in items) {
        if (!first) sb.append(separator)
        sb.append(item)
        first = false
    }

    sb.append(suffix)
    println(sb.toString())
}

fun main() {
    printJoined("kotlin", "vararg", "spread")                 // kotlin, vararg, spread
    printJoined("a", "b", "c", separator = " | ")             // a | b | c
    printJoined("OK", prefix = "[", suffix = "]")             // [OK]
}

Здесь есть тонкий момент дизайна: separator, prefix, suffix — это параметры, которые по смыслу являются настройками, а не частью данных. Поэтому именованные аргументы делают вызов “самодокументируемым”: читатель видит separator = " | ", и мозг не тратит силы на расшифровку.

И ещё один момент: если items пустой, результат будет просто prefix + suffix. Иногда это полезно (например, печать пустых скобок []), иногда — нет. Важно помнить: vararg почти всегда заставляет задуматься про f().

Шпаргалка: как проектировать читаемые вызовы

Когда вы проектируете утилиту, главный критерий — не то, насколько вы “умно” написали функцию, а то, насколько приятно её вызывать. Поэтому полезно держать в голове простую шпаргалку:

Хотим на месте вызова Значит, в сигнатуре
“Данные” идут перечислением: f("a", "b") vararg для данных
“Настройки” читаются явно: separator = ... параметры после vararg + значения по умолчанию
Не гадать, что такое “пусто” или нейтральный результат, или ...OrNull, или require
Лёгко переиспользовать одну утилиту внутри другой проксирование vararg через *

Последний пункт — как раз центр сегодняшней лекции: проксирование vararg.

3. Проксирование vararg и цепочки утилит

Массив ≠ набор аргументов: spread‑оператор *

Самая частая ситуация в реальном коде: вы написали одну полезную vararg‑функцию, а потом хотите написать вторую — которая делает чуть больше, но внутри вызывает первую.

Например:

  • printLines(vararg lines: String) — печатает строки как есть
  • printBlock(title: String, vararg lines: String) — печатает заголовок, потом вызывает printLines(...) для строк

Интуитивно хочется сделать так:

fun printLines(vararg lines: String) {
    for (line in lines) println(line)
}

fun printBlock(title: String, vararg lines: String) {
    println("== $title ==")
    printLines(lines) // ОШИБКА
}

Но lines внутри printBlock — это уже массив (Array<String>), а printLines(...) ожидает набор аргументов. Это разные формы вызова.

Правильный способ — spread‑оператор *, который “раскрывает” массив как список отдельных аргументов.

fun printLines(vararg lines: String) {
    for (line in lines) println(line)
}

fun printBlock(title: String, vararg lines: String) {
    println("== $title ==")
    printLines(*lines) // <-- распаковали массив в vararg
}

fun main() {
    printBlock("Сегодня", "кофе", "код", "ещё код")
}

Если вы один раз прочувствуете мысль “массив ≠ набор аргументов”, 70% странных ошибок с vararg исчезнут сами собой.

Проксирование + добавить свой аргумент: printBlock(...) с подвалом

Иногда вы хотите:

  • взять набор строк
  • добавить к нему ещё одну строку (например, подвал)
  • вывести всё одним блоком

Можно сделать так:

fun printLines(vararg lines: String) {
    for (line in lines) println(line)
}

fun printBlock(title: String, vararg lines: String, footer: String = "") {
    println("== $title ==")

    if (footer.isEmpty()) {
        printLines(*lines)
    } else {
        // Смешанный вызов: часть аргументов вручную, часть — через *lines
        printLines(*lines, footer)
    }
}

fun main() {
    printBlock("Покупки", "хлеб", "молоко", footer = "Итого: 2 позиции")
}

Смешанный вызов (*lines, footer) — это прямой “супер‑кейс” vararg: вы как будто собираете один общий список аргументов из двух источников.

И да: footer стоит после vararg, значит, мы передаём его именованно в вызове printBlock(..., footer = ...). Это ожидаемо и соответствует правилам vararg.

Мини‑логгер: log(tag, vararg messages) и обёртки info(...), error(...)

Давайте сделаем утилиту, которая печатает сообщения с “тегом”, как в логах:

[INFO] started
[INFO] loading data
[ERROR] invalid input

Пусть базовая функция делает одно действие: печатает одну строку:

fun logLine(tag: String, message: String) {
    println("[$tag] $message")
}

fun main() {
    logLine("INFO", "started")  // [INFO] started
}

Теперь сделаем log(tag, vararg messages), которая печатает много строк, но переиспользует logLine:

fun logLine(tag: String, message: String) {
    println("[$tag] $message")
}

fun log(tag: String, vararg messages: String) {
    for (m in messages) {
        logLine(tag, m)
    }
}

fun main() {
    log("INFO", "started", "loading data", "done")
}

А теперь сделаем “обёртки” info(...) и error(...), которые фиксируют тег и просто пробрасывают сообщения дальше.

Вот здесь проксирование vararg проявляется во всей красе:

fun log(tag: String, vararg messages: String) {
    for (m in messages) {
        println("[$tag] $m")
    }
}

fun info(vararg messages: String) {
    log("INFO", *messages)
}

fun error(vararg messages: String) {
    log("ERROR", *messages)
}

fun main() {
    info("app started", "user = guest")
    error("wrong input", "expected number")
}

Если забыть *messages, компилятор скажет, что типы не подходят: он увидит попытку передать Array<String> туда, где ждут отдельные String. И в этом месте компилятор ваш друг: он спасает вас от логической ошибки. Spread‑оператор — официально предусмотренный способ “передать массив как vararg”.

Схема: как выглядит “цепочка утилит” в консоли

Иногда полезно увидеть это не только глазами, но и в виде схемы — особенно когда утилит становится несколько.

flowchart TD
    A[main] --> B["info(vararg messages)"]
    B --> C["log(tag, vararg messages)"]
    C --> D["println(...)"]

Смысл этой схемы простой: маленькие функции не обязательно “делают что-то новое”. Иногда их задача — зафиксировать настройки (tag = "INFO") и сделать вызов короче и понятнее.

4. Мини‑пример: печать отчёта

Теперь объединим всё в мини‑сценарий, похожий на реальную консольную программу. Допустим, у нас есть “учёт расходов” (условный), и мы хотим красиво вывести отчёт из нескольких строк.

Сделаем утилиту printReport(title, vararg lines), которая:

  • печатает шапку
  • печатает строки
  • печатает линию‑разделитель
fun printSeparator(char: Char = '-', count: Int = 20) {
    repeat(count) { print(char) }
    println()
}

fun printReport(title: String, vararg lines: String) {
    printSeparator('=')
    println(title)
    printSeparator('=')
    for (line in lines) println(line)
    printSeparator()
}

fun main() {
    printReport(
        "Отчёт за день",
        "кофе: 3.50",
        "обед: 12.00",
        "итого: 15.50"
    )
}

Здесь мы используем то, что вы уже знаете: repeat(...) и println. При этом дизайн у printReport такой, что на месте вызова читается почти как “данные отчёта”.

Если вы обратите внимание, printSeparator — тоже маленькая утилита, но уже без vararg. Это нормальная практика: vararg хорош там, где естественно передавать “много однотипных кусочков”.

5. Ещё примеры и антипримеры vararg

Когда vararg делать не надо

Очень легко влюбиться в vararg и начать делать так:

fun doEverything(vararg args: String) { ... }

…а потом внезапно понять, что вы написали “функцию‑мешок”, и никто (включая вас через неделю) не знает, что в неё передавать и в каком порядке.

В рамках наших утилит хорошее правило такое: vararg должен быть однородным по смыслу. Если это строки отчёта — отлично. Если это сообщения лога — отлично. Если это “команда, имя пользователя, пароль, режим, таймаут, и ещё три штуки” — это уже не vararg, а попытка спрятать плохой дизайн под ковёр.

Пример с числами: printStats(...) и контракт ...OrNull

Сделаем ещё одну утилиту, где vararg — это набор чисел, а функция печатает статистику. Мы не будем использовать коллекции и сложные операции — только циклы и переменные, как вы уже умеете.

fun meanOrNull(vararg xs: Int): Double? {
    if (xs.size == 0) return null

    var sum = 0
    for (x in xs) sum += x
    return sum.toDouble() / xs.size
}

fun printStats(label: String, vararg xs: Int) {
    val mean = meanOrNull(*xs)
    println("$label: mean = ${mean ?: "n/a"}")
}

fun main() {
    printStats("Скорости", 10, 12, 11)     // Скорости: mean = 11.0
    printStats("Пусто")                    // Пусто: mean = n/a
}

Обратите внимание на две вещи.

Во-первых, printStats получает xs как vararg, но передаёт его в meanOrNull через *xs. Это опять то самое проксирование набора аргументов.

Во-вторых, meanOrNull — это пример контракта ...OrNull: если данных нет, результата нет. Вызывающий код обязан обработать null, и это честно.

6. Типичные ошибки при дизайне утилит на vararg

Ошибка №1: утилита делает вызов короче, но смысл — туманнее.
Иногда функция на vararg выглядит “классно”, потому что в неё можно передать что угодно. Но если с места вызова непонятно, что за строки вы передаёте и почему именно так, то это ухудшение читаемости, а не улучшение. Утилита хороша, когда вызов похож на предложение на русском: “распечатай отчёт с такими строками”, “залогируй такие сообщения”.

Ошибка №2: забыли * при проксировании vararg в другую vararg‑функцию.
Это классика: внутри у вас параметр messages, и вы пишете log("INFO", messages). Но messages — это массив, а не список аргументов. Нужно log("INFO", *messages), потому что spread‑оператор — официальный способ передать массив как vararg.

Ошибка №3: слишком много “опций” вокруг vararg, и вызов становится как приборная панель самолёта.
Если у функции после vararg три-четыре параметра, да ещё без значений по умолчанию, то каждый вызов превращается в длинную строку с кучей name = value. Иногда это нормально, но чаще сигнал, что вы пытаетесь одной функцией закрыть слишком много сценариев. Лучше иметь две небольшие утилиты, чем одну “универсальную” — универсальность в учебном коде часто означает “теперь это никто не понимает”.

Ошибка №4: vararg используется “на всякий случай”, хотя значений всегда ровно два.
Если по смыслу всегда два элемента (например, “логин и пароль” — хотя в реальных проектах так делать не надо), то vararg только ухудшит дизайн. vararg полезен тогда, когда количество значений действительно меняется, и это нормальная часть сценария.

Ошибка №5: не продуман случай пустого набора, и функция падает на xs[0].
Это особенно часто бывает в функциях типа “максимум” или “минимум”: вы берёте xs[0] как старт, а потом внезапно кто-то вызывает max() без аргументов. Если пустой набор допустим, возвращайте null (...OrNull) или нейтральное значение там, где оно честно. Если пустой набор — ошибка использования, фиксируйте контракт через require(...), чтобы падение было ранним и понятным.

1
Задача
Kotlin SELF, 16 уровень, 4 лекция
Недоступна
Шапка приложения
Шапка приложения
1
Задача
Kotlin SELF, 16 уровень, 4 лекция
Недоступна
Строка статуса
Строка статуса
1
Задача
Kotlin SELF, 16 уровень, 4 лекция
Недоступна
Печатный блок
Печатный блок
1
Задача
Kotlin SELF, 16 уровень, 4 лекция
Недоступна
Логи по уровням
Логи по уровням
1
Опрос
`vararg` и spread `*`, 16 уровень, 4 лекция
Недоступен
`vararg` и spread `*`
`vararg` и spread `*`
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Yan Уровень 1
16 апреля 2026
Здесь мы используем то, что вы уже знаете: repeat(...) В каком шаге был repeat?