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

12 KiB
Raw Blame History

title, description, tags
title description tags
Enum class Перечисления со свойствами и методами, entries, when по enum и его границы
kotlin
oop

enum class - класс с фиксированным набором экземпляров.

enum class Status { ACTIVE, BLOCKED, DELETED }

fun main() {
    val s = Status.ACTIVE
    println(s)            // ACTIVE
    println(s.name)       // ACTIVE — строка
    println(s.ordinal)    // 0 — позиция в объявлении
}

Свойства и конструктор

Каждая константа - вызов конструктора:

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 по самой себе:

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
}

Каждая такая константа компилируется в анонимный подкласс.

Реализация интерфейса

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

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)
аллокация новый массив при каждом вызове один неизменяемый список
изменяемость элементы можно подменить нельзя
enum class Status { ACTIVE, BLOCKED }

fun main() {
    @Suppress("DEPRECATION")
    println(Status.values().contentToString())    // [ACTIVE, BLOCKED]
    println(Status.entries)                        // [ACTIVE, BLOCKED]
}

В новом коде всегда entries.

Безопасный разбор строки:

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 не нужен:

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 защищает.

Импорт констант сокращает запись:

enum class Status { ACTIVE, BLOCKED }

fun message(s: Status): String {
    return when (s) {
        Status.ACTIVE -> "работает"
        Status.BLOCKED -> "заблокирован"
    }
}

fun main() = println(message(Status.ACTIVE))

Сравнение и порядок

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 - массив вместо хеш-таблицы:

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

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

Когда enum, а когда sealed

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

[!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

[!warning] Подводные камни ordinal нельзя сохранять в БД или в файл. Он меняется при вставке константы в середину:

enum class Status { ACTIVE, BLOCKED }           // BLOCKED.ordinal == 1
// добавили NEW в середину:
// enum class Status { ACTIVE, NEW, BLOCKED }   // BLOCKED.ordinal == 2

Все сохранённые единицы станут означать NEW. Для сериализации используйте name или свой стабильный code.

name тоже хрупок при переименовании - но его хотя бы нельзя изменить случайно. Самый надёжный вариант - явное свойство:

enum class Status(val code: String) {
    ACTIVE("active"), BLOCKED("blocked")
}

fun main() { println(Status.ACTIVE.code) }   // active

valueOf бросает исключение на незнакомой строке, а не возвращает null. Для внешних данных нужна своя обёртка через entries.find.

values() аллоцирует массив на каждый вызов:

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 с изменяемым состоянием - глобальная переменная:

enum class Counter {
    INSTANCE;
    var count = 0        // общее состояние на всю программу
}

fun main() {
    Counter.INSTANCE.count++
    println(Counter.INSTANCE.count)   // 1 — и так во всех потоках
}

Константы enum - синглтоны, поэтому var в них небезопасен при многопоточности.

when по nullable enum всё равно требует else или ветки null:

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 class и interface