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} тощо. Так само вони працюють і всередині """...""". Це зручно: ви можете зробити багаторядковий «шаблон звіту» й підставити туди змінні без конкатенації.
Нагадування про синтаксис шаблонів: простий ідентифікатор можна підставляти як $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, а price — 160.
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 усередину довгої конкатенації. У минулій лекції ми обговорювали схожу ідею для шаблонів рядків: складне — спочатку в 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), а потім форматувати рядок. Такий підхід робить код читабельнішим і зменшує ризик помилок у форматуванні.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ