Enums ที่มีประสิทธิภาพใน Kotlin ด้วยการค้นหาแบบย้อนกลับ?


105

ฉันพยายามหาวิธีที่ดีที่สุดในการ 'ค้นหาแบบย้อนกลับ' ใน enum ใน Kotlin ข้อได้เปรียบอย่างหนึ่งของฉันจาก Effective Java คือคุณแนะนำแผนที่แบบคงที่ภายใน enum เพื่อจัดการการค้นหาแบบย้อนกลับ การย้ายสิ่งนี้ไปยัง Kotlin ด้วย enum อย่างง่ายทำให้ฉันไปสู่รหัสที่มีลักษณะดังนี้:

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]
        }
    }
}

คำถามของฉันคือวิธีนี้เป็นวิธีที่ดีที่สุดหรือมีวิธีที่ดีกว่านี้ จะเกิดอะไรขึ้นถ้าฉันมี enums หลายตัวที่มีรูปแบบคล้ายกัน? มีวิธีใน Kotlin ที่จะทำให้รหัสนี้สามารถใช้งานได้อีกครั้งใน enums หรือไม่?


Enum ของคุณควรใช้อินเทอร์เฟซ Identifiable กับคุณสมบัติ id และอ็อบเจ็กต์ที่แสดงร่วมควรขยายคลาสนามธรรม GettableById ซึ่งเก็บแผนที่ idToEnumValue และส่งคืนค่า enum ตาม id รายละเอียดอยู่ด้านล่างในคำตอบของฉัน
Eldar Agalarov

คำตอบ:


178

ประการแรกอาร์กิวเมนต์fromInt()ควรเป็น an Intไม่ใช่Int?. การพยายามหาTypeค่าว่างโดยใช้จะนำไปสู่โมฆะอย่างเห็นได้ชัดและผู้โทรไม่ควรลองทำเช่นนั้นด้วยซ้ำ Mapยังมีเหตุผลที่จะไม่แน่นอนไม่มี รหัสสามารถลดลงเป็น:

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

รหัสนั้นสั้นมากตรงไปตรงมาฉันไม่แน่ใจว่าคุ้มค่าที่จะพยายามหาวิธีแก้ปัญหาที่ใช้ซ้ำได้


8
ฉันกำลังจะแนะนำเหมือนกัน นอกจากนี้ผมจะทำให้fromIntผลตอบแทนที่ไม่ใช่ null เช่นEnum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26

4
ด้วยการสนับสนุน kotlin สำหรับ null-safety การคืนค่า null จากวิธีการจะไม่รบกวนฉันเหมือนใน Java: ผู้เรียกจะถูกบังคับโดยคอมไพเลอร์ให้จัดการกับค่าที่ส่งคืนเป็นโมฆะและตัดสินใจว่าจะทำอย่างไร (โยนหรือทำ อื่น ๆ อีก).
JB Nizet

1
@Raphael เพราะ enums ถูกนำมาใช้ใน Java 5 และเป็นทางเลือกใน Java 8
JB Nizet

2
รุ่นของฉันในการใช้รหัสนี้by lazy{}สำหรับmapและgetOrDefault()สำหรับการเข้าถึงที่ปลอดภัยโดยvalue
Hoang Tran

2
วิธีนี้ใช้ได้ผลดี โปรดทราบว่าจะสามารถที่จะเรียกType.fromInt()จากรหัส Java @JvmStaticคุณจะต้องใส่คำอธิบายประกอบกับวิธีการ
Arto Bendiken

35

เราสามารถใช้findซึ่งส่งคืนองค์ประกอบแรกที่ตรงกับเพรดิเคตที่กำหนดหรือเป็นค่าว่างหากไม่พบองค์ประกอบดังกล่าว

companion object {
   fun valueOf(value: Int): Type? = Type.values().find { it.value == value }
}

4
การเพิ่มประสิทธิภาพที่เห็นได้ชัดจะใช้first { ... }แทนเนื่องจากไม่มีการใช้สำหรับผลลัพธ์หลายรายการ
creativecreatorormaybenot

9
ไม่มีการใช้firstไม่ได้เพิ่มประสิทธิภาพในขณะที่มันเปลี่ยนแปลงพฤติกรรมและพ่นNoSuchElementExceptionถ้ารายการไม่พบที่findซึ่งเท่ากับผลตอบแทนfirstOrNull nullดังนั้นหากคุณต้องการโยนแทนที่จะกลับมาใช้โมฆะfirst
61

วิธีนี้สามารถใช้ได้กับ enums ที่มีค่าหลายค่า: fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } นอกจากนี้คุณยังสามารถยกเว้นได้หากค่าไม่อยู่ใน enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") หรือคุณสามารถใช้เมื่อเรียกวิธีนี้: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth

วิธีการของคุณมีความซับซ้อนเชิงเส้น O (n) ดีกว่าที่จะใช้การค้นหาใน HashMap ที่กำหนดไว้ล่วงหน้าที่มีความซับซ้อน O (1)
Eldar Agalarov

ใช่ฉันรู้ แต่ในกรณีส่วนใหญ่ enum จะมีสถานะจำนวนน้อยมากดังนั้นจึงไม่สำคัญว่าจะเป็นอย่างไรมีอะไรอ่านได้มากกว่า
ฮัมเพลง

27

มันไม่ค่อยสมเหตุสมผลในกรณีนี้ แต่นี่คือ "การแยกตรรกะ" สำหรับโซลูชันของ @ 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

โดยทั่วไปนั่นคือสิ่งที่เกี่ยวกับอ็อบเจ็กต์ที่แสดงร่วมซึ่งสามารถนำมาใช้ซ้ำได้ (ไม่เหมือนกับสมาชิกแบบคงที่ในคลาส Java)


ทำไมคุณถึงใช้คลาสเปิด เพียงแค่ทำให้เป็นนามธรรม
Eldar Agalarov

21

อีกทางเลือกหนึ่งที่อาจถือได้ว่าเป็น "สำนวน" มากกว่ามีดังต่อไปนี้:

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

ซึ่งสามารถนำมาใช้เช่นType[type].


สำนวนกว่านี้แน่นอน! ไชโย
AleksandrH

6

ฉันพบว่าตัวเองทำการค้นหาแบบย้อนกลับด้วยการกำหนดเองโค้ดด้วยมือค่าสองสามครั้งและใช้แนวทางต่อไปนี้

ทำให้enumใช้อินเทอร์เฟซที่ใช้ร่วมกัน:

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
}

อินเทอร์เฟซนี้(แต่ชื่อแปลก :))ทำเครื่องหมายค่าหนึ่งเป็นรหัสที่ชัดเจน เป้าหมายคือสามารถเขียน:

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

ซึ่งสามารถทำได้อย่างง่ายดายด้วยรหัสต่อไปนี้:

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)

3
นั่นเป็นงานจำนวนมากสำหรับการใช้งานที่เรียบง่ายเช่นนี้คำตอบที่ได้รับการยอมรับคือ IMO ที่สะอาดกว่ามาก
Connor Wyatt

2
เห็นด้วยอย่างยิ่งสำหรับการใช้งานที่เรียบง่ายมันดีกว่าแน่นอน ฉันมีรหัสข้างต้นเพื่อจัดการกับชื่อที่ชัดเจนสำหรับสมาชิกที่แจกแจง
miensol

รหัสของคุณใช้การสะท้อน (ไม่ดี) และป่อง (ไม่ดีด้วย)
Eldar Agalarov

1

ตัวแปรของข้อเสนอก่อนหน้านี้อาจเป็นดังต่อไปนี้โดยใช้ฟิลด์ลำดับและ 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)
    }
}

}


1

การใช้งานตัวอย่างอื่น นอกจากนี้ยังตั้งค่าเริ่มต้น (ที่นี่เป็นOPEN) หากไม่มีอินพุตที่ตรงกับอ็อพชัน 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
}

}


0

มาพร้อมกับโซลูชันทั่วไปมากขึ้น

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

ตัวอย่างการใช้งาน:

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

0

วิธี Kotlin สำนวนที่แท้จริง ไม่มีรหัสสะท้อนป่อง:

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])
}

0

แนวทางเพิ่มเติมเล็กน้อยของโซลูชันที่ยอมรับโดยมีการตรวจสอบค่าว่างและเรียกใช้ฟังก์ชัน

fun main(args: Array<String>) {
    val a = Type.A // find by name
    val anotherA = Type.valueOf("A") // find by name with Enums default valueOf
    val aLikeAClass = Type(3) // find by value using invoke - looks like object creation

    val againA = Type.of(3) // find by value
    val notPossible = Type.of(6) // can result in null
    val notPossibleButThrowsError = Type.ofNullSave(6) // can result in IllegalArgumentException

    // prints: A, A, 0, 3
    println("$a, ${a.name}, ${a.ordinal}, ${a.value}")
    // prints: A, A, A null, java.lang.IllegalArgumentException: No enum constant Type with value 6
    println("$anotherA, $againA, $aLikeAClass $notPossible, $notPossibleButThrowsError")
}

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

    companion object {
        private val map = values().associateBy(Type::value)
        operator fun invoke(type: Int) = ofNullSave(type)
        fun of(type: Int) = map[type]
        fun ofNullSave(type: Int) = map[type] ?: IllegalArgumentException("No enum constant Type with value $type")
    }
}

-1

วาล t = Type.values ​​() [ลำดับ]

:)


สิ่งนี้ใช้ได้กับค่าคงที่ 0, 1, ... , N ถ้าคุณมีค่าเช่น 100, 50, 35 ก็จะไม่ได้ผลลัพธ์ที่ถูกต้อง
CoolMind
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.