JavaRush /Курси /Kotlin SELF /StringBuilder — складання довгого тексту

StringBuilder — складання довгого тексту

Kotlin SELF
Рівень 9 , Лекція 5
Відкрита

1. Вступ

Коли ви тільки починаєте програмувати, конкатенація рядків через + здається ідеальним рішенням: просто берете шматочки тексту й склеюєте їх в один. І це справді нормально, доки цих шматочків небагато. Але щойно з’являється цикл, багато рядків і «складання звіту», раптом виявляється, що таке склеювання безпосередньо створює надто багато проміжних рядків — ніби ви щоразу переписуєте весь звіт заново, додаючи по одній літері.

String незмінюваний

Рядок (String) у Kotlin незмінюваний. Це означає: якщо ви написали val s2 = s1 + "!", то s1 не «розширився», а створився новий рядок s2. Старий рядок змінити не можна — «що написано пером», як то кажуть.

Це зручно й безпечно, але іноді — дорого.

Невдала практика: += у циклі

Подивіться на приклад. Він працює, але має пастку:

fun main() {
    var text = ""

    for (i in 1..3) {
        text += "Рядок №$i\n"
    }

    print(text)
    // Рядок №1
    // Рядок №2
    // Рядок №3
}

Проблема тут не в тому, що код «неправильний». Проблема в тому, що на кожній ітерації створюється новий рядок text, у який копіюється весь попередній вміст плюс новий шматок. На трьох рядках ви цього не помітите. А на 3 000 — ваш комп’ютер почне трохи замислюватися про сенс життя.

Що нам потрібно натомість

Нам потрібен інструмент, який дасть змогу «дописувати текст усередині одного об’єкта», не створюючи щоразу величезний рядок на кожному кроці. І саме тут на сцену виходить StringBuilder.

2. StringBuilder: що це і як він працює

Якщо String — це «напис на камені», то StringBuilder — це «блокнот з олівцем». У блокнот можна дописувати рядки по одному, стирати, перебудовувати (у межах можливого), і лише наприкінці сказати: «Так, тепер акуратно перепиши це начисто в гарний рядок».

StringBuilder — це спеціальний клас для поетапного складання тексту. Він зберігає внутрішній буфер символів і вміє ефективно доповнювати його.

Три основні етапи

Корисно тримати в голові просту схему:

flowchart LR
    A["Шматочки тексту"] --> B["StringBuilder (накопичуємо)"]
    B --> C["toString() (отримали готову String)"]

Тобто логіка така: «накопичуємо» → «перетворюємо на рядок» → «друкуємо/повертаємо».

Мінімальний приклад: скласти фразу

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
    sb.append("Привіт")
    sb.append(", ")
    sb.append("світ!")

    println(sb.toString()) // Привіт, світ!
}

Зверніть увагу: ми не робимо String + String + String. Натомість ми дописуємо все в один об’єкт sb, а наприкінці викликаємо toString().

String і StringBuilder простими словами

String — це готовий незмінюваний рядок. Він чудово підходить для коротких значень, шаблонів на кшталт "Привіт, $name" і ситуацій, де ви один раз зібрали рядок, а далі його лише читаєте.

StringBuilder — це змінюваний «збирач». Він зручний, коли ви складаєте довгий текст по шматочках, особливо в циклі, і хочете наприкінці отримати підсумковий String через toString().

3. append() і appendLine() для складання тексту

Коли ви починаєте формувати звіти, то доволі швидко розумієте: «дописати шматок тексту» й «дописати рядок повністю та додати перенесення» — це дві різні дії. Можна, звісно, щоразу додавати "\n" вручну, але тоді легко заплутатися: десь забули перенесення, десь зробили два перенесення підряд, а десь випадково «приліпили» пробіл перед \n. StringBuilder пропонує читабельніший стиль.

append(...): додали шматочок

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()

    sb.append("Сума: ")
    sb.append(10)
    sb.append(" євро")

    println(sb.toString()) // Сума: 10 євро
}

Суть у тому, що append уміє приймати не лише рядки, а й числа та символи. Kotlin/Java самі перетворять це на текст.

appendLine(...): додали рядок і перенесення

appendLine() — зручна штука: вона додає текст і перенесення рядка. У стандартній бібліотеці Kotlin це extension-функція для StringBuilder, і її можна викликати ланцюжком — як «конструктор звіту».

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
        .appendLine("Звіт")
        .appendLine("-----")
        .appendLine("Статус: OK")

    print(sb.toString())
    // Звіт
    // -----
    // Статус: OK
}

Тут особливо приємно, що appendLine(...) повертає той самий StringBuilder, тож можна писати ланцюжком.

Порожній рядок: appendLine() без аргументів

Іноді між блоками потрібне «повітря».

import kotlin.text.StringBuilder

fun main() {
    val sb = StringBuilder()
    sb.appendLine("Перша частина")
    sb.appendLine() // порожній рядок
    sb.appendLine("Друга частина")

    print(sb.toString())
    // Перша частина
    //
    // Друга частина
}

4. Хороші практики використання StringBuilder

Дуже поширена помилка початківців — зробити StringBuilder глобальним, передавати його через десяток функцій і дописувати «хто що захоче». Так теж можна, але це швидко перетворюється на текстову версію чогось на кшталт: «у нас у проєкті один спільний пакет із дротами — чіпати не можна, тримається на чесному слові».

Набагато краща звичка — створювати StringBuilder усередині функції, складати текст і повертати звичайний String.

Builder усередині функції, назовні — String

import kotlin.text.StringBuilder

fun buildHeader(title: String): String {
    val sb = StringBuilder()
    sb.appendLine("====")
    sb.appendLine(title)
    sb.appendLine("====")
    return sb.toString()
}

fun main() {
    print(buildHeader("Мій звіт"))
    // ====
    // Мій звіт
    // ====
}

Тут дуже чітка межа: функція повертає рядок, а не «напівфабрикат для складання». Це зручно для тестування, для читання коду і для того, щоб випадково не дописати текст «у попередній звіт».

Коли StringBuilder не потрібен

Після знайомства зі StringBuilder у декого з’являється спокуса: «О, раз він швидкий, то буду складати взагалі все через нього». Це схоже на ситуацію, коли вам подарували шурупокрут, і ви починаєте ним відкривати йогурт. Технічно можливо, але дивно.

Якщо ви будуєте один короткий рядок, особливо через рядкові шаблони, то + або шаблон — цілком ок.

fun main() {
    val name = "Іра"
    val tasks = 3

    println("Користувач: $name, завдань: $tasks")
    // Користувач: Іра, завдань: 3
}

Зазвичай працює таке правило: якщо рядок будується в циклі, якщо у вас багато appendLine(...), або ви хочете скласти багаторядковий текст і повернути його з функції, — StringBuilder чудовий кандидат. Якщо рядок один і короткий, шаблон буде простішим і зрозумілішим.

5. Практика: звіт list для мінісписку завдань

На цьому етапі курсу у вас уже достатньо інструментів, щоб скласти простий консольний застосунок «список завдань» без складних структур даних: фіксований масив рядків плюс лічильник кількості зайнятих елементів. Ми вже вміємо читати рядок, нормалізувати його, розбирати команду через split(), використовувати if/else, цикли та функції. Зараз додамо гарний вивід списку завдань, не перетворюючи код на «100 рядків із println».

Уявімо, що стан застосунку такий: є масив tasks і число taskCount, яке показує, скільки завдань реально заповнено.

Заготовка даних

fun main() {
    val tasks = Array(5) { "" }
    var taskCount = 0

    tasks[taskCount++] = "Купити молоко"
    tasks[taskCount++] = "Вивчити StringBuilder"
    tasks[taskCount++] = "Не склеювати рядки в циклі як варвар"

    println("Усього завдань: $taskCount") // Усього завдань: 3
}

Невдалий стиль виводу: багато println

Так робити можна, але це швидко розростається:

fun main() {
    val tasks = arrayOf("A", "B", "C")

    println("Список завдань:")
    println("1. ${tasks[0]}")
    println("2. ${tasks[1]}")
    println("3. ${tasks[2]}")
}

Щойно у вас не три завдання, а «скільки введе користувач», ви переходите на цикл. І тут з’являється або println усередині циклу, або складання тексту. Друк усередині циклу — нормальна практика, але іноді хочеться спочатку побудувати гарний текст, а потім вивести його одним шматком (наприклад, щоб легко повторно використати цей текст в інших місцях програми).

Хороший стиль: зібрати звіт у StringBuilder

Зробімо функцію, яка повертає готовий багаторядковий звіт.

import kotlin.text.StringBuilder

fun buildTaskListReport(tasks: Array<String>, count: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Список завдань:")

    for (i in 0 until count) {
        sb.appendLine("${i + 1}. ${tasks[i]}")
    }

    return sb.toString()
}

Тепер main може виглядати охайно:

fun main() {
    val tasks = Array(5) { "" }
    var taskCount = 0

    tasks[taskCount++] = "Купити молоко"
    tasks[taskCount++] = "Вивчити StringBuilder"

    print(buildTaskListReport(tasks, taskCount))
    // Список завдань:
    // 1. Купити молоко
    // 2. Вивчити StringBuilder
}

Зверніть увагу на print, а не println: звіт уже містить перенесення рядків, і зайве перенесення наприкінці потрібне не завжди.

Нумерація з вирівнюванням

У попередній лекції ви вивчали padStart()/padEnd(). Використаємо їх, щоб номери не «танцювали», якщо завдань стане 12 або 123.

import kotlin.text.StringBuilder

fun buildTaskListReport(tasks: Array<String>, count: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Список завдань:")

    for (i in 0 until count) {
        val num = (i + 1).toString().padStart(2)
        sb.appendLine("$num. ${tasks[i]}")
    }

    return sb.toString()
}

Якщо завдань 3, вийде:

 1. ...
 2. ...
 3. ...

Якщо завдань 12, то:

 9. ...
10. ...
11. ...
12. ...

І текст візуально рівніший, а отже — читається легше.

6. Практика: help і повідомлення про помилки

Коли застосунок стає хоч трохи інтерактивним, «користувацький інтерфейс» у консолі — це не лише список даних, а й підказки: як вводити команди, що означає помилка, які приклади допустимі. Не найкращий спосіб — зберігати все це як один величезний рядок або як багато println. Але якщо підказка хоча б трохи динамічна (наприклад, залежить від ліміту завдань), StringBuilder знову виявляється дуже зручним.

Складання help-тексту через StringBuilder

import kotlin.text.StringBuilder

fun buildHelp(maxTasks: Int): String {
    val sb = StringBuilder()
    sb.appendLine("Команди:")
    sb.appendLine("  add <текст>    — додати завдання")
    sb.appendLine("  list           — показати список")
    sb.appendLine("  exit           — вийти")
    sb.appendLine()
    sb.appendLine("Ліміт завдань: $maxTasks")
    return sb.toString()
}

Використання:

fun main() {
    val maxTasks = 5
    print(buildHelp(maxTasks))
}

Перевага в тому, що якщо ви зміните ліміт завдань, help автоматично залишиться коректним.

Єдиний стиль повідомлень: функція форматування помилки

import kotlin.text.StringBuilder

fun buildError(message: String): String {
    val sb = StringBuilder()
    sb.appendLine("Помилка:")
    sb.appendLine(message)
    return sb.toString()
}

fun main() {
    print(buildError("Невідома команда. Напишіть help."))
    // Помилка:
    // Невідома команда. Напишіть help.
}

Так, можна було зробити "Помилка:\n$message\n". Але щойно формат стане трохи складнішим (пара рядків, приклад, підказка) — builder виграє за читабельністю.

7. Типові помилки під час роботи зі StringBuilder

Помилка № 1: продовжувати робити text += ... усередині циклу «за звичкою».
Це не «гріх»: компілятор вас не насварить, і програма працюватиме. Але на довгих текстах ви отримаєте зайві проміжні рядки та менш передбачувану продуктивність. StringBuilder якраз створений для ситуацій, коли ви додаєте по шматочку багато разів.

Помилка № 2: забувати toString() і намагатися друкувати builder так, ніби це рядок.
У Kotlin println(sb) зазвичай виведе вміст (бо викличеться toString()), і це підступно: здається, що можна «і так». Але щойно ви почнете повертати це значення з функції або передавати далі як String, з’ясується, що тип не той. Хороша дисципліна: builder усередині, назовні — лише String.

Помилка № 3: робити один глобальний StringBuilder на весь застосунок.
Таке рішення швидко призводить до «випадкового накопичення тексту»: ви побудували звіт, потім побудували другий — а він «прилип» до першого, бо builder ніхто не очистив. Значно безпечніше створювати builder локально у функції, де ви формуєте конкретний текст, і там само перетворювати його на String.

Помилка № 4: плутати append() і appendLine() та отримувати «злиплий» вивід.
Коли перенесення рядка важливе для читабельності, краще писати appendLine. Коли ви будуєте один рядок із частин (наприклад, рядок таблиці), краще append. Якщо не розрізняти ці випадки, у вас або все зіллється в один рядок, або з’являться дивні порожні рядки.

Помилка № 5: змішувати обчислення й форматування в одному гігантському append(...).
Іноді хочеться написати монстра на кшталт sb.appendLine("Разом: ${a + b * c - ...}"). Комп’ютер це проковтне, але людина — не зобов’язана. Читабельніше спочатку порахувати у val total = ..., а потім підставити total у рядок. Так ви допомагаєте і собі, і тим, хто читатиме код; та й налагодження стане легшим.

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