Edit Page

反射

最终更新: 2024/03/21

反射 是语言与库中的一组功能, 允许你在运行时刻获取程序本身的信息. 函数和属性在 Kotlin 是语言中的一等公民(first-class citizen), 而且, 通过反射获取它们的信息(比如, 在运行时刻得到一个函数或属性的名称和数据类型) 也是函数式或交互式的编程方式中的基本功能.

Kotlin/JS 对反射只提供了有限的支持. 更多详情请参见 Kotlin/JS 中的反射功能.

JVM 依赖项

在 JVM 平台上, Kotlin 编译器包含了使用反射功能所需要的运行时组件, 它是一个单独的 JAR 文件 kotlin-reflect.jar. 这样做为了对那些不使用反射功能的应用程序, 减少其运行库的大小.

在 Gradle 或 Maven 项目中, 如果需要使用反射, 需要添加 kotlin-reflect 的依赖项:

  • 在 Gradle 项目中:
dependencies {
    implementation(kotlin("reflect"))
}
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.23"
}
  • 在 Maven 项目中:
<dependencies>
  <dependency>
      <groupId>org.jetbrains.kotlin</groupId>
      <artifactId>kotlin-reflect</artifactId>
  </dependency>
</dependencies>

如果你没有使用 Gradle 或 Maven, 请注意将 kotlin-reflect.jar 添加到你的项目的 classpath 中. 对于其他支持的场景(使用命令行编译器, 或 Ant 的 IntelliJ IDEA 项目), 这个 jar 文件默认会加入到 classpath 中. 在命令行编译器和 Ant 中, 你可以使用 -no-reflect 编译选项, 从 classpath 中删除 kotlin-reflect.jar.

类引用(Class Reference)

最基本的反射功能就是获取一个 Kotlin 类的运行时引用. 要得到一个静态的已知的 Kotlin 类的引用, 可以使用 类字面值(class literal) 语法:

val c = MyClass::class

类引用是一个 KClass 类型的值.

在 JVM 平台: Kotlin 的类引用与 Java 的类引用不相同. 要得到 Java 的类引用, 请使用 KClass 对象实例的 .java 属性.

与对象实例绑定的类引用语法

::class 语法同样可以用于取得某个对象实例的类的引用:

val widget: Widget = ...
assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" }

在这个例子中, 尽管 widget 的类型为 Widget, 但你会得到对象实例的确切的类的引用, 比如 GoodWidget, 或 BadWidget.

可调用的引用

指向函数, 属性, 构造器的引用, 可以被调用, 或用作 函数类型 的实例.

所有可调用的引用的共同的超类是 KCallable<out R>, 这里的 R 是返回值的类型. 对于属性来说就是属性类型, 对构造器来说就是它创建出来的类的类型.

函数引用(Function Reference)

假设你有一个有名称的函数, 声明如下, 你可以直接调用它(isOdd(5)):

fun isOdd(x: Int) = x % 2 != 0

另一种情况是, 你可以将它用作一个函数类型的值, 比如, 传给另一个函数作为参数. 为了实现这个功能, 可以使用 :: 操作符:

fun isOdd(x: Int) = x % 2 != 0

fun main() {
//sampleStart
    val numbers = listOf(1, 2, 3)
    println(numbers.filter(::isOdd))
//sampleEnd
}

这里的 ::isOdd 是一个 (Int) -> Boolean 函数类型的值.

函数引用的类型属于 KFunction<out R> 的子类之一, 具体是哪个由函数的参数个数决定. 比如, 可能是 KFunction3<T1, T2, T3, R>.

:: 也可以用在重载函数上, 前提是必须能够推断出对应的函数参数类型. 比如:

fun main() {
//sampleStart
    fun isOdd(x: Int) = x % 2 != 0
    fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove"

    val numbers = listOf(1, 2, 3)
    println(numbers.filter(::isOdd)) // 指向 isOdd(x: Int) 函数
//sampleEnd
}

或者, 你也可以将方法引用保存到一个明确指定了类型的变量中, 通过这种方式来提供必要的函数参数类型信息:

val predicate: (String) -> Boolean = ::isOdd   // 指向 isOdd(x: String) 函数

如果你需要使用一个类的成员函数, 或者一个扩展函数, 就必须使用限定符: String::toCharArray.

即使你将一个变量初始化赋值为一个扩展函数的引用, 编译器自动推断得到的函数类型实际上是不带接受者的, 但它会带有一个额外的参数, 对应于接受者对象. 如果想要使用带接受者的函数类型, 需要明确指定函数类型:

val isEmptyStringList: List<String>.() -> Boolean = List<String>::isEmpty

示例: 函数组合

我们来看看下面的函数:

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

这个函数返回一个新的函数, 由它的两个参数代表的函数组合在一起构成: compose(f, g) = f(g(*)). 你可以使用可以执行的函数引用来调用这个函数:

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

fun isOdd(x: Int) = x % 2 != 0

fun main() {
//sampleStart
    fun length(s: String) = s.length

    val oddLength = compose(::isOdd, ::length)
    val strings = listOf("a", "ab", "abc")

    println(strings.filter(oddLength))
//sampleEnd
}

属性引用(Property Reference)

在 Kotlin 中, 可以将属性作为一等对象来访问, 方法是使用 :: 操作符:

val x = 1

fun main() {
    println(::x.get())
    println(::x.name)
}

表达式 ::x 的计算结果是一个属性对象, 类型为 KProperty0<Int>. 你可以通过它的 get() 方法得到属性值, 或者通过它的 name 属性得到属性名称. 详情请参见 KProperty 类的 API 文档.

对于值可变的属性, 比如, var y = 1, ::y 返回的属性对象的类型为 KMutableProperty0<Int>, 它有一个 set() 方法:

var y = 1

fun main() {
    ::y.set(2)
    println(y)
}

所有使用单参数函数的地方都可以使用属性引用:

fun main() {
//sampleStart
    val strs = listOf("a", "bc", "def")
    println(strs.map(String::length))
//sampleEnd
}

要访问类的成员属性, 需要使用限定符, 如下:

fun main() {
//sampleStart
    class A(val p: Int)
    val prop = A::p
    println(prop.get(A(1)))
//sampleEnd
}

对于扩展属性:

val String.lastChar: Char
    get() = this[length - 1]

fun main() {
    println(String::lastChar.get("abc"))
}

与 Java 反射功能的互操作性

在 Java 平台上, Kotlin 的标准库包含了针对反射类的扩展函数, 这些反射类提供了与 Java 反射对象的相互转换功能(参见包 kotlin.reflect.jvm). 比如, 要查找一个 Kotlin 属性的后端域变量, 或者查找充当这个属性取值函数的 Java 方法, 你可以编写下面这样的代码:

import kotlin.reflect.jvm.*

class A(val p: Int)

fun main() {
    println(A::p.javaGetter) // 打印结果为: "public final int A.getP()"
    println(A::p.javaField)  // 打印结果为: "private final int A.p"
}

要查找与一个 Java 类相对应的 Kotlin 类, 可以使用 .kotlin 扩展属性:

fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin

构造器引用(Constructor Reference)

与方法和属性一样, 也可以引用构造器. 凡是使用使用函数类型对象的地方, 你都可以使用构造器的引用, 但这个函数类型接受的参数应该与构造器相同, 返回值应该是构造器所属类的对象实例. 引用构造器使用 :: 操作符, 再加上类名称. 我们来看看下面的函数, 它接受的参数是一个函数, 这个函数参数本身没有参数, 并返回 Foo 类型:

class Foo

fun function(factory: () -> Foo) {
    val x: Foo = factory()
}

使用 ::Foo, 也就是 Foo 类的无参构造器的引用, 你可以这样调用上面的函数:

function(::Foo)

指向构造器的引用的类型是 KFunction<out R> 的子类之一, 具体是哪个由函数的参数个数决定.

与对象实例绑定的函数和属性引用

你可以引用某个具体的对象实例的方法:

fun main() {
//sampleStart
    val numberRegex = "\\d+".toRegex()
    println(numberRegex.matches("29"))

    val isNumber = numberRegex::matches
    println(isNumber("29"))
//sampleEnd
}

上面的示例使用 matches 方法的引用, 而不是直接调用这个方法. 这样的引用会与方法的接受者绑定在一起. 这样的引用可以直接调用(就像上面的示例程序中那样), 也可以用在任何使用函数类型的地方:

fun main() {
//sampleStart
    val numberRegex = "\\d+".toRegex()
    val strings = listOf("abc", "124", "a70")
    println(strings.filter(numberRegex::matches))
//sampleEnd
}

我们来比较一下绑定到对象实例的引用, 以及未绑定到实例的引用. 绑定到对象实例的引用与它的接受者对象实例结合在一起, 因此接受者的类型不再是它的一个参数:

val isNumber: (CharSequence) -> Boolean = numberRegex::matches

val matches: (Regex, CharSequence) -> Boolean = Regex::matches

同样, 属性的引用也可以与对象实例绑定:

fun main() {
//sampleStart
    val prop = "abc"::length
    println(prop.get())
//sampleEnd
}

你不需要指定 this 接收者: this::foo 可以简写为 ::foo.

与实例绑定的构造器引用

(译注: 内部类与普通类不同, 在创建内部类实例时, 需要绑定到一个具体的外部类实例.) 通过指定一个外部类的实例, 可以得到与这个外部类实例绑定的 内部类 (inner class) 的构造器引用:

class Outer {
    inner class Inner
}

val o = Outer()
val boundInnerCtor = o::Inner