1. Рядок як послідовність символів
Коли ви починаєте «розбирати» рядок, мозок автоматично прагне сприймати його як текст із книжки. Але компʼютеру книжка не потрібна — йому потрібна послідовність символів, у якій кожен символ лежить на своєму місці та має номер. Цей номер називається індексом, і в Kotlin (як і майже всюди) індексація починається з нуля.
Індекси з нуля: чому перший символ — це 0
Ідея «перший елемент має індекс 0» спочатку здається знущанням, але швидко стає зручною. Індекс — це «скільки символів стоїть до поточного». До першого символу стоїть нуль символів — отже, індекс 0. До другого стоїть один — отже, індекс 1. Це ще знадобиться, коли будемо вирізати substring().
Подивімося на рядок "Kotlin":
| Символ | |
|
|
|
|
|
|---|---|---|---|---|---|---|
| Індекс | |
|
|
|
|
|
Кількість символів — 6, а останній індекс — 5.
length і lastIndex: не плутайте «розмір» і «останній індекс»
Властивість length показує, скільки символів у рядку. А lastIndex — це індекс останнього символу.
Важливо відчути різницю: length — це «кількість», а lastIndex — «остання позиція». У колекціях і масивах працює та сама логіка: останній індекс дорівнює size - 1.
Мініприклад:
fun main() {
val s = "Kotlin"
println(s.length) // 6
println(s.lastIndex) // 5
}
Якщо рядок порожній (""), то length буде 0, а lastIndex стане -1. Це звучить дивно, але логічно: «останнього символу немає», тож «останній індекс» іде у відʼємну область. Саме тому ми так багато говоримо про перевірки меж.
2. Доступ до символу за індексом: s[i] і акуратна перевірка меж
Тепер, коли індекси вже є, руки тягнуться зробити так: «дай мені символ номер 0». І Kotlin справді дозволяє звертатися до символу за індексом через квадратні дужки. Але є підступ: якщо індекс неправильний, програма впаде з винятком. А вам потрібно писати код, який не падає через один “не там” поставлений пробіл.
s[i] повертає Char: текст — це не завжди рядок
Оператор s[i] повертає один символ типу Char. Символ — це не рядок. Рядок — це багато символів.
fun main() {
val s = "Kotlin"
val first: Char = s[0]
val last: Char = s[s.lastIndex]
println(first) // K
println(last) // n
}
Якщо ви спробуєте зробити s[6] для "Kotlin", програма впаде: індекси там лише 0..5.
Перевірка меж: «перед тим як узяти — переконайтеся, що можна»
Тут корисно тримати в голові коротке правило: допустимі індекси — це всі числа, які не менші за 0 і менші за length.
Невелика допоміжна функція, яка безпечно повертає символ або '?', якщо індекс неправильний:
fun charOrQuestion(text: String, index: Int): Char {
if (index < 0 || index >= text.length) return '?'
return text[index]
}
fun main() {
println(charOrQuestion("Kotlin", 2)) // t
println(charOrQuestion("Kotlin", 99)) // ?
}
Порожній рядок: найменший, але дуже підступний випадок
Порожній рядок — це рядок довжини 0. У нього немає ані першого, ані останнього символу. Це означає, що навіть s[0] — небезпечний.
fun main() {
val s = ""
println(s.length) // 0
println(s.lastIndex) // -1
println(s[0]) // так робити не можна — буде помилка
}
Корисна звичка, яка рятує нерви: перед доступом до s[0] запитувати себе: «а рядок точно не порожній?»
3. Пошук у рядку: indexOf() і перевірки contains(), startsWith(), endsWith()
Зазвичай ми розбираємо рядки не від нудьги, а тому що в рядку є роздільник: пробіл, @, =, двокрапка, крапка тощо. Сценарій часто такий: «знайди, де стоїть роздільник, і розріж рядок на дві частини».
indexOf(...): домовленість про -1
Метод indexOf(...) шукає підрядок (або символ) і повертає індекс першого входження. А якщо не знайшов — повертає -1. Це дуже важлива домовленість: -1 означає «не знайдено».
fun main() {
val email = "user@example.com"
val at = email.indexOf('@')
println(at) // 4
val slash = email.indexOf('/')
println(slash) // -1
}
Якщо ви забудете перевірити -1 і почнете різати рядок за цим індексом, вийде або помилка меж, або дуже дивний результат.
contains(), startsWith(), endsWith(): коли індекси не потрібні
Іноді вам не потрібен індекс — вам потрібна лише відповідь «так/ні». Тоді зручніше використовувати методи-перевірки.
fun main() {
val filename = "report.txt"
println(filename.contains(".")) // true
println(filename.startsWith("rep")) // true
println(filename.endsWith(".txt")) // true
}
Ці методи відповідають на прості запитання:
- contains() — чи міститься в рядку переданий підрядок?
- startsWith() — чи починається рядок із переданого підрядка?
- endsWith() — чи закінчується рядок на переданий підрядок?
Такі перевірки часто роблять код безпечнішим: ви рідше вручну «лізете» в індекси, і шансів помилитися «на 1» теж менше.
4. substring(): ріжемо рядок на частини, не наступаючи на граблі
Якщо індекси — це «координати» в рядку, то substring() — це «ножиці». Майже будь-який розбір рядка зводиться до одного шаблону: знайти індекс роздільника й вирізати ліву та праву частини.
Головне правило substring(start, end): end не входить
Метод substring(start, end) бере символи з позиції start включно, а от end — не включається. Це класичний стиль «напівінтервалу». Він неймовірно зручний, але спочатку плутає.
Схема:
"Kotlin"
012345
substring(0, 3) -> беремо індекси 0,1,2 -> "Kot"
Приклад:
fun main() {
val s = "Kotlin"
println(s.substring(0, 3)) // Kot
println(s.substring(3, 6)) // lin
}
Зауважте: end може дорівнювати length, і це нормально. «До кінця рядка» — це якраз end = length.
substring(start) — від позиції до кінця
Є зручна форма substring(start) — вона бере від start до кінця рядка.
fun main() {
val s = "Kotlin"
println(s.substring(2)) // tlin
}
Це дуже зручно для правої частини після роздільника: «після @ до кінця», «після = до кінця».
Межі substring(): де програма може впасти
substring() вимагає коректних меж. Якщо спростити: start і end мають бути в діапазоні 0..length, а також start <= end. Якщо порушити ці умови — буде помилка.
Тому досвідчений програміст майже ніколи не пише substring(...) напряму: спершу він перевіряє, що індекси коректні.
5. Шаблон «знайшов → перевірив → вирізав»
Зараз ми зберемо все в один стійкий шаблон і додамо його в наш навчальний консольний застосунок. Уявімо, що ми робимо просту програму «Профіль користувача»: користувач вводить повне імʼя одним рядком і email, а ми виводимо акуратну картку. Сьогодні розберемося саме з тим, як безпечно отримувати частини цих рядків через індекси.
Наша реальність сувора: люди вводять імʼя як завгодно. Так, вони дивно ставлять пробіли. Так, іноді вони забувають @. Це не «погані користувачі», це «реальний світ».
Розбираємо повне імʼя "Імʼя Прізвище" через indexOf(' ')
Найпростіша версія повного імені — це два слова, розділені пробілом. Ми поки навмисно не використовуємо split() (це буде в наступній лекції), тому зробимо «ручний розріз»: знайдемо індекс пробілу й виріжемо ліву та праву частини.
fun main() {
val fullName = "Іван Петров"
val space = fullName.indexOf(' ')
val firstName = if (space == -1) fullName else fullName.substring(0, space)
val lastName = if (space == -1) "" else fullName.substring(space + 1)
println(firstName) // Іван
println(lastName) // Петров
}
Зверніть увагу на space + 1: ми не хочемо, щоб пробіл потрапив у прізвище. Якщо пробіл — останній символ рядка, substring(space + 1) перетвориться на substring(length), а це просто порожній рядок (і це нормально).
Дістаємо домен з email: indexOf('@') і перевірки
Email — класичний приклад рядка з роздільником. Ми хочемо взяти домен: усе, що стоїть після @. Але потрібно захиститися від випадків "@example.com" і "user@".
fun extractDomain(email: String): String {
val at = email.indexOf('@')
if (at == -1) return "INVALID"
if (at == 0) return "INVALID"
if (at == email.lastIndex) return "INVALID"
return email.substring(at + 1)
}
fun main() {
println(extractDomain("user@example.com")) // example.com
println(extractDomain("user@")) // INVALID
}
Тут ми використовуємо email.lastIndex, щоб перевірити: «@ стоїть останнім символом чи ні». І це якраз той випадок, коли lastIndex читається краще, ніж length - 1.
Розбираємо рядок формату "key=value"
Дуже популярний формат введення — ключ=значення. Сьогодні ми зробимо просту версію: знайдемо = і розріжемо рядок.
fun main() {
val line = "city=Tokio"
val eq = line.indexOf('=')
val key = if (eq == -1) "" else line.substring(0, eq)
val value = if (eq == -1) "" else line.substring(eq + 1)
println("key='$key', value='$value'") // key='city', value='Tokio'
}
Шаблон той самий: знайшли індекс → перевірили -1 → вирізали ліву та праву частини.
Збираємо «картку профілю» і не плутаємо розбір із виведенням
Тепер зберемо маленький фрагмент нашого застосунку: читаємо fullName і email, обчислюємо firstName, lastName, domain і друкуємо картку.
fun extractDomain(email: String): String {
val at = email.indexOf('@')
if (at <= 0 || at == email.lastIndex) return "INVALID"
return email.substring(at + 1)
}
fun main() {
print("Введіть імʼя та прізвище: ")
val fullName = readln()
print("Введіть email: ")
val email = readln()
val space = fullName.indexOf(' ')
val firstName = if (space == -1) fullName else fullName.substring(0, space)
val lastName = if (space == -1) "" else fullName.substring(space + 1)
val domain = extractDomain(email)
val card = """
|Профіль
|-------
|Імʼя: $firstName
|Прізвище: $lastName
|Домен email: $domain
""".trimMargin()
println(card)
}
Тут логіка розбору (індекси, indexOf(), substring()) розташована поруч, але не змішується в один гігантський рядок. Такий стиль простіше налагоджувати: якщо картка дивна, ви можете роздрукувати проміжні значення space, firstName, lastName.
Шпаргалка щодо індексів і проблемних місць
Коли ви працюєте з рядками, корисно тримати в голові маленьку таблицю-шпаргалку: що повертається і що обовʼязково потрібно перевіряти.
| Операція | Що повертає | На що дивитися, щоб не впасти |
|---|---|---|
|
кількість символів | якщо 0, рядок порожній |
|
індекс останнього символу | буде -1, якщо рядок порожній |
|
за індексом |
має бути |
|
індекс або -1 | -1 обовʼязково обробити |
|
шматок рядка | і у , і |
|
від до кінця |
має бути в |
І ще один важливий «місток» до розуміння: індекси, що починаються з нуля, — це загальний принцип Kotlin для індексованих структур. Він однаково проявляється і в масивах, і в списках, і в циклах за indices.
6. Типові помилки під час роботи з індексами та підрядками
Помилка №1: плутати length й «останній індекс».
Дуже частий промах — думати, що якщо довжина рядка 6, то останній індекс теж 6. Насправді за індексації з нуля останній індекс завжди на 1 менший. Через це й зʼявляються помилки «на 1»: ви або берете не той символ, або виходите за межі й отримуєте виняток.
Помилка №2: робити s[0] без перевірки на порожній рядок.
Порожній рядок трапляється частіше, ніж здається: користувач натиснув Enter, прийшло порожнє введення, десь «склеїлося» в "". Якщо після цього ви робите text[0], програма падає. Правильна звичка — спочатку перевіряти text.isEmpty() або хоча б text.length == 0, і лише потім лізти в індекси.
Помилка №3: ігнорувати indexOf(...) == -1.
indexOf не «лається» — він чесно повертає -1. Але якщо ви одразу використаєте цей -1 як межу в substring(), починається хаос: у найкращому разі буде виняток, у найгіршому — ви відріжете щось не те й отримаєте «тихо неправильний» результат. Тому звʼязка «знайшов → перевірив → вирізав» має стати рефлексом.
Помилка №4: забути, що substring(start, end) не включає end.
Це джерело помилок, які особливо дратують: програма наче працює, але видає «майже правильну» відповідь — на один символ коротшу або довшу. Найкращі ліки — завжди проговорювати: «start включно, end не включається». А коли сумніваєтеся — зробити маленьку схему на папері.
Помилка №5: вирізати праву частину, не прибравши роздільник (substring(at) замість substring(at + 1)).
Якщо ви ділите "user@example.com" за @, то праву частину ви хочете без @. Це означає substring(at + 1). Якщо написати substring(at), домен почнеться з @, і далі логіка «валідності» та порівняння рядків може почати поводитися дивно. Це особливо неприємно в програмах, де потім домен порівнюють із "gmail.com".
Помилка №6: змішувати розбір рядка та форматування результату в один величезний рядок.
Технічно можна написати один гігантський println("Імʼя: ${...} ..."), але це майже гарантовано призведе до помилки та погіршить читабельність. Надійніше спочатку отримати val firstName, val lastName, val domain, а вже потім зібрати фінальне виведення. Такий код простіше читати, простіше тестувати й простіше лагодити, коли користувач знову введе щось «креативне».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ