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 і показуйте його. Тоді ви не привʼязуєте інтерфейс до технічних імен і зможете змінювати відображення без переписування логіки.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ