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(...), чтобы падение было ранним и понятным.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ