Kotlin的注解与反射

155 阅读11分钟

注解与反射可以提高代码的灵活性。

注解

Kotlin 当中的注解,其实就是“程序代码的一种补充”。

注解的定义:

首先看看注解是如何定义的。Kotlin 的源代码当中,提供了很多内置的注解,比如 @Deprecated、@JvmStatic、@JvmOverloads 等等。除了 Kotlin 默认就有的注解以外,我们也可以定义自己的注解。

比如定义一个 @Deprecated 注解,其实非常简单,总体结构和定义一个普通的 Kotlin 类差不多,只是多了一些额外的东西。

@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
    val message: String,
    val replaceWith: ReplaceWith = ReplaceWith(""),
    val level: DeprecationLevel = DeprecationLevel.WARNING
)

可以看到,@Deprecated 这个注解的定义上面,还有其他的注解 @Target、@MustBeDocumented。这样的注解,我们叫做元注解,即它本身是注解的同时,还可以用来修饰其他注解。

Kotlin 常见的元注解有四个:

@Target,这个注解是指定了被修饰的注解都可以用在什么地方,也就是目标;

@Retention,这个注解是指定了被修饰的注解是不是编译后可见、是不是运行时可见,也就是保留位置;

@Repeatable,这个注解是允许我们在同一个地方,多次使用相同的被修饰的注解,使用场景比较少;

@MustBeDocumented,指定被修饰的注解应该包含在生成的 API 文档中显示,这个注解一般用于 SDK 当中。

这里需要注意的是 Target 和 Retention 的取值:

public enum class AnnotationTarget {
    // 类、接口、object、注解类
    CLASS,
    // 注解类
    ANNOTATION_CLASS,
    // 泛型参数
    TYPE_PARAMETER,
    // 属性
    PROPERTY,
    // 字段、幕后字段
    FIELD,
    // 局部变量
    LOCAL_VARIABLE,
    // 函数参数
    VALUE_PARAMETER,
    // 构造器
    CONSTRUCTOR,
    // 函数
    FUNCTION,
    // 属性的getter
    PROPERTY_GETTER,
    // 属性的setter
    PROPERTY_SETTER,
    // 类型
    TYPE,
    // 表达式
    EXPRESSION,
    // 文件
    FILE,
    // 类型别名
    TYPEALIAS
}

public enum class AnnotationRetention {
    // 注解只存在于源代码,编译后不可见
    SOURCE,
    // 注解编译后可见,运行时不可见
    BINARY,
    // 编译后可见,运行时可见
    RUNTIME
}

这段代码的注释详细解释了 Target 和 Retention 的取值,以及它们各自代表的意义。现在看看我们定义的“@Deprecated”到底是什么含义。通过 @Target 的取值,我们可以看到,@Deprecated 只能作用于这些地方:类、 函数、 属性、注解类、构造器、属性 getter、属性 setter、类型别名。此外,@Deprecated 这个类当中还包含了几个成员:message 代表了废弃的提示信息;replaceWith 代表了应该用什么来替代废弃的部分;level 代表警告的程度,分别是 WARNING、ERROR、HIDDEN。

OK,现在已经知道如何定义注解了,接下来看看如何用它。仍然以 @Deprecated 注解为例。

注解的使用:

假设现在要开发一个计算器,第一个版本的 Calculator 代码出现了问题,然后这个问题在 CalculatorV3 当中修复了。这时候,我们希望所有的调用方都将 Calculator 改为 CalculatorV3。这种情况,@Deprecated 这个注解就恰好符合我们的需求。

@Deprecated(
    message = "Use CalculatorV3 instead.",
    replaceWith = ReplaceWith("CalculatorV3"),
    level = DeprecationLevel.ERROR
)
class Calculator {
    // 错误逻辑
    fun add(a: Int, b: Int): Int = a - b
}

class CalculatorV3 {
    // 正确逻辑
    fun add(a: Int, b: Int): Int = a + b
}

在上面的代码中,我们使用 @Deprecated 修饰了 Calculator 这个类。message 代表了报错的提示信息;replaceWith 代表了正确的解决方案;DeprecationLevel.ERROR 则代表了 IDE 会把这个问题当作是错误的来看待。

当我们在代码当中使用了 Calculator 的时候,IDE 会报错,鼠标挪到报错处后,IDE 会显示 message 当中的内容“Use CalculatorV3 instead.”。另外,IDE 还会提供一个快速修复的选项“Replace with CalculatorV3”,只要我们点击那个选项,我们的 Calculator 就会被直接替换成 CalculatorV3,从而达到修复错误的目的。

还有,由于我们使用的 level 是 DeprecationLevel.ERROR,所以 IDE 会直接报错。而如果我们使用的是 DeprecationLevel.WARNING,IDE 就只会提示一个警告,而不是错误。

注解的精确使用目标:

看一个具体的例子,比如Dagger当中的 @Inject 注解:

object Singleton {
//    ①
//    ↓
    @set:Inject
    lateinit var person: Person
//     ↑
//     ②
}

这段代码,是一个简单的 Dagger 使用场景。

注释①:如果去掉 set 这个标记,直接使用 @Inject 这个注解,我们的程序将无法正常工作。这是因为 Kotlin 当中的一个 var 修饰的属性,它会有多种含义:这个属性背后的字段(field)、这个属性对应的 setter、还有这个属性对应的 getter,在没有明确标注出来要用哪个的时候,@Inject 根本不知道该如何决定。因此,这里的“@set:Inject”的作用,就是明确标记出注解的精确使用目标(Use-site targets)。

注释②:如果没有 lateinit 这个关键字,person 这个属性是必须要有初始值的,要么直接赋值,要么在构造函数当中被赋值。因为如果它不被初始化,person 这个属性将为空,而这个就和它的类型“不可为空的 Person 类型”冲突了。而加上 lateinit 修饰的属性,即使它是不可为空的,编译器也会允许它无初始值。但当我们需要依赖注入的时候,常常需要与 lateinit 结合使用。

实际上,注解的精确使用目标,一般是和注解一起使用的,在上面的例子当中,set 就是和 @Inject 一起使用的。而除了 set 以外,Kotlin 当中还有其他的使用目标:

file,作用于文件;

property,作用于属性;

field,作用于字段;

get,作用于属性 getter;

set,作用于属性 setter;

receiver,作用于扩展的接受者参数;

param,作用于构造函数参数;

setparam,作用于函数参数;

delegate,作用于委托字段。

反射

反射,则是程序自我反省的能力。

总的来看,Kotlin 反射具备这三个特质:

感知程序的状态,包含程序的运行状态,还有源代码结构;

修改程序的状态;根据程序的状态,

调整自身的决策行为。

Kotlin默认不集成反射库,处于安装包体积考虑。

果需要用到反射,必须要引入这个依赖:

implementation "org.jetbrains.kotlin:kotlin-reflect"

假设,现在有一个待实现的函数 readMembers。这个函数的参数 obj 可能是任何的类型,我们需要读取 obj 当中所有的成员属性的名称和值。无法预知情况下,考虑使用反射:

fun readMembers(obj: Any) {
    obj::class.memberProperties.forEach {
        println("${obj::class.simpleName}.${it.name}=${it.getter.call(obj)}")
    }
}

obj::class,这是 Kotlin 反射的语法,我们叫做类引用,通过这样的语法,我们就可以读取一个变量的“类型信息”,并且就能拿到这个变量的类型,它的类型是 KClass。

public actual interface KClass<T : Any> : KDeclarationContainer, KAnnotatedElement, KClassifier {

    public actual val simpleName: String?

    public actual val qualifiedName: String?

    override val members: Collection<KCallable<*>>
    // 省略部分代码
}

这个 KClass 其实就代表了一个 Kotlin 类,通过 obj::class,我们就可以拿到这个类型的所有信息,比如说,类的名称“obj::class.simpleName”。而如果要获取类的所有成员属性,我们访问它的扩展属性 memberProperties 就可以了。

val <T : Any> KClass<T>.memberProperties: Collection<KProperty1<T, *>>
    get() = (this as KClassImpl<T>).data().allNonStaticMembers.filter { it.isNotExtension && it is KProperty1<*, *> } as Collection<KProperty1<T, *>>

在拿到所有的成员属性以后,我们可以通过 forEach 遍历所有的属性,它的类型是 KProperty1,同时也是 KCallable 的子类,我们通过调用属性的 getter.call(),就可以拿到 obj 属性的值了。

到目前为止,我们的程序就已经可以感知到自身的状态了。

接下来尝试修改自身的状态,这是反射的第二个特质。

具体需求是这样的:如果传入的参数当中,存在 String 类型的 address 变量,我们就将其改为 China。

fun main() {
    val student = Student("Tom", 99.5, 170)
    val school = School("PKU", "Beijing...")

    readMembers(student)
    readMembers(school)

    // 修改其中的address属性
    modifyAddressMember(school)

    readMembers(school)
    readMembers(student)
}

fun modifyAddressMember(obj: Any) {
    obj::class.memberProperties.forEach {
        if (it.name == "address" && // ①
            it is KMutableProperty1 && // ②
            it.setter.parameters.size == 2 && // ③
            it.getter.returnType.classifier == String::class // ④
        ) {
            // ⑤
            it.setter.call(obj, "China")
            println("====Address changed.====")
        }
    }
}

// 运行结果:
Student.height=170
Student.name=Tom
Student.score=99.5
// 注意这里
School.address=Beijing...
School.name=PKU
====Address changed.====
// 注意这里
School.address=China
School.name=PKU
Student.height=170
Student.name=Tom
Student.score=99.5

从上面的代码可以看到,当运行了 modifyAddressMember(school) 这行代码以后,反射代码就会检查传入的变量当中,是否存在 String 类型的 address,如果存在,就会将它的值修改为“China”。

可以关注标出来的四个注释,它们就代表了关键的逻辑:

注释①,判断属性的名称是否为 address,如果不是,则跳过;

注释②,判断属性是否可变,在我们的例子当中 address 是用 var 修饰的,因此它的类型是 KMutableProperty1;

注释③,我们在后面要调用属性的 setter,所以我们要先判断 setter 的参数是否符合预期,这里 setter 的参数个数应该是 2,第一个参数是 obj 自身,第二个是实际的值;

注释④,根据属性的 getter 的返回值类型 returnType,来判断属性的类型是不是 String 类型;

注释⑤,调用属性的 setter 方法,传入 obj,还有“China”,来完成属性的赋值。

已经了解反射的两种特质,分别是感知程序的状态和修改程序的状态。现在只剩下第三种,根据程序状态作出不同决策。这个其实非常容易做到。假如在前面的例子的基础上,我们想要增加一个功能:如果传入的参数没有符合需求的 address 属性,我们就输出一行错误日志。这其实也就代表了根据程序的状态,作出不同的行为。比如,我们可以看看下面这段示例,其中的 else 分支就是我们的决策行为“输出错误日志”:

    obj::class.memberProperties.forEach {
        if (it.name == "address" &&
            it is KMutableProperty1 &&
            it.getter.returnType.classifier == String::class
        ) {
            it.setter.call(obj, "China")
            println("====Address changed.====")
        } else {
            // 差别在这里
            println("====Wrong type.====")
        }
    }
}

// 输出结果:
Student.height=170
Student.name=Tom
Student.score=99.5
School.address=Beijing...
School.name=PKU
====Address changed.====
====Wrong type.====  // 差别在这里
School.address=China
School.name=PKU
Student.height=170
Student.name=Tom
Student.score=99.5

在前面的几个案例当中,用到了 Kotlin 反射的几个关键的反射 Api 和类:KClass、KCallable、KParameter、KType。现在,进一步看看它们的关键成员。

KClass 代表了一个 Kotlin 的类,下面是它的重要成员:

simpleName,类的名称,对于匿名内部类,则为 null;

qualifiedName,完整的类名;

members,所有成员属性和方法,类型是Collection>;

constructors,类的所有构造函数,类型是Collection>>;

nestedClasses,类的所有嵌套类,类型是Collection>;

visibility,类的可见性,类型是KVisibility?,分别是这几种情况,PUBLIC、PROTECTED、INTERNAL、PRIVATE;isFinal,是不是 final;

isOpen,是不是 open;

isAbstract,是不是抽象的;

isSealed,是不是密封的;

isData,是不是数据类;

isInner,是不是内部类;

isCompanion,是不是伴生对象;

isFun,是不是函数式接口;

isValue,是不是 Value Class。

KCallable 代表了 Kotlin 当中的所有可调用的元素,比如函数、属性、甚至是构造函数。下面是 KCallable 的重要成员:

name,名称,这个很好理解,属性和函数都有名称;

parameters,所有的参数,类型是List,指的是调用这个元素所需的所有参数;

returnType,返回值类型,类型是 KType;

typeParameters,所有的类型参数 (比如泛型),类型是List;

call(),KCallable 对应的调用方法,在前面的例子中,我们就调用过 setter、getter 的 call() 方法。

visibility,可见性;

isSuspend,是不是挂起函数。

KParameter,代表了KCallable当中的参数,它的重要成员如下:

index,参数的位置,下标从 0 开始;

name,参数的名称,源码当中参数的名称;

type,参数的类型,类型是 KType;

kind,参数的种类,对应三种情况:INSTANCE 是对象实例、EXTENSION_RECEIVER 是扩展接受者、VALUE 是实际的参数值。

KType,代表了 Kotlin 当中的类型,它重要的成员如下:

classifier,类型对应的 Kotlin 类,即 KClass,我们前面的例子中,就是用的 classifier == String::class 来判断它是不是 String 类型的;

arguments,类型的类型参数,看起来好像有点绕,其实它就是这个类型的泛型参数;

isMarkedNullable,是否在源代码中标记为可空类型,即这个类型的后面有没有“?”修饰。

所以,归根结底,反射,其实就是 Kotlin 为我们开发者提供的一个工具,通过这个工具,我们可以让程序在运行的时候“自我反省”。这里的“自我反省”一共有三种情况,其实跟我们的现实生活类似。

第一种情况,程序在运行的时候,可以通过反射来查看自身的状态。

第二种情况,程序在运行的时候,可以修改自身的状态。

第三种情况,程序在运行的时候,可以根据自身的状态调整自身的行为。

小结:

注解和反射,是 Kotlin 当中十分重要的特性,它们可以极大地提升程序的灵活性。那么,在使用注解和反射的时候,要知道,注解,其实就是“程序代码的一种补充”,而反射,其实就是“程序代码自我反省的一种方式”。