5.13 反射

原文链接: https://kotlinlang.org/docs/reflection.html

5.13 反射

反射是一组语言和库特性,让你可以在运行时检视程序的结构。函数和属性在 Kotlin 中是一等公民,而检视它们的能力(例如在运行时获取属性或函数的名称或类型)在使用函数式或响应式风格时至关重要。

注意: Kotlin/JS 对反射特性的支持有限。进一步了解 Kotlin/JS 中的反射。

JVM 依赖

在 JVM 平台上,Kotlin 编译器发行版把使用反射功能所需的运行时组件作为单独的构件包含在内,即 kotlin-reflect.jar。这样做是为了减小不使用反射功能的应用的运行时库体积。

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

  • 在 Gradle 中:

Kotlin

1
2
3
    dependencies {
        implementation(kotlin("reflect"))
    }

Groovy

1
2
3
    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-reflect:2.4.20"
    }
  • 在 Maven 中:
1
2
3
4
5
6
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </dependency>
    </dependencies>

如果你不使用 Gradle 或 Maven,请确保项目的 classpath 中有 kotlin-reflect.jar。在其他受支持的情况下(使用命令行编译器的 IntelliJ IDEA 项目),它会默认被添加。在命令行编译器中,你可以用 -no-reflect 编译器选项把 kotlin-reflect.jar 从 classpath 中排除。

类引用

最基本的反射功能是获取对 Kotlin 类的运行时引用。要获取对静态已知 Kotlin 类的引用,可以使用类字面量语法:

1
val c = MyClass::class

该引用是一个 KClass 类型的值。

在 JVM 上:Kotlin 的类引用与 Java 的类引用不同。要获取 Java 类引用,请使用 KClass 实例上的 .java 属性。

绑定的类引用

用相同的 ::class 语法,把对象作为接收者,就能获取该对象所属具体类的引用:

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

无论接收者表达式的类型是什么(这里是 Widget),你都会得到该对象确切类的引用,例如 GoodWidget 或 BadWidget。

可调用引用

对函数、属性和构造函数的引用也可以被调用,或用作函数类型的实例。

所有可调用引用的共同超类型是 KCallable<out R>,其中 R 是返回值类型。对于属性,它是属性类型;对于构造函数,它是构造出的类型。

函数引用

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

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

或者,你可以把该函数用作函数类型的值,也就是把它传给另一个函数。为此,请使用 :: 运算符:

1
2
3
4
5
6
fun isOdd(x: Int) = x % 2 != 0

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

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

函数引用属于 KFunction<out R> 的某个子类型,具体取决于参数个数。例如 KFunction3<T1, T2, T3, R>。

当期望的类型能从上下文中得知时,:: 也可以用于重载函数。例如:

1
2
3
4
5
6
7
fun main() {
    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)
}

或者,你可以把方法引用存储到显式指定了类型的变量中,从而提供必要的上下文:

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

如果你需要使用类的成员或扩展函数,则必须对它进行限定:String::toCharArray。

即使你用扩展函数的引用初始化变量,推断出的函数类型也不带接收者,而是会多出一个接受接收者对象的参数。若要改为带接收者的函数类型,请显式指定类型:

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

示例:函数组合

考虑下面这个函数:

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

它返回传入两个函数的组合:compose(f, g) = f(g(*))。你可以把这个函数用于可调用引用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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() {
    fun length(s: String) = s.length

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

    println(strings.filter(oddLength))
}

属性引用

要把属性作为一等对象访问,请使用 :: 运算符:

1
2
3
4
5
6
val x = 1

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

表达式 ::x 求值为 KProperty0<Int> 类型的属性对象。你可以用 get() 读取它的值,或用 name 属性获取属性名。更多信息请参阅 KProperty 类的文档。

对于像 var y = 1 这样的可变属性,::y 返回具有 KMutableProperty0<Int> 类型的值,它带有一个 set() 方法:

1
2
3
4
5
6
var y = 1

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

属性引用可以用在期望带单个泛型参数的函数的地方:

1
2
3
4
fun main() {
    val strs = listOf("a", "bc", "def")
    println(strs.map(String::length))
}

要访问类的成员属性,请按如下方式限定:

1
2
3
4
5
fun main() {
    class A(val p: Int)
    val prop = A::p
    println(prop.get(A(1)))
}

对于扩展属性:

1
2
3
4
5
6
val String.lastChar: Char
    get() = this[length - 1]

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

与 Java 反射的互操作

在 JVM 平台上,标准库为反射类提供了扩展,从而能够在 Java 反射对象之间相互映射(参见包 kotlin.reflect.jvm)。例如,要找到某个 Kotlin 属性的幕后字段或充当其 getter 的 Java 方法,你可以这样写:

1
2
3
4
5
6
7
8
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 扩展属性:

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

构造函数引用

构造函数可以像方法和属性一样被引用。在程序期望一个"参数与构造函数相同、返回相应类型对象"的函数类型对象的地方,都可以使用它们。构造函数通过 :: 运算符加上类名来引用。考虑下面这个期望无参数、返回类型为 Foo 的函数参数的函数:

1
2
3
4
5
class Foo

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

使用 ::Foo(类 Foo 的零参数构造函数),你可以这样调用它:

1
function(::Foo)

指向构造函数的可调用引用会按参数个数被归类为 KFunction<out R> 的某个子类型。

绑定的函数与属性引用

你可以引用某个特定对象的实例方法:

1
2
3
4
5
6
7
fun main() {
    val numberRegex = "\\d+".toRegex()
    println(numberRegex.matches("29"))

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

这个例子没有直接调用 matches 方法,而是使用了它的引用。这样的引用绑定到它的接收者上。它可以直接调用(如上面的例子),也可以用在任何期望函数类型表达式的地方:

1
2
3
4
5
fun main() {
    val numberRegex = "\\d+".toRegex()
    val strings = listOf("abc", "124", "a70")
    println(strings.filter(numberRegex::matches))
}

比较一下绑定引用和未绑定引用的类型。绑定的可调用引用把接收者"附加"到自身上,因此接收者的类型不再是参数:

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

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

属性引用同样可以绑定:

1
2
3
4
fun main() {
    val prop = "abc"::length
    println(prop.get())
}

你不需要把 this 指定为接收者:this::foo 与 ::foo 是等价的。

绑定的构造函数引用

通过提供外部类的实例,可以获取指向内部类构造函数的绑定可调用引用:

1
2
3
4
5
6
class Outer {
    inner class Inner
}

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