Enums hiệu quả trong Kotlin với tra cứu ngược?


102

Tôi đang cố gắng tìm cách tốt nhất để thực hiện 'tra cứu ngược' trên một enum trong Kotlin. Một trong những điểm rút ra của tôi từ Java hiệu quả là bạn giới thiệu một bản đồ tĩnh bên trong enum để xử lý tra cứu ngược. Việc chuyển cái này sang Kotlin với một enum đơn giản sẽ dẫn tôi đến đoạn mã trông giống như sau:

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

Câu hỏi của tôi là, đây là cách tốt nhất để làm điều này, hay có cách nào tốt hơn? Điều gì sẽ xảy ra nếu tôi có một số enum theo một mẫu tương tự? Có cách nào trong Kotlin để làm cho mã này có thể tái sử dụng nhiều hơn trên các enums không?


Enum của bạn nên triển khai giao diện Nhận dạng với thuộc tính id và đối tượng đồng hành nên mở rộng lớp trừu tượng GettableById chứa bản đồ idToEnumValue và trả về giá trị enum dựa trên id. Chi tiết dưới đây trong câu trả lời của tôi.
Eldar Agalarov

Câu trả lời:


176

Trước hết, đối số của fromInt()phải là an Int, không phải là an Int?. Cố gắng lấy Typenull bằng cách sử dụng rõ ràng sẽ dẫn đến null và người gọi thậm chí không nên thử làm điều đó. Các Mapcũng không có lý do gì để có thể thay đổi. Mã có thể được giảm thành:

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

Đoạn mã đó ngắn đến nỗi, thành thật mà nói, tôi không chắc nó đáng để thử tìm một giải pháp có thể tái sử dụng.


8
Tôi đã giới thiệu như vậy. Ngoài ra, tôi sẽ fromInttrả về không phải là null như Enum.valueOf(String):map[type] ?: throw IllegalArgumentException()
mfulton26

4
Với sự hỗ trợ kotlin cho null-an toàn, việc trả về null từ phương thức sẽ không làm phiền tôi như trong Java: người gọi sẽ bị trình biên dịch buộc phải xử lý với giá trị trả về null và quyết định phải làm gì (ném hoặc làm thứ gì khác).
JB Nizet

1
@Raphael vì enums đã được giới thiệu trong Java 5 và bắt buộc trong Java 8.
JB Nizet

2
phiên bản mã này của tôi sử dụng by lazy{}để truy cập mapgetOrDefault()an toàn hơn bởivalue
Hoang Tran

2
Giải pháp này hoạt động tốt. Lưu ý rằng để có thể gọi Type.fromInt()từ mã Java, bạn sẽ cần phải chú thích phương thức với @JvmStatic.
Arto Bendiken

34

chúng ta có thể sử dụng findcái nào Trả về phần tử đầu tiên phù hợp với vị từ đã cho hoặc null nếu không tìm thấy phần tử như vậy.

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

4
Một cải tiến rõ ràng là sử dụng first { ... }thay thế vì không sử dụng cho nhiều kết quả.
creativecreatorormaybenot

9
Không, việc sử dụng firstkhông phải là một sự cải tiến vì nó thay đổi hành vi và ném NoSuchElementExceptionnếu không tìm thấy vật phẩm ở nơi findtương đương với hàng firstOrNulltrả lại null. vì vậy nếu bạn muốn ném thay vì trả lại giá trị sử dụng vô hiệufirst
hài hước

Phương thức này có thể được sử dụng với fun valueFrom(valueA: Int, valueB: String): EnumType? = values().find { it.valueA == valueA && it.valueB == valueB } enum có nhiều giá trị: Ngoài ra, bạn có thể ném một ngoại lệ nếu các giá trị không có trong enum: fun valueFrom( ... ) = values().find { ... } ?: throw Exception("any message") hoặc bạn có thể sử dụng nó khi gọi phương thức này: var enumValue = EnumType.valueFrom(valueA, valueB) ?: throw Exception( ...)
ecth

Phương pháp của bạn có độ phức tạp tuyến tính O (n). Tốt hơn nên sử dụng tra cứu trong HashMap được xác định trước với độ phức tạp O (1).
Eldar Agalarov

vâng, tôi biết nhưng trong hầu hết các trường hợp, enum sẽ có rất ít trạng thái nên cách nào cũng không quan trọng, cái gì dễ đọc hơn.
kinh ngạc

27

Nó không có nhiều ý nghĩa trong trường hợp này, nhưng đây là "trích xuất logic" cho giải pháp của @ 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

Nói chung đó là điều về các đối tượng đồng hành mà chúng có thể được sử dụng lại (không giống như các thành viên tĩnh trong một lớp Java)


Tại sao bạn sử dụng lớp học mở? Chỉ làm cho nó trừu tượng.
Eldar Agalarov

21

Một lựa chọn khác, có thể được coi là "thành ngữ" hơn, sẽ như sau:

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

Mà sau đó có thể được sử dụng như thế nào Type[type].


Chắc chắn là thành ngữ hơn! Chúc mừng.
AleksandrH

6

Tôi thấy mình đã thực hiện tra cứu ngược theo tùy chỉnh, mã hóa bằng tay, giá trị vài lần và nghĩ ra cách tiếp cận sau.

Thực enumhiện triển khai giao diện dùng chung:

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
}

Giao diện này (tuy tên lạ :)) đánh dấu một giá trị nhất định là mã rõ ràng. Mục tiêu là có thể viết:

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

Có thể dễ dàng đạt được điều này với đoạn mã sau:

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
Đó là rất nhiều công việc cho một thao tác đơn giản như vậy, câu trả lời được chấp nhận là nhiều sạch IMO
Connor Wyatt

2
Hoàn toàn đồng ý để sử dụng đơn giản nó chắc chắn tốt hơn. Tôi đã có mã trên để xử lý các tên rõ ràng cho thành viên đã liệt kê.
miensol

Mã của bạn sử dụng phản chiếu (xấu) và bị phình ra (xấu quá).
Eldar Agalarov

1

Một biến thể của một số đề xuất trước đó có thể là như sau, sử dụng trường thứ tự và 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

Một ví dụ thực hiện khác. Điều này cũng đặt giá trị mặc định (ở đây thành OPEN) nếu không có đầu vào nào phù hợp với không có tùy chọn 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

Đưa ra một giải pháp chung chung hơn

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

Ví dụ sử dụng:

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

0

True Idiomatic Kotlin Way. Không có mã phản chiếu cồng kềnh:

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

-1

val t = Type.values ​​() [thứ tự]

:)


Điều này hoạt động đối với các hằng số 0, 1, ..., N. Nếu bạn có chúng như 100, 50, 35, thì nó sẽ không cho kết quả đúng.
CoolMind
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.