JavaRush /Курсы /Kotlin SELF /Многострочные строки в Kotlin

Многострочные строки в Kotlin

Kotlin SELF
9 уровень , 2 лекция
Открыта

1. Многострочные строки

Когда вы только начинаете программировать, кажется, что вывод в консоль — это просто: println("что-то"), и всё. Но как только вы захотите вывести меню, справку, «красивую шапку» или мини‑отчёт на несколько строк, начинается цирк. Вставлять \n вручную можно, но код становится плохо читаемым, а правка текста превращается в игру «найди, где лишний слеш».

Именно поэтому в Kotlin есть многострочные строки (их ещё называют raw strings): они позволяют писать текст в коде почти так, как он должен выглядеть в консоли, без «гитлеров» из \n. Kotlin‑конвенции прямо советуют использовать многострочные строки вместо ручного встраивания \n, а для контроля отступов использовать trimIndent() или trimMargin().

Для разогрева — маленький контраст. Сначала сделаем в лоб через \n, а потом сделаем красиво.

fun main() {
    val help = "Команды:\n" +
        "1) add\n" +
        "2) list\n" +
        "3) exit\n"

    print(help)
}

Да, работает. Но редактировать такое — удовольствие уровня «почистить клавиатуру зубной щёткой».

Синтаксис многострочной строки """...""" и отличие от "..."

Многострочная строка в Kotlin записывается в тройных кавычках: """ ... """. Это выглядит чуть необычно, но логика простая: внутри такого литерала можно писать переносы строк как есть. То есть вы нажимаете Enter — и в строке реально появляется перевод строки. Это резко улучшает читабельность, особенно для больших блоков текста: меню, шаблонов отчётов, «плашек» и т.д.

Важный нюанс: такая строка называется raw («сырая») не потому, что она недоваренная, а потому что внутри неё почти ничего не нужно экранировать. В частности, символы \n и \t внутри raw string — это просто два символа \ и n (или t), а не «перенос строки» и «табуляция». И это одновременно удобно и коварно: удобно — потому что меньше экранирования, коварно — потому что иногда вы ожидаете одно, а получаете другое.

Вот базовый пример многострочной строки:

fun main() {
    val help = """
Команды:
1) add
2) list
3) exit
"""
    print(help)
}

Обратите внимание: текст в консоли будет ровно таким же, как в коде. Это и есть основная идея.

Почему отступы «прилипают» к выводу и зачем нужен trimIndent()

Когда вы начинаете писать многострочный текст внутри функции, вы почти всегда делаете отступы в коде — просто потому что так красиво и по правилам форматирования. Но raw string сохраняет пробелы буквально. Получается классическая ситуация: в коде всё ровно, а в консоли меню внезапно уехало вправо на четыре пробела, потому что вы писали строку внутри блока fun main().

trimIndent() решает именно эту проблему: он убирает общий (минимальный) отступ у всех непустых строк многострочного литерала. Kotlin‑конвенции формулируют это как «используйте trimIndent(), когда результату не нужна внутренняя “особая” индентация».

Пример: сначала «плохо» (отступы попадут в вывод), потом «хорошо».

fun main() {
    val help = """
        Команды:
        1) add
        2) list
        3) exit
    """

    print(help)
}

Этот код выведет строки с отступом. А теперь то же самое, но с trimIndent():

fun main() {
    val help = """
        Команды:
        1) add
        2) list
        3) exit
    """.trimIndent()

    print(help)
}

Теперь пробелы, которые нужны были только для красивого исходника, не ломают итоговый текст.

Чтобы было проще запомнить: trimIndent() — это как «убрать служебный отступ кода, но сохранить относительные отступы внутри текста, если они реально разные».

trimMargin() — когда нужно управлять отступами точнее

Иногда вы хотите, чтобы у текста были отступы внутри (например, вы печатаете кусок псевдокода, шаблон письма или блок отчёта), но при этом не хотите тащить в вывод «лишний мусор» слева из‑за форматирования Kotlin‑кода. В таких случаях trimIndent() может быть недостаточно управляемым: он убирает общий отступ, но не даёт вам «явно отметить» границу текста.

Тут приходит trimMargin(): вы выбираете символ‑маркер (по умолчанию это |) и пишете строки так, чтобы всё «левее маркера» отрезалось. В Kotlin‑конвенциях это описано как вариант для случая, когда внутренняя индентация текста нужна и важна.

Пример с trimMargin():

fun main() {
    val report = """
        |Пользователь: Ира
        |Статус: OK
        |Сообщение: Данные приняты
    """.trimMargin()

    println(report)
}

Здесь вы можете как угодно выравнивать код по отступам, но «граница текста» остаётся стабильной. Если вы внимательно посмотрите на примеры из Kotlin‑гайдов, идея ровно та же: | помогает аккуратно «обрезать» левую часть и оставить красивый вывод.

Небольшой бонус: trimMargin() умеет работать не только с |. Можно выбрать другой маркер (например >), но чаще всего | — это стандарт де‑факто, и лучше не изобретать велосипеды, пока вы не велосипедный завод.

Многострочные строки и string templates: как подставлять значения

До этого момента мы использовали string templates в обычных строках: $name, ${a + b} и так далее. Точно так же они работают и внутри """...""". Это удобно: вы можете сделать многострочный «шаблон отчёта» и вставить туда переменные без конкатенации.

Напоминание по синтаксису templates: простой идентификатор можно подставлять как $a, а выражения — как ${...}.

Вот короткий пример «многострочный отчёт + данные»:

fun main() {
    val user = "Ира"
    val total = 3

    val text = """
        |Отчёт по пользователю
        |---------------------
        |Имя: $user
        |Покупок: $total
    """.trimMargin()

    println(text)
}

В прошлой лекции мы разбирали интересный кейс: если вам нужно вывести символ доллара как текст, Kotlin попытается трактовать $ как начало template. В обычной строке вы писали бы "\$". В raw string это тоже актуально, но сегодня мы не будем углубляться: просто помните, что $ — «спецсимвол» для подстановок, и он не всегда «просто доллар».

2. Простое выравнивание текста: padStart() и padEnd()

Как сделать простые «колонки» и не превращаться в Excel

После того как вы научились выводить многострочный текст, почти сразу хочется следующего: «а можно сделать, чтобы числа стояли ровно столбиком, как в таблице?» Можно. И нет, нам не нужен ни Excel, ни волшебный форматтер, ни печать на матричном принтере 1997 года (хотя у него своя романтика).

padEnd(width) дополняет строку справа пробелами (или другим символом), чтобы она стала нужной длины. padStart(width) делает то же самое, но слева. В результате вы можете выравнивать названия по левому краю, а числа — по правому, как в отчётах и квитанциях.

Сразу важная оговорка: эти функции работают со строками. Поэтому числа (Int) обычно нужно превратить в строку через toString(). Вы уже умеете это делать.

Мини‑пример выравнивания:

fun main() {
    val item = "Tea"
    val qty = 3
    val price = 90

    val line = item.padEnd(10) +
        qty.toString().padStart(3) +
        " x " +
        price.toString().padStart(4)

    println(line) // Tea          3 x   90
}

Тут мы сделали «очень простую» табличную строку. Да, это ещё не полноценный отчёт, но уже видно, что значения не прыгают, даже если qty станет 12, а price160.

3. Печатаем чек в консольном приложении

Сейчас соберём небольшой цельный кусок программы, который мы будем развивать дальше по курсу. Идея простая: мини‑«Кофейня в консоли». Пользователь вводит количество позиций (пока без сложных команд и без хранения истории на годы вперёд), а программа печатает аккуратный чек: с шапкой, списком и итогом.

Почему это хороший пример именно для этой лекции: у нас сразу есть многострочная «шапка» и «подвал», и есть табличные строки, где padStart()/padEnd() реально спасают.

Для начала — функция, которая возвращает красивую шапку. Мы специально возвращаем String, чтобы вы видели: многострочная строка — это обычная строка, просто удобнее написанная.

fun buildHeader(): String {
    return """
        |========================
        |   Kotlin Coffee CLI
        |========================
    """.trimMargin()
}

Теперь сделаем функцию для форматирования одной строки чека. Мы пока держим её максимально простой и «строчной», без наворотов.

fun formatLine(name: String, qty: Int, price: Int): String {
    val sum = qty * price
    return name.padEnd(12) +
        qty.toString().padStart(3) +
        " x " +
        price.toString().padStart(4) +
        " = " +
        sum.toString().padStart(5)
}

Обратите внимание на стиль: мы сначала вычислили sum, а потом форматируем строку. Это почти всегда читабельнее, чем запихивать qty * price внутрь длинной конкатенации. В прошлой лекции мы обсуждали похожую идею для string templates: сложное — сначала в val, потом в текст.

Теперь — main, где мы используем и шапку, и форматирование, и многострочную подсказку для ввода. Здесь мы используем массивы, потому они идеально подходят для «фиксированного набора товаров» на старте.

fun main() {
    val names = arrayOf("Espresso", "Cappuccino", "Tea")
    val prices = intArrayOf(120, 160, 90)
    val qty = IntArray(names.size)

    println(buildHeader())

    for (i in names.indices) {
        print("Сколько '${names[i]}'? ")
        qty[i] = readln().toInt()
    }

    println()
    println("Чек:")
    for (i in names.indices) {
        println(formatLine(names[i], qty[i], prices[i]))
    }
}

Добавим финальный штрих: подвал чека, тоже многострочной строкой. И покажем итоговую сумму. Итог — просто Int, считаем в цикле.

fun buildFooter(total: Int): String {
    return """
        |
        |------------------------
        |ИТОГО: ${total.toString().padStart(5)} евро.
        |Спасибо! Заходите ещё :)
    """.trimMargin()
}

И обновим конец main, добавив подсчёт total и печать подвала:

fun main() {
    val names = arrayOf("Espresso", "Cappuccino", "Tea")
    val prices = intArrayOf(120, 160, 90)
    val qty = IntArray(names.size)

    println(buildHeader())

    for (i in names.indices) {
        print("Сколько '${names[i]}'? ")
        qty[i] = readln().toInt()
    }

    var total = 0
    println("\nЧек:")
    for (i in names.indices) {
        total += qty[i] * prices[i]
        println(formatLine(names[i], qty[i], prices[i]))
    }

    println(buildFooter(total))
}

Вы получили программу, в которой шапка и подвал читаются в коде нормально (не надо мысленно интерпретировать \n), отступы не уезжают благодаря trimMargin(), а строки чека выглядят ровно благодаря padStart()/padEnd(). Это как раз тот стиль, который обычно и рекомендуют: многострочные строки плюс явный контроль отступов через trimIndent()/trimMargin().

Чтобы закрепить картинку, вот маленькая схема того, что мы сделали (не код, а «маршрут мысли»):

flowchart TD
    A["Пишем текст в '''''' ...'''''' "] --> B["Убираем служебные отступы trimIndent/trimMargin"]
    B --> C["Подставляем данные через $var и ${expr}"]
    C --> D["Выравниваем колонки padEnd/padStart"]
    D --> E["println(...) в консоль"]

4. Типичные ошибки при работе с многострочными строками

Ошибка №1: ожидать, что """...""" автоматически «красиво выровняется».
Raw string сохраняет пробелы и переносы строк буквально. Поэтому если вы написали текст с отступом в коде, он с этим отступом и выведется. Лечится это не «подумаю о хорошем», а конкретно: trimIndent() или trimMargin() — в зависимости от того, нужна ли внутренняя индентация.

Ошибка №2: писать \n внутри raw string и ждать переноса строки.
Внутри """...""" последовательность \n не превращается в перевод строки, это просто два символа. Перенос строки делается реальным Enter. Если вы хотите «управлять» переносом, делайте это либо реальными переносами, либо используйте обычную строку "..." (но сегодня как раз смысл в том, чтобы не страдать с \n).

Ошибка №3: путать, что именно убирает trimIndent().
trimIndent() убирает общий минимальный отступ у непустых строк. Если вы хотите явно управлять границей каждой строки (например, часть текста должна быть «внутри блока» и иметь свои отступы), тогда trimMargin() обычно оказывается понятнее: вы сами ставите | и видите, где начинается полезный текст.

Ошибка №4: пытаться выравнивать числа пробелами «на глаз».
Когда вы пишете "Tea 3 90" руками, это кажется нормальным до тех пор, пока не появится "Cappuccino 12 160". Всё немедленно «поплывёт». padStart()/padEnd() решают проблему тем, что ширина колонки задаётся числом, а не настроением автора.

Ошибка №5: выравнивать Int, забыв превратить его в String.
padStart и padEnd — это операции строк. Если вы попробуете применить их к числу напрямую, компилятор справедливо скажет: «я понимаю твоё желание, но у Int такого метода нет». Решение простое: qty.toString().padStart(3) и аналогично для других чисел.

Ошибка №6: перегружать одну огромную строку вычислениями и форматированием.
Технически можно написать монстра на 200 символов и радоваться, что «в одну строку же». Практически через два дня вы сами себе начнёте задавать вопрос: «кто это написал и почему он меня ненавидит?». Чуть более взрослый стиль — сначала вычислить значения в val (например, sum), потом форматировать строку. Такой подход делает код читаемым и уменьшает риск ошибок в форматировании.

1
Задача
Kotlin SELF, 9 уровень, 2 лекция
Недоступна
Экран помощи
Экран помощи
1
Задача
Kotlin SELF, 9 уровень, 2 лекция
Недоступна
Статусная плашка
Статусная плашка
1
Задача
Kotlin SELF, 9 уровень, 2 лекция
Недоступна
Сертификат новичка
Сертификат новичка
1
Задача
Kotlin SELF, 9 уровень, 2 лекция
Недоступна
Таблица погоды
Таблица погоды
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ