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.

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(...), щоб падіння було раннім і зрозумілим.

1
Опитування
`vararg` і spread `*`, рівень 16, лекція 4
Недоступний
`vararg` і spread `*`
`vararg` і spread `*`
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ