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.
2. Проксування 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") і зробити виклик коротшим і зрозумілішим.
3. Міні‑приклад: друк звіту
Тепер об’єднаємо все в міні‑сценарій, схожий на реальну консольну програму. Припустімо, у нас є «облік витрат» (умовний), і ми хочемо гарно вивести звіт із кількох рядків.
Зробімо утиліту 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 добрий там, де природно передавати «багато однотипних шматочків».
4. Ще приклади й антиприклади 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 ?: "н/д"}")
}
fun main() {
printStats("Швидкості", 10, 12, 11) // Швидкості: mean = 11.0
printStats("Порожньо") // Порожньо: mean = н/д
}
Зверніть увагу на дві речі.
По‑перше, printStats отримує xs як vararg, але передає його в meanOrNull через *xs. Це знову те саме проксування набору аргументів.
По‑друге, meanOrNull — це приклад контракту ...OrNull: якщо даних немає, результату немає. Код, що викликає, зобов’язаний обробити null, і це чесно.
5. Типові помилки під час дизайну утиліт на 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(...), щоб падіння було раннім і зрозумілим.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ