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