Kotlin数据类:如果我在编译时不知道属性的名称,如何读取属性的值?

47
如何在运行时仅知道属性名的情况下读取 Kotlin 数据类实例中属性的值?

Kotlin拥有一个反射库:https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/index.html - JB Nizet
4个回答

61

下面是一个函数,可以从类的实例中读取属性名称(如果未找到属性,则会抛出异常,但您可以更改该行为):

import kotlin.reflect.KProperty1
import kotlin.reflect.full.memberProperties

@Suppress("UNCHECKED_CAST")
fun <R> readInstanceProperty(instance: Any, propertyName: String): R {
    val property = instance::class.members
                     // don't cast here to <Any, R>, it would succeed silently 
                     .first { it.name == propertyName } as KProperty1<Any, *> 
    // force a invalid cast exception if incorrect type here
    return property.get(instance) as R  
}

build.gradle.kts

dependencies {
    implementation(kotlin("reflect"))
}

Using

// some data class
data class MyData(val name: String, val age: Int)
val sample = MyData("Fred", 33)

// and reading property "name" from an instance...
val name: String = readInstanceProperty(sample, "name")

// and reading property "age" placing the type on the function call...
val age = readInstanceProperty<Int>(sample, "age")

println(name) // Fred
println(age)  // 33

3
我知道这个答案已经有近2年的历史了... 这还是现在的正确方法吗?我很好奇是否已经有任何新的 Kotlin API 可以创建另一种实现方式。 - Hatem Jaber
如果您在编译时不知道属性的名称,那么是的,您可以使用@HatemJaber。如果您想要读取一个在编译时已知的属性,则可以使用属性引用来访问该属性。 - Jayson Minard
3
反射库的新版本允许使用instance::class.members代替instance.javaClass.kotlin.declaredMemberProperties,但应检查属性的类型是否为KProperty<*,*>KMutableProperty<*,*>。请注意不要改变原意。 - outis
如果实例类包含类型为List的字段,则会引发异常“IllegalAccessException:class kotlin.reflect.jvm.internal.calls.CallerImpl $ FieldGetter无法访问具有修饰符“private final”的类java.util.Collections $ SingletonList(在模块java.base中)的成员”。是否有一种方法可以绕过这个问题? - KeNVin Favo
如果有人在发布版本(Exception:java.lang.IllegalAccessException)方面遇到问题,请尝试访问此链接:https://dev59.com/zVYM5IYBdhLWcg3w-Dpf#48159066 - Reejesh PK
显示剩余2条评论

29
你可以通过反射来实现,对于数据类和普通类来说都是一样的。
第一种选择就是使用Java反射:
val name = obj.javaClass
              .getMethod("getName") // to get property called `name`
              .invoke(obj)

你甚至可以创建一个扩展函数:

inline fun <reified T : Any> Any.getThroughReflection(propertyName: String): T? {
    val getterName = "get" + propertyName.capitalize()
    return try {
        javaClass.getMethod(getterName).invoke(this) as? T
    } catch (e: NoSuchMethodException) {
        null
    }
}

它调用了公共的getter。要获取私有属性的值,您可以使用getDeclaredMethodsetAccessible修改此代码。这也适用于具有相应getter的Java对象(但它缺少booleanishas getter的约定)。
使用方法:
data class Person(val name: String, val employed: Boolean)

val p = Person("Jane", true)
val name = p.getThroughReflection<String>("name")
val employed = p.getThroughReflection<Boolean>("employed")

println("$name - $employed") // Jane - true

第二个选项涉及使用kotlin-reflect库,你需要将其单独添加到你的项目中,这是它的文档。它将让你获取实际的Kotlin属性值,忽略Java的getter方法。
你可以使用javaClass.kotlin来获取实际的Kotlin类令牌,然后从中获取属性。
val name = p.javaClass.kotlin.memberProperties.first { it.name == "name" }.get(p)

这个解决方案仅适用于 Kotlin 类,而不适用于 Java 类,但如果你需要处理 Kotlin 类,则更加可靠:它不依赖于底层实现。

第一种解决方案存在几个问题:它排除了继承属性,不能处理布尔属性(getter 以 is... 开头的),即使属性不是公共的也会尝试访问。使用 java.beans 类(或任何允许访问 bean 属性的 Java 库)可能是一个更好的选择。 - JB Nizet
@JBNizet,我已经修正了你关于实现的说法。 - hotkey
@JBNizet,嗯,我刚试了一下--Kotlin甚至为Boolean属性生成了名为getSomething的方法,而不是isSomething - hotkey
2
这就是我的观点之一:与其假设和依赖低级方法,你最好依赖Kotlin反射支持属性(即答案的第二部分)或处理Java Beans的更高级别的Java抽象。 - JB Nizet
或者只需使用@JvmField注解来省略setter和getter。 - Vlad

19

以上的答案对我没有用,所以我创建了一个扩展函数来解决这个问题:

@Throws(IllegalAccessException::class, ClassCastException::class)
inline fun <reified T> Any.getField(fieldName: String): T? {
    this::class.memberProperties.forEach { kCallable ->
        if (fieldName == kCallable.name) {
            return kCallable.getter.call(this) as T?
        }
    }
    return null
}

这是一个示例调用:
val valueNeeded: String? = yourObject.getField<String>("exampleFieldName")

也请在您应用的build.gradle文件中包含以下内容:
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

Kotlin 团队正在努力将该语言与已检查异常分离开来,我相信。https://kotlinlang.org/docs/reference/exceptions.html#checked-exceptions - Farid
2
这个答案对我有用。谢谢。 - sanya5791
@Farid 是的,在 Kotlin 中,我们甚至没有声明“... throws IllegalAccessException”的选项,但指示方法可能出现哪些类型的异常是有用的,因此使用 Throws 注释。 - Adam Kis
1
你救了我的一天(几天),谢谢。 - Yasin Bikmazer

1

我想知道是否可以通过编程来定义字段的类型。您可以轻松地获取类型:

kCallable.returnType

然而,您仍然需要明确指定通用类型:
getField<String>

替代

getField<kCallable.returnType>

编辑:

我最终使用了以下内容:
when (prop.call(object)) {
    is ObservableList<*> -> {}
    is Property<*> -> {}
}

你找到答案了吗?那正是我现在正在处理的问题。 - melis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接