JavaRush /Курсы /Kotlin SELF /`enum class`: значения, параметры, методы и правила объяв...

`enum class`: значения, параметры, методы и правила объявления

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

1. Зачем нужен enum class

Если вы когда-нибудь писали что-то вроде if (category == "food"), то вы уже на пути к древнему злу под названием «волшебные строки». Они волшебные, потому что любая опечатка превращает логику в тыкву: "Food", "FOOD", "food " и "fod" выглядят почти одинаково для человека, но для программы это разные значения. В итоге программа не ломается компилятором — она ломается жизнью.

enum class — это способ сказать компилятору: «Вот полный список допустимых вариантов. Других быть не может». И дальше Kotlin начинает вам помогать: проверяет типы, подсказывает варианты в IDE, ругается, если вы пытаетесь присвоить что-то “не из списка”.

Представим маленькую табличку, чтобы почувствовать разницу:

Подход Что храним Что может пойти не так
Строки
"FOOD", "food", "еда"
опечатки, разные регистры, разные языки, пробелы
enum class
Category.FOOD
практически только ошибки при разборе пользовательского ввода (и это решаемо)

И ещё один жизненный пример: категории расходов. Если вы храните категорию строкой, то однажды у вас появится одновременно "Transport" и "TRANSPORT", и отчёт «по категориям» внезапно покажет две почти одинаковые категории. Это не баг, это «особенность пользовательского ввода», но страдать будете вы.

Минимальный enum

Когда говорят «enum», часто представляют что-то страшное и корпоративное. На деле базовая форма максимально простая: вы перечисляете варианты через запятую. Каждый вариант (его ещё называют enum entry, то есть «значение перечисления») — это готовое значение типа enum.

Вот самый минимальный пример:

enum class Status { NEW, IN_PROGRESS, DONE }

fun main() {
    val s: Status = Status.DONE
    println(s) // DONE
}

Здесь Status.DONE — это не строка, не число и не «просто текст». Это значение типа Status. И Kotlin теперь не позволит вам случайно присвоить туда что-то другое:

enum class Status { NEW, IN_PROGRESS, DONE }

fun main() {
    val s: Status = Status.DONE
    // s = "DONE" // так нельзя: тип String не подходит
}

Важная мысль: enum — это «тип с фиксированным набором значений». Именно поэтому enum хорошо заменяет строки в местах, где «варианты заранее известны».

name и ordinal

Когда вы выводите enum через println(s), Kotlin обычно печатает имя варианта (DONE). И это не случайность: у каждого enum-значения есть стандартные свойства name и ordinal. name — это строковое имя константы, а ordinal — её номер по порядку, начиная с нуля. Это стандартное поведение перечислений в Kotlin.

Посмотрим:

enum class Status { NEW, IN_PROGRESS, DONE }

fun main() {
    println(Status.NEW.name)       // NEW
    println(Status.NEW.ordinal)    // 0
    println(Status.DONE.ordinal)   // 2
}

name обычно полезен для отладки и простого вывода. А вот ordinal звучит заманчиво (“о, бесплатный индекс!”), но это ловушка.

Представьте: вы сохраняли ordinal в файл как «код статуса», а потом решили переставить варианты:

enum class Status { NEW, DONE, IN_PROGRESS } // поменяли порядок

И всё: теперь у DONE другой ordinal. Значит, старые данные начнут читаться неправильно. Поэтому ordinal нельзя считать стабильным идентификатором, если порядок может меняться (а он почти всегда может — хотя бы потому, что вам захотелось «красивее»).

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

Все варианты enum: entries, values() и valueOf

Почти сразу возникает практический вопрос: «А как вывести пользователю список всех категорий?» или «Как перебрать все варианты?». У enum для этого есть синтетические (то есть автоматически сгенерированные компилятором) способы получить «все значения перечисления».

Современный Kotlin даёт свойство entries, которое возвращает специализированный список всех вариантов.

Пример:

enum class Category { FOOD, TRANSPORT, BOOKS }

fun main() {
    for (c in Category.entries) {
        println(c.name)
    }
    // FOOD
    // TRANSPORT
    // BOOKS
}

Раньше (и во многих старых примерах в интернете) вы увидите values() — он возвращает массив. Но сейчас entries считается более удачным: он не создаёт новый массив при каждом вызове и выглядит “по-коллекционному”. Kotlin официально отмечает, что до появления entries использовали values().

Ещё одна полезная штука — valueOf("NAME"), которая возвращает enum по строковому имени. Но у неё есть характер: если передать неизвестное имя, она бросит IllegalArgumentException. То есть valueOf — это «строгий разбор», хорошо работает, если вы уверены в данных.

Enum с параметрами

До этого момента enum выглядел как просто список слов. Но в реальном проекте часто нужно, чтобы у каждого варианта было “своё описание”, “код”, “человеческое имя”, “лимит” и т.д. И тут начинается приятная часть: enum может иметь параметры конструктора — почти как обычный класс.

Сделаем категории расходов «по-человечески»: с русским названием и, например, стабильным кодом.

enum class Category(val code: Int, val title: String) {
    FOOD(10, "Еда"),
    TRANSPORT(20, "Транспорт"),
    BOOKS(30, "Книги")
}

fun main() {
    println(Category.FOOD.title)   // Еда
    println(Category.FOOD.code)    // 10
}

Теперь вместо ordinal у нас есть code, и он стабилен: мы сами его задали. Хотите переставляйте варианты, хотите добавляйте новые — code не изменится сам собой. Это отличный подход, если вы планируете сохранять категорию куда-то вовне: в файл, в базу, отправлять по сети.

А title — это именно то, что вы будете показывать пользователю в интерфейсе или в отчёте, чтобы не выводить капсом TRANSPORT (хотя, конечно, если вам нравится стиль “кричащий бухгалтер”, то почему бы и нет).

Методы внутри enum

Данные — это хорошо, но иногда хочется, чтобы вариант перечисления умел делать что-то полезное. И это нормально: enum — это класс, а его значения — объекты этого класса. Значит, у enum могут быть методы, которые используют параметры (code, title, и т.д.).

Сделаем метод, который форматирует категорию для вывода:

enum class Category(val code: Int, val title: String) {
    FOOD(10, "Еда"),
    TRANSPORT(20, "Транспорт"),
    BOOKS(30, "Книги");

    fun label(): String = "$title (#$code)"
}

fun main() {
    println(Category.TRANSPORT.label()) // Транспорт (#20)
}

Обратите внимание на ; после списка значений. Это не декоративная точка с запятой «для солидности», а синтаксическое правило: если у enum есть тело (методы, свойства), то после последнего enum-значения нужен разделитель ;.

Почему так? Потому что иначе парсеру тяжело понять, где закончился список констант и начались члены класса. А Kotlin старается, чтобы ошибки компиляции были понятными, а не «что-то пошло не так в глубинах вселенной».

Запятые и ; в enum

На практике больше всего боли в enum — не в идее и не в логике, а в «почему оно не компилируется, я же просто добавил метод». Обычно ответ: «потому что забыли ;».

Правило можно запомнить так: пока вы просто перечисляете варианты — это “список”, разделённый запятыми. Как только вы хотите добавить что-то ещё (метод/свойство) — ставите ;, и дальше идёт «обычное тело класса».

Вот корректный шаблон:

enum class Category(val title: String) {
    FOOD("Еда"),
    TRANSPORT("Транспорт"),
    BOOKS("Книги");

    fun upperTitle(): String = title.uppercase()
}

И ещё маленький нюанс из той же области: запятая между enum-значениями обязательна (кроме последнего). А ; ставится после последнего значения, если ниже есть члены класса.

Если IntelliJ подсвечивает красным и говорит что-то вроде «Expecting member declaration» или «There must be a semicolon after the last enum entry» — почти наверняка вы попали именно сюда.

2. Пример: enum в консольном учёте расходов

Теперь давайте сделаем самое полезное: применим enum не “в вакууме”, а в нашем учебном консольном приложении. Представим, что у нас уже есть простая модель расхода (мы её делали раньше через data class) и где-то в программе храним список расходов. Раньше категория могла быть строкой, но мы хотим типобезопасность.

Вводим Category как enum

Начнём с базового: перечислим категории. Чтобы пользователю было удобно, дадим каждой категории “человеческое имя”.

enum class Category(val title: String) {
    FOOD("Еда"),
    TRANSPORT("Транспорт"),
    BOOKS("Книги")
}

Обновляем модель расхода

Теперь Expense хранит category: Category, а не String.

data class Expense(
    val amount: Int,
    val category: Category,
    val note: String
)

Безопасно разбираем категорию из ввода

Пользователь будет вводить что-то вроде food или FOOD. А Category.valueOf(...) требует точного имени и ещё может бросить исключение.

Поэтому для «пользовательского ввода» лучше сделать мягкий разбор: нормализуем строку и ищем среди entries.

Вот аккуратная функция:

fun parseCategoryOrNull(raw: String): Category? {
    val key = raw.trim().uppercase()
    return Category.entries.firstOrNull { it.name == key }
}

Здесь entries даёт нам список всех вариантов enum. Если пользователь ввёл что-то неизвестное — вернётся null, и дальше мы обработаем это в логике ввода (не падая).

Для наглядности можно представить мини-схему этого процесса:

flowchart TD
    A["Пользователь ввёл строку"] --> B["trim()"]
    B --> C["uppercase()"]
    C --> D["ищем среди Category.entries по name"]
    D -->|нашли| E["возвращаем Category"]
    D -->|не нашли| F["возвращаем null"]

Добавляем расход: прочитал → распарсил → создал объект

Соберём маленькую функцию добавления расхода. Пусть формат будет простой: сумма, категория, заметка (заметка — остаток строки).

fun addExpense(rawAmount: String, rawCategory: String, note: String): Expense? {
    val amount = rawAmount.trim().toIntOrNull() ?: return null
    val category = parseCategoryOrNull(rawCategory) ?: return null
    return Expense(amount = amount, category = category, note = note.trim())
}

И протестируем на мини-примере:

fun main() {
    val e = addExpense("120", "food", "обед") ?: return
    println(e) // Expense(amount=120, category=FOOD, note=обед)
}

Пока вывод у category будет FOOD, потому что это name. Но для пользователя мы будем показывать title:

fun main() {
    val e = addExpense("120", "food", "обед") ?: return
    println("${e.amount} ₣  — ${e.category.title} (${e.note})")
    // 120 ₣  — Еда (обед)
}

Подсказка пользователю: какие категории есть

Раз уж у нас enum — мы можем легко вывести список категорий:

fun printCategories() {
    val text = Category.entries.joinToString { it.name.lowercase() }
    println("Категории: $text")
    // Категории: food, transport, books
}

entries тут снова используется как «источник истины» по всем вариантам.

Такой подход хорош тем, что если вы добавите категорию HEALTH("Здоровье"), подсказка обновится автоматически (и вам не придётся вспоминать, где вы руками печатали список строк).

3. Типичные ошибки при работе с enum class

Ошибка №1: продолжать хранить варианты строками “потому что так быстрее”.
Иногда кажется, что String проще: прочитал, сравнил, готово. Но как только появляется отчёт, фильтрация, статистика и хоть один пользователь, который вводит food с пробелом — вы начнёте тратить время на “нормализацию строк” в десятке мест. Enum переносит проблему в одно место: разбор ввода. А внутри программы значения становятся строгими и одинаковыми.

Ошибка №2: использовать ordinal как стабильный идентификатор.
ordinal зависит от порядка объявления. Сегодня FOOD имеет ordinal = 0, завтра вы переставили варианты — и всё сломалось тихо, без предупреждений. ordinal годится для очень локальных вещей (например, “вывести позицию в списке прямо сейчас”), но не для хранения и не для протоколов. Если нужен стабильный код — добавляйте своё поле, например code.

Ошибка №3: забыть ; перед методами и свойствами enum.
Это классическая ситуация: вы объявили варианты, потом добавили метод — и компилятор внезапно начал ругаться на ровном месте. Причина проста: если у enum есть члены класса, список значений должен быть отделён ;. Это правило Kotlin для enum, и оно действительно обязательно.

Ошибка №4: разбирать пользовательский ввод через valueOf() без защиты.
valueOf("FOOD") удобен, но если строка не совпала — он бросит IllegalArgumentException. Для “данных извне” (ввод пользователя) чаще нужен безопасный разбор: через entries.firstOrNull { ... }, чтобы получить null и спокойно показать понятное сообщение, а не падать исключением.

Ошибка №5: смешивать “человеческое имя” и name enum-значения.
name — это техническое имя константы (FOOD), оно отлично подходит для кода и отладки, но часто выглядит не очень дружелюбно для пользователя. Если вы хотите красивый вывод, добавляйте поле вроде title: String и показывайте его. Тогда вы не привязываете интерфейс к техническим именам и сможете менять отображение без переписывания логики.

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