Enum yang efektif di Kotlin dengan pencarian terbalik?

103

Saya mencoba menemukan cara terbaik untuk melakukan 'pencarian terbalik' pada enum di Kotlin. Salah satu kesimpulan saya dari Java Efektif adalah Anda memperkenalkan peta statis di dalam enum untuk menangani pencarian terbalik. Mem-porting ini ke Kotlin dengan enum sederhana mengarahkan saya ke kode yang terlihat seperti ini:

enum class Type(val value: Int) {
    A(1),
    B(2),
    C(3);

    companion object {
        val map: MutableMap<Int, Type> = HashMap()

        init {
            for (i in Type.values()) {
                map[i.value] = i
            } 
        }

        fun fromInt(type: Int?): Type? {
            return map[type]
        }
    }
}

Pertanyaan saya adalah, apakah ini cara terbaik untuk melakukan ini, atau ada cara yang lebih baik? Bagaimana jika saya memiliki beberapa enum yang mengikuti pola serupa? Apakah ada cara di Kotlin untuk membuat kode ini lebih dapat digunakan kembali di seluruh enum?

Baron
sumber
Enum Anda harus mengimplementasikan antarmuka Identifiable dengan properti id dan objek pendamping harus memperluas kelas abstrak GettableById yang menampung peta idToEnumValue dan mengembalikan nilai enum berdasarkan id. Detail ada di bawah di jawaban saya.
Eldar Agalarov

Jawaban:

177

Pertama-tama, argumen dari fromInt()haruslah an Int, bukan Int?. Mencoba mendapatkan Typemenggunakan null jelas akan menghasilkan null, dan penelepon bahkan tidak boleh mencoba melakukan itu. Itu Mapjuga tidak memiliki alasan untuk bisa berubah. Kode dapat dikurangi menjadi:

companion object {
    private val map = Type.values().associateBy(Type::value)
    fun fromInt(type: Int) = map[type]
}

Kode itu sangat pendek sehingga, sejujurnya, saya tidak yakin apakah perlu mencoba menemukan solusi yang dapat digunakan kembali.

JB Nizet
sumber
8
Saya akan merekomendasikan hal yang sama. Selain itu, saya akan membuat fromIntpengembalian non-null seperti Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26
4
Mengingat dukungan kotlin untuk null-safety, mengembalikan null dari metode tidak akan mengganggu saya seperti di Java: pemanggil akan dipaksa oleh kompiler untuk menangani nilai yang dikembalikan null, dan memutuskan apa yang harus dilakukan (lempar atau lakukan sesuatu yang lain).
JB Nizet
1
@Raphael karena enum diperkenalkan di Java 5 dan Opsional di Java 8.
JB Nizet
2
versi saya dari kode ini digunakan by lazy{}untuk mapdan getOrDefault()untuk akses yang lebih aman olehvalue
Hoang Tran
2
Solusi ini bekerja dengan baik. Perhatikan bahwa untuk dapat memanggil Type.fromInt()dari kode Java, Anda perlu memberi anotasi pada metode @JvmStatic.
Arto Bendiken
35

kita bisa menggunakan findyang Mengembalikan elemen pertama yang cocok dengan predikat yang diberikan, atau null jika tidak ada elemen yang ditemukan.

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}
humazed
sumber
4
Peningkatan yang jelas digunakan first { ... }karena tidak ada gunanya untuk banyak hasil.
creativecreatorormaybenot
9
Tidak, menggunakan firstbukanlah peningkatan karena mengubah perilaku dan melempar NoSuchElementExceptionjika item tidak ditemukan di mana findyang sama dengan firstOrNullpengembalian null. jadi jika Anda ingin membuang alih-alih mengembalikan penggunaan nolfirst
humazed
Metode ini dapat digunakan dengan enum dengan beberapa nilai: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } Anda juga dapat membuat pengecualian jika nilainya tidak ada dalam enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") atau Anda dapat menggunakannya saat memanggil metode ini: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth
Metode Anda memiliki kompleksitas linier O (n). Lebih baik menggunakan pencarian di HashMap yang telah ditentukan dengan kompleksitas O (1).
Eldar Agalarov
ya, saya tahu, tetapi dalam banyak kasus, enum akan memiliki jumlah status yang sangat kecil sehingga tidak masalah, mana yang lebih mudah dibaca.
humazed
27

Tidak masuk akal dalam kasus ini, tetapi berikut adalah "ekstraksi logika" untuk solusi @ JBNized:

open class EnumCompanion<T, V>(private val valueMap: Map<T, V>) {
    fun fromInt(type: T) = valueMap[type]
}

enum class TT(val x: Int) {
    A(10),
    B(20),
    C(30);

    companion object : EnumCompanion<Int, TT>(TT.values().associateBy(TT::x))
}

//sorry I had to rename things for sanity

Secara umum, itulah hal tentang objek pendamping yang dapat digunakan kembali (tidak seperti anggota statis di kelas Java)

voddan
sumber
Mengapa Anda menggunakan kelas terbuka? Buat saja menjadi abstrak.
Eldar Agalarov
21

Opsi lain, yang dapat dianggap lebih "idiomatis", adalah sebagai berikut:

companion object {
    private val map = Type.values().associateBy(Type::value)
    operator fun get(value: Int) = map[value]
}

Yang kemudian bisa digunakan seperti Type[type].

Ivan Plantevin
sumber
Jelas lebih idiomatis! Bersulang.
AleksandrH
6

Saya menemukan diri saya melakukan pencarian terbalik dengan kustom, kode tangan, nilai beberapa kali dan datang dengan pendekatan berikut.

Make enums mengimplementasikan antarmuka bersama:

interface Codified<out T : Serializable> {
    val code: T
}

enum class Alphabet(val value: Int) : Codified<Int> {
    A(1),
    B(2),
    C(3);

    override val code = value
}

Antarmuka ini (betapapun aneh namanya :)) menandai nilai tertentu sebagai kode eksplisit. Tujuannya agar bisa menulis:

val a = Alphabet::class.decode(1) //Alphabet.A
val d = Alphabet::class.tryDecode(4) //null

Yang dapat dengan mudah dicapai dengan kode berikut:

interface Codified<out T : Serializable> {
    val code: T

    object Enums {
        private val enumCodesByClass = ConcurrentHashMap<Class<*>, Map<Serializable, Enum<*>>>()

        inline fun <reified T, TCode : Serializable> decode(code: TCode): T where T : Codified<TCode>, T : Enum<*> {
            return decode(T::class.java, code)
        }

        fun <T, TCode : Serializable> decode(enumClass: Class<T>, code: TCode): T where T : Codified<TCode> {
            return tryDecode(enumClass, code) ?: throw IllegalArgumentException("No $enumClass value with code == $code")
        }

        inline fun <reified T, TCode : Serializable> tryDecode(code: TCode): T? where T : Codified<TCode> {
            return tryDecode(T::class.java, code)
        }

        @Suppress("UNCHECKED_CAST")
        fun <T, TCode : Serializable> tryDecode(enumClass: Class<T>, code: TCode): T? where T : Codified<TCode> {
            val valuesForEnumClass = enumCodesByClass.getOrPut(enumClass as Class<Enum<*>>, {
                enumClass.enumConstants.associateBy { (it as T).code }
            })

            return valuesForEnumClass[code] as T?
        }
    }
}

fun <T, TCode> KClass<T>.decode(code: TCode): T
        where T : Codified<TCode>, T : Enum<T>, TCode : Serializable 
        = Codified.Enums.decode(java, code)

fun <T, TCode> KClass<T>.tryDecode(code: TCode): T?
        where T : Codified<TCode>, T : Enum<T>, TCode : Serializable
        = Codified.Enums.tryDecode(java, code)
miensol
sumber
3
Itu banyak pekerjaan untuk operasi sederhana seperti itu, jawaban yang diterima jauh lebih bersih IMO
Connor Wyatt
2
Sepenuhnya setuju untuk penggunaan sederhana itu pasti lebih baik. Saya sudah memiliki kode di atas untuk menangani nama eksplisit untuk anggota yang dicacah.
miensol
Kode Anda menggunakan refleksi (buruk) dan membengkak (buruk juga).
Eldar Agalarov
1

Varian dari beberapa proposal sebelumnya mungkin adalah sebagai berikut, menggunakan ordinal field dan getValue:

enum class Type {
A, B, C;

companion object {
    private val map = values().associateBy(Type::ordinal)

    fun fromInt(number: Int): Type {
        require(number in 0 until map.size) { "number out of bounds (must be positive or zero & inferior to map.size)." }
        return map.getValue(number)
    }
}

}

mengiris
sumber
1

Contoh implementasi lainnya. Ini juga menyetel nilai default (di sini untuk OPEN) jika tidak ada input yang cocok dengan opsi enum:

enum class Status(val status: Int) {
OPEN(1),
CLOSED(2);

companion object {
    @JvmStatic
    fun fromInt(status: Int): Status =
        values().find { value -> value.status == status } ?: OPEN
}

}

Tormod Haugene
sumber
0

Hadir dengan solusi yang lebih umum

inline fun <reified T : Enum<*>> findEnumConstantFromProperty(predicate: (T) -> Boolean): T? =
T::class.java.enumConstants?.find(predicate)

Contoh penggunaan:

findEnumConstantFromProperty<Type> { it.value == 1 } // Equals Type.A
Shalbert
sumber
0

Cara Kotlin Idiomatik Sejati. Tanpa kode refleksi kembung:

interface Identifiable<T : Number> {

    val id: T
}

abstract class GettableById<T, R>(values: Array<R>) where T : Number, R : Enum<R>, R : Identifiable<T> {

    private val idToValue: Map<T, R> = values.associateBy { it.id }

    operator fun get(id: T): R = getById(id)

    fun getById(id: T): R = idToValue.getValue(id)
}

enum class DataType(override val id: Short): Identifiable<Short> {

    INT(1), FLOAT(2), STRING(3);

    companion object: GettableById<Short, DataType>(values())
}

fun main() {
    println(DataType.getById(1))
    // or
    println(DataType[2])
}
Eldar Agalarov
sumber
-1

val t = Type.values ​​() [ordinal]

:)

shmulik.r
sumber
Ini bekerja untuk konstanta 0, 1, ..., N. Jika Anda memiliki konstanta seperti 100, 50, 35, maka itu tidak akan memberikan hasil yang benar.
CoolMind