1. Pair/Triple і контракт результату
У реальному житті майже будь-яка корисна функція трохи «жадібна»: їй часто хочеться повернути не одне значення, а одразу кілька. Наприклад, ви розібрали рядок і хочете повернути і результат, і «хвіст» рядка. Або порахували мінімум і максимум. Або знайшли координати й одразу обчислили за ними ще щось. І тут виникає конфлікт: функція в Kotlin завжди повертає одне значення.
Та, як ви вже, мабуть, здогадуєтеся, трагедії тут немає: одне повернене значення може бути контейнером, у якому лежить кілька частин. Pair і Triple якраз і існують, щоб «звʼязати в пачку» 2–3 значення та повернути їх однією посилкою.
Повертаємо контейнер, а не «одне число»
Раніше ви звикли, що повернене значення — це щось на кшталт Int, String або Boolean. Але Kotlin не обмежує вас «примітивами»: поверненим значенням може бути і Pair, і Triple.
Наприклад, якщо функція обчислює мінімум і максимум із двох чисел, вона може повернути Pair<Int, Int>, де за контрактом «перше — мінімум, друге — максимум»:
fun minMax(a: Int, b: Int): Pair<Int, Int> {
return if (a < b) a to b else b to a
}
fun main() {
val result = minMax(10, 3)
println(result.first) // 3
println(result.second) // 10
}
Тут a to b — це створення Pair(a, b) в інфіксній формі (тобто «між двома значеннями»). По суті, це звичайний виклик функції to зі стандартної бібліотеки.
З погляду компілятора все чесно: функція повернула одне значення — обʼєкт Pair. А для нас, людей, це «дві повʼязані штуки в одному пакеті».
Контракт результату: головна домовленість
Коли ви повертаєте Pair або Triple, ви ніби кажете коду, що викликає: «Я поверну вам коробку. У ній два або три відділення. І ви маєте знати, що в якому лежить». Це і є контракт результату: домовленість про зміст і порядок компонентів.
Найпоширеніша пастка така: first/second/third — імена технічні. Вони не несуть змісту предметної області. Зміст зʼявляється лише з контексту: як ви назвали змінні, де розпакували результат і як задокументували функцію.
Подивіться на контраст:
fun minMax(a: Int, b: Int): Pair<Int, Int> = if (a < b) a to b else b to a
fun main() {
val x = minMax(10, 3)
println(x.first) // 3
println(x.second) // 10
}
Це робочий код, але читається він так собі: «x.first — це що? мінімум? початок діапазону? x-координата?».
А тепер те саме, але з нормальними іменами:
fun minMax(a: Int, b: Int): Pair<Int, Int> = if (a < b) a to b else b to a
fun main() {
val (min, max) = minMax(10, 3)
println("min=$min max=$max") // min=3 max=10
}
Технічно все те саме. Але тепер контракт результату «винесено назовні», і він став очевидним: min і max підписані прямо в місці використання. Це майже як коментар — тільки коментар, який компілюється.
Щоб простіше було тримати це в голові, зручно уявляти контракт Pair/Triple як маленьку табличку (у себе в голові або в коментарі над функцією):
| Функція повертає | first означає | second означає | third означає |
|---|---|---|---|
для діапазону |
|
|
— |
для min/max |
|
|
— |
для статистики |
|
|
|
І так, порядок — це частина контракту. Якщо ви один раз вирішили, що «first — це min», далі так і живете. Не «сьогодні min/max, завтра max/min, бо настрій змінився».
Деконструкція як документація в місці виклику
Багато хто думає, що деконструкція — це просто «щоб коротше». Насправді в навчальному та командному коді це часто про інше: щоб зрозуміліше.
Коли ви пишете:
val (min, max) = minMax(10, 3)
ви одночасно робите дві речі. По-перше, розпаковуєте Pair. По-друге, документуєте контракт: у first лежить мінімум, у second — максимум.
Тут корисно виробити звичку: якщо ви повертаєте Pair/Triple із функції, то в місці виклику майже завжди краще одразу деконструювати результат. Тоді далі за кодом ви працюєте зі звичайними змінними, а не з «анонімними компонентами».
2. Практика: парсимо рядок і повертаємо 2–3 значення
Продовжімо наш навчальний консольний «мініпарсер профілю». Ідея проста: користувач вводить рядок виду:
"Ada Lovelace, 36" — а ми хочемо отримати окремо імʼя, прізвище та вік.
Крок 1: ділимо рядок за комою й повертаємо дві частини
Спочатку виділимо зовсім маленьку, але корисну функцію: «розділи рядок за першою комою».
Контракт: first — ліва частина, second — права частина (обидві вже trim()).
fun splitByComma(text: String): Pair<String, String> {
val commaIndex = text.indexOf(',')
if (commaIndex == -1) return text.trim() to ""
val left = text.substring(0, commaIndex).trim()
val right = text.substring(commaIndex + 1).trim()
return left to right
}
fun main() {
val (a, b) = splitByComma(" Ada Lovelace, 36 ")
println("a='$a' b='$b'") // a='Ada Lovelace' b='36'
}
Зверніть увагу: ми не ускладнюємо собі життя. Якщо коми немає — повертаємо праву частину порожнім рядком. Це теж контракт, просто «контракт для некоректного введення».
Крок 2: парсимо вік і повертаємо «значення + повідомлення»
Іноді корисно повернути не лише дані, а й пояснення того, що сталося. Наприклад: розпарсили вік — чудово. Не розпарсили — повернули null і текст «вік не число».
Контракт: first — Int? (число або null), second — повідомлення для користувача або логу.
fun parseAge(text: String): Pair<Int?, String> {
val trimmed = text.trim()
if (trimmed.isEmpty()) return null to "Вік не вказано"
val age = trimmed.toIntOrNull()
if (age == null) return null to "Вік має бути числом"
if (age < 0) return null to "Вік не може бути відʼємним"
return age to "OK"
}
fun main() {
val (age, message) = parseAge(" 36 ")
println("age=$age message='$message'") // age=36 message='OK'
}
Тут важливий момент про контракт: ми домовилися, що "OK" означає успіх. Можна було б повернути Boolean замість рядка (наприклад, Pair<Boolean, Int>), але для навчального прикладу рядок зручніший: ми одразу бачимо «чому не вийшло».
Крок 3: збираємо все разом і повертаємо Triple
Тепер зробімо функцію, яка повертає три результати: імʼя, прізвище та вік (вік може бути null, якщо не вдалося розпарсити).
Контракт: first — firstName, second — lastName, third — ageOrNull.
Щоб не заглиблюватися в складні рядкові операції, прізвище шукатимемо за пробілом (перший пробіл розділяє імʼя та прізвище). Якщо пробілу немає — прізвище буде порожнім рядком.
fun parseNameAge(line: String): Triple<String, String, Int?> {
val (fullName, ageText) = splitByComma(line)
val spaceIndex = fullName.indexOf(' ')
val firstName = if (spaceIndex == -1) fullName.trim() else fullName.substring(0, spaceIndex).trim()
val lastName = if (spaceIndex == -1) "" else fullName.substring(spaceIndex + 1).trim()
val (age, _) = parseAge(ageText)
return Triple(firstName, lastName, age)
}
fun main() {
val (first, last, age) = parseNameAge("Ada Lovelace, 36")
println("first='$first' last='$last' age=$age") // first='Ada' last='Lovelace' age=36
}
Так, Triple тут виглядає доречно: три частини однієї сутності «профіль». Але зверніть увагу: Triple сам по собі не робить контракт зрозумілим. Зрозумілим його зробили ми: розпакували результат у first/last/age, а не в a/b/c.
Мінісхема: від введення до використання
Іноді корисно закріпити в голові загальний процес. Він виглядає приблизно так:
flowchart TD
A["Ввід рядка (readln)"] --> B["parseNameAge(line) -> Triple"]
B --> C["val (first, last, age) = ..."]
C --> D["Використовуємо first/last/age у логіці"]
І ось тут Pair/Triple справді добре працюють: функція повертає один обʼєкт, а ви одразу «розкладаєте його по поличках» у місці виклику.
3. Коли Pair/Triple починають погіршувати читабельність
На перших кроках Pair відчувається як подарунок: менше коду, усе компактно. Але є момент, коли компактність починає працювати проти вас.
Типовий симптом: ви ловите себе на тому, що пишете щось на кшталт result.first і вже не памʼятаєте, що там лежить. Або у вас зʼявляються змінні p, x, t, res, і ви починаєте спілкуватися з кодом як археолог: «цікаво, що хотів сказати автор цим написом на стіні».
Є і більш явний червоний прапорець: коли ви повертаєте Pair/Triple, але змушені писати довгий коментар на кшталт «first — це…, second — це…, third — це…», бо інакше ніхто не зрозуміє. Це означає, що вашому коду вже проситься «іменований результат» (окремий тип, де поля названі за змістом).
Ми поки не вводитимемо власні типи даних — це тема наступних великих блоків курсу. Але ідею варто запамʼятати: Pair/Triple добрі як швидкий контейнер, а не як офіційний формат даних.
Правила хорошого тону
Коли ви повертаєте 2–3 значення через Pair/Triple, намагайтеся мислити не «як би впхнути все в контейнер», а «як зробити контракт очевидним».
По-перше, фіксуйте порядок компонентів як частину контракту й не змінюйте його залежно від настрою. Якщо minMax повертає (min, max), то так має бути завжди.
По-друге, майже завжди деконструюйте результат у місці виклику й давайте компонентам імена за змістом. val (min, max) = ... у рази читабельніше, ніж val p = ..., а далі p.first.
По-третє, не робіть Triple «мішком усього». Три значення мають бути частинами одного змісту. Якщо ви повертаєте Triple(userName, taxRate, planetName), то це не «контракт», а початок поганого анекдоту.
4. Типові помилки
Помилка №1: «плаваючий» порядок компонентів.
Дуже легко одного разу повернути (max, min), бо «так вийшло», а в іншому місці очікувати (min, max). Ззовні обидва варіанти виглядають однаково: Pair<Int, Int>. Компілятор вас не врятує. Врятує лише дисципліна: один раз зафіксували контракт і дотримуємося його всюди.
Помилка №2: беззмістовні імена під час деконструкції.
Формально val (a, b) = minMax(10, 3) працює. Але вже за день ви самі дивитиметеся на a і b як на «щось перше» і «щось друге». Деконструкція сильна саме тим, що ви можете назвати компоненти осмислено: val (min, max) = ....
Помилка №3: Pair/Triple використовуються для неповʼязаних даних.
Pair — це не «двомісний візок для всього підряд», а контейнер для повʼязаного результату. Якщо значення не є частинами одного змісту, читання коду перетворюється на вгадування, а будь-яке редагування — на ризик переплутати компоненти.
Помилка №4: повертаємо Triple, хоча потрібна лише одна частина.
Іноді початківці роблять «універсальний результат» на всі випадки життя, а потім у 80% місць використовують лише first. Це сигнал, що дизайн результату не збігається з реальним використанням. Або повертайте рівно те, що потрібно, або перегляньте контракт.
Помилка №5: спроба «сховати» зміст усередині first/second/third.
Іноді хочеться думати, що first — це «головне», а second — «другорядне». Але для Kotlin це просто «перше за порядком» і «друге за порядком». Не приписуйте контейнеру магію. Магія має бути у ваших іменах змінних і в прозорому контракті функції.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ