Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user