JavaRush /Курсы /Kotlin SELF /Возврат 2–3 значений из функции

Возврат 2–3 значений из функции

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

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 означает
Pair<Int, Int>
для диапазона
from
to
Pair<Int, Int>
для min/max
min
max
Triple<Int, Int, Int>
для статистики
min
max
sum

И да, порядок — это часть контракта. Если вы один раз решили “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 и текст “возраст не число”.

Контракт: firstInt? (число или 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, если не удалось распарсить).

Контракт: firstfirstName, secondlastName, thirdageOrNull.

Чтобы не уходить в сложные строковые операции, фамилию будем искать по пробелу (первый пробел разделяет имя и фамилию). Если пробела нет — фамилия будет пустой строкой.

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 это просто “первое по порядку” и “второе по порядку”. Не приписывайте контейнеру магию. Магия должна быть в ваших именах переменных и в понятном контракте функции.

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