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, в которую копируется всё старое содержимое + новый кусок. На трёх строках вы этого не заметите. На трёх тысячах — ваш компьютер начнёт немного задумываться о смысле жизни.
Что мы хотим вместо этого
Нам нужен инструмент, который позволит «дописывать текст внутрь одного объекта», не создавая заново гигантскую строку на каждом шаге. И вот тут на сцену выходит 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 у некоторых появляется соблазн: «О, раз он быстрый, буду собирать вообще всё через него». Это похоже на ситуацию, когда вам подарили шуруповёрт, и вы начинаете им открывать йогурт. Технически возможно, но странно.
Если вы строите одну короткую строку, особенно через string templates, то + или шаблон — абсолютно ок.
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 в строку. Вы и себе помогаете, и отладка станет легче.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ