Files
obsidian/Kotlin/Синтаксис и типы/Базовые типы ООП/Enum class.md
T

299 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Enum class
description: Перечисления со свойствами и методами, entries, when по enum и его границы
tags:
- kotlin
- oop
---
`enum class` - класс с фиксированным набором экземпляров.
```kotlin
enum class Status { ACTIVE, BLOCKED, DELETED }
fun main() {
val s = Status.ACTIVE
println(s) // ACTIVE
println(s.name) // ACTIVE — строка
println(s.ordinal) // 0 — позиция в объявлении
}
```
## Свойства и конструктор
Каждая константа - вызов конструктора:
```kotlin
enum class Planet(val radiusKm: Int, val hasRings: Boolean) {
EARTH(6371, false),
SATURN(58232, true),
MARS(3390, false);
val radiusM: Int get() = radiusKm * 1000
fun describe() = "$name: радиус $radiusKm км, кольца=$hasRings"
}
fun main() {
println(Planet.SATURN.describe()) // SATURN: радиус 58232 км, кольца=true
println(Planet.EARTH.radiusM) // 6371000
}
```
Точка с запятой после последней константы обязательна, если дальше идёт тело класса.
## Методы в константах
Константа может переопределить метод - это заменяет `when` по самой себе:
```kotlin
enum class Operation {
PLUS { override fun apply(a: Int, b: Int) = a + b },
MINUS { override fun apply(a: Int, b: Int) = a - b },
TIMES { override fun apply(a: Int, b: Int) = a * b };
abstract fun apply(a: Int, b: Int): Int
}
fun main() {
println(Operation.PLUS.apply(2, 3)) // 5
println(Operation.TIMES.apply(2, 3)) // 6
println(Operation.entries.joinToString { "${it.name}=${it.apply(6, 3)}" })
// PLUS=9, MINUS=3, TIMES=18
}
```
Каждая такая константа компилируется в анонимный подкласс.
## Реализация интерфейса
```kotlin
interface Describable { fun describe(): String }
enum class Level(val weight: Int) : Describable {
LOW(1), HIGH(10);
override fun describe() = "$name (вес $weight)"
}
fun main() {
val items: List<Describable> = Level.entries
println(items.joinToString { it.describe() }) // LOW (вес 1), HIGH (вес 10)
}
```
## entries, values, valueOf
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun main() {
println(Status.entries) // [ACTIVE, BLOCKED] — список
println(Status.entries.size) // 2
println(Status.valueOf("ACTIVE")) // ACTIVE
println(Status.entries.first().name) // ACTIVE
// println(Status.valueOf("НЕТ")) // IllegalArgumentException
}
```
`entries` (Kotlin 1.9+) заменяет `values()`. Разница практическая:
| | `values()` | `entries` |
|---|---|---|
| тип | `Array<Status>` | `EnumEntries<Status>` (это `List`) |
| аллокация | **новый массив при каждом вызове** | один неизменяемый список |
| изменяемость | элементы можно подменить | нельзя |
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun main() {
@Suppress("DEPRECATION")
println(Status.values().contentToString()) // [ACTIVE, BLOCKED]
println(Status.entries) // [ACTIVE, BLOCKED]
}
```
В новом коде всегда `entries`.
Безопасный разбор строки:
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun parseStatus(s: String): Status? =
Status.entries.find { it.name == s }
fun main() {
println(parseStatus("ACTIVE")) // ACTIVE
println(parseStatus("нет")) // null — без исключения
}
```
Через `enumValueOf` то же самое, но с исключением. Для внешних данных (JSON, БД, параметры запроса) нужна версия с `null`.
## when по enum
Главное применение. Компилятор проверяет полноту, и `else` не нужен:
```kotlin
enum class Status { ACTIVE, BLOCKED, DELETED }
fun message(s: Status): String = when (s) {
Status.ACTIVE -> "работает"
Status.BLOCKED -> "заблокирован"
Status.DELETED -> "удалён"
}
fun main() {
println(message(Status.BLOCKED)) // заблокирован
}
```
Если добавить константу `ARCHIVED`, этот `when` перестанет компилироваться - компилятор найдёт все места, которые нужно обновить. Поэтому `else` в `when` по enum - вредная привычка: он скрывает именно те ошибки, от которых enum защищает.
Импорт констант сокращает запись:
```kotlin
enum class Status { ACTIVE, BLOCKED }
fun message(s: Status): String {
return when (s) {
Status.ACTIVE -> "работает"
Status.BLOCKED -> "заблокирован"
}
}
fun main() = println(message(Status.ACTIVE))
```
## Сравнение и порядок
```kotlin
enum class Level { LOW, MEDIUM, HIGH }
fun main() {
println(Level.LOW == Level.LOW) // true
println(Level.LOW < Level.HIGH) // true — по ordinal
println(Level.entries.sorted()) // [LOW, MEDIUM, HIGH]
println(Level.HIGH.ordinal) // 2
println(Level.entries.maxOf { it.ordinal }) // 2
}
```
`enum` реализует `Comparable` по `ordinal`. Сравнивать через `==` безопасно: экземпляр единственный, поэтому `==` и `===` дают одно и то же.
## EnumMap и EnumSet
Специализированные коллекции для enum - массив вместо хеш-таблицы:
```kotlin
import java.util.EnumMap
import java.util.EnumSet
enum class Day { MON, TUE, WED }
fun main() {
val hours = EnumMap<Day, Int>(Day::class.java)
hours[Day.MON] = 8
hours[Day.TUE] = 6
println(hours) // {MON=8, TUE=6}
val workdays = EnumSet.of(Day.MON, Day.TUE)
println(workdays.contains(Day.WED)) // false
println(EnumSet.complementOf(workdays)) // [WED]
}
```
Это классы JDK; в stdlib Kotlin аналогов нет. Для небольших enum разница с `HashMap` несущественна.
## companion object
```kotlin
enum class Status(val code: Int) {
ACTIVE(1), BLOCKED(2);
companion object {
private val byCode = entries.associateBy { it.code }
fun fromCode(code: Int): Status? = byCode[code]
}
}
fun main() {
println(Status.fromCode(2)) // BLOCKED
println(Status.fromCode(99)) // null
}
```
Поиск по коду через заранее построенную `Map` - стандартный приём. Подробнее: [Object и companion object](Object%20и%20companion%20object.md)
## Когда enum, а когда sealed
```kotlin
enum class Status { OK, ERROR } // варианты без данных
sealed interface Result2
data class Ok(val value: Int) : Result2 // варианты с разными данными
data class Error(val message: String) : Result2
```
`enum` - фиксированный набор **значений**, у всех одинаковая структура. `sealed` - фиксированный набор **типов**, у каждого свои свойства. Подробнее: [Sealed class и interface](Sealed%20class%20и%20interface.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `enum Status { ACTIVE }` | `enum class Status { ACTIVE }` |
> | `Status.values()` | `Status.entries` |
> | `Status.valueOf(s)` | `Status.valueOf(s)` или `entries.find` |
> | `switch` без проверки полноты до Java 21 | `when` проверяет всегда |
> | `EnumMap`/`EnumSet` | то же (из JDK) |
>
> Устройство одинаковое - на JVM это тот же `java.lang.Enum`. Два практических различия:
>
> **`entries` вместо `values()`.** Java-метод `values()` возвращает новый массив при каждом вызове, потому что массив изменяем и вернуть общий нельзя. `entries` возвращает неизменяемый список, поэтому экземпляр один. В цикле это заметная разница.
>
> **Проверка полноты.** В Java `switch` по enum без `default` долго не проверялся: добавление константы молча менялo поведение. Kotlin проверяет `when`-выражение с момента появления языка, а с Kotlin 2.0 - и `when`-инструкцию.
>
> Сравнить: [Enum в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Enum.md)
> [!warning] Подводные камни
> **`ordinal` нельзя сохранять в БД или в файл.** Он меняется при вставке константы в середину:
> ```kotlin
> enum class Status { ACTIVE, BLOCKED } // BLOCKED.ordinal == 1
> // добавили NEW в середину:
> // enum class Status { ACTIVE, NEW, BLOCKED } // BLOCKED.ordinal == 2
> ```
> Все сохранённые единицы станут означать `NEW`. Для сериализации используйте `name` или свой стабильный `code`.
>
> **`name` тоже хрупок при переименовании** - но его хотя бы нельзя изменить случайно. Самый надёжный вариант - явное свойство:
> ```kotlin
> enum class Status(val code: String) {
> ACTIVE("active"), BLOCKED("blocked")
> }
>
> fun main() { println(Status.ACTIVE.code) } // active
> ```
>
> **`valueOf` бросает исключение на незнакомой строке**, а не возвращает `null`. Для внешних данных нужна своя обёртка через `entries.find`.
>
> **`values()` аллоцирует массив на каждый вызов:**
> ```kotlin
> enum class Status { ACTIVE }
>
> fun main() {
> @Suppress("DEPRECATION")
> repeat(3) { Status.values() } // три разных массива
> repeat(3) { Status.entries } // один и тот же список
> println("ok")
> }
> ```
>
> **`else` в `when` по enum отключает проверку полноты.** Самая частая ошибка: добавили константу, компилятор промолчал, в рантайме сработала ветка `else`.
>
> **Enum нельзя наследовать и расширять.** Если набор вариантов должен быть расширяемым (например, плагинами), enum не подходит - нужен `sealed interface` или обычная иерархия.
>
> **Enum с изменяемым состоянием - глобальная переменная:**
> ```kotlin
> enum class Counter {
> INSTANCE;
> var count = 0 // общее состояние на всю программу
> }
>
> fun main() {
> Counter.INSTANCE.count++
> println(Counter.INSTANCE.count) // 1 — и так во всех потоках
> }
> ```
> Константы enum - синглтоны, поэтому `var` в них небезопасен при многопоточности.
>
> **`when` по nullable enum всё равно требует `else` или ветки `null`:**
> ```kotlin
> enum class Status { ACTIVE, BLOCKED }
>
> fun message(s: Status?): String = when (s) {
> Status.ACTIVE -> "работает"
> Status.BLOCKED -> "заблокирован"
> null -> "не задан"
> }
>
> fun main() { println(message(null)) } // не задан
> ```
Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md)
Далее: [Sealed class и interface](Sealed%20class%20и%20interface.md)