Kotlin-注解与反射

213 阅读7分钟

一、注解与反射

注解与反射的意义:提高代码的灵活性。只有深刻理解了注解和反射,我们才可能理解那些著名开源库的设计思路,也才可能读懂这些世界顶级开发者的代码。

二、注解

1、认识注解

Kotlin 当中的注解,其实就是“程序代码的一种补充”。比如我们在Test包中去写测试代码 image.png

添加@TEST注解和不添加@Test注解的测试代码:

image.png

告诉系统上面的是测试代码,于是系统会在左边添加绿色小三角,可以直接点击运行。

2、注解的定义

如果我们想定义一个 @Deprecated 注解,应该怎么做呢?其实非常简单,总体结构 和定义一个普通的 Kotlin 类差不多,只是多了一些额外的东西。

import  kotlin.annotation.AnnotationTarget.*

@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
}

在这段代码的注释当中,我详细解释了 TargetRetention 的取值,以及它们各自代表的意 义。现在我们就可以回过头,来看看我们定义的@Deprecated到底是什么含义。

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

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

3、注解的使用

假设现在我们要开发一个计算器,第一个版本的 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
}

fun main(){
    val calculator = Calculator()  //报红
}

因为我们没有写自定义注解的实现,其实这里报红是官方注解提示的。并不是我们上面的自定义注解。

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

好了,到这里,我们就了解了注解要如何定义和使用。其实啊,只要我们真正理解了 Kotlin 的 注解到底是什么东西,前面的这些注解的语法是很容易就能记住的。不过,Kotlin 注解在使用 的时候,还有一个细节需要注意,那就是注解的精确使用目标

4、注解的精确使用目标

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

object Singleton {
// 1
// ↓
@set:Inject
lateinit var person: Person
// ↑
// 2
}

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

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

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

  • file,作用于文件;
  • property,作用于属性;
  • field,作用于字段;
  • get,作用于属性 getter;
  • set ,作用于属性 setter;
  • receiver,作用于扩展的接受者参数;
  • param,作用于构造函数参数;
  • setparam,作用于函数参数;
  • delegate,作用于委托字段。

三、反射

1、准备工作

如果想要使用反射,需要填入依赖:

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

我们需要反射下面这个类:

class Person {
    var userName: String = "unKnow"
    var age: Int = 0
}

2、读取成员变量

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

//调用
readMembers(Person())

//打印
TAG: Person.age=0
TAG: Person.userName=unKnow

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

/**
 * Returns non-extension properties declared in this class and all of its superclasses.
 */
@SinceKotlin("1.1")
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, *>>

另外通过调用属性的 getter.call(),就可以拿到 obj属性的值了,于是有了打印的结果。

3、修改成员变量

假如我们想修改Person类中userName的默认值应该怎么办? 看下面的代码:

/**
 * @param memberName 成员属性的名称
 * @param newValue 成员属性的新的值
 */
fun changeStringMembers(obj: Any, memberName: String, newValue: String) {
    obj::class.memberProperties.forEach {
        if (it.name == memberName &&   //属性的名称
            it is KMutableProperty1 &&  //判断属性是否可变
            it.setter.parameters.size == 2 && //这里 setter 的参数个数应该是 2,第一个参数是 obj 自身,第二个是实际的值
            it.getter.returnType.classifier == String::class //根据属性的 getter 的返回值类型 returnType,来判断属性的类型是不是 String 类型
        ) {
            it.setter.call(obj, newValue)   //修改属性的值
        }
        Log.d("TAG", "${obj::class.simpleName}.${it.name}=${it.getter.call(obj)}")
    }
}

//调用
changeStringMembers(Person(),"userName","张三")

//打印
TAG: Person.age=0
TAG: Person.userName=张三

4、反射的关键成员

Kotlin 反射的几个关键的反射 Api 和类:KClassKCallableKParameterKType。现在,我们来进一步看看它们的关键成员。

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

重要成员说明
simpleName类的名称,对于匿名内部类,则为 null
qualifiedName完整的类名
members所有成员属性和方法,类型是Collection<KCallable<*>>
constructors类的所有构造函数,类型是Collection<KFunction<T>>>
nestedClasses类的所有嵌套类,类型是Collection<KClass<*>>
visibility类的可见性,类型是KVisibility?,分别是这几种情况,PUBLIC、PROTECTED、INTERNAL、PRIVATE
isFinal是不是 final
isOpen是不是 open
isAbstract是不是抽象的
isSealed是不是密封的
isData是不是数据类
isInner是不是内部类
isCompanion是不是伴生对象
isFun是不是函数式接口
isValue是不是 Value Class

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

重要成员说明
name名称,这个很好理解,属性和函数都有名称
parameters所有的参数,类型是List<KParameter>,指的是调用这个元素所需的所有参数
returnType返回值类型,类型是 KType
typeParameters所有的类型参数 (比如泛型),类型是List<KTypeParameter>
call()KCallable 对应的调用方法,在前面的例子中,我们就调用过 setter、getter 的 call()方法
visibility可见性
isSuspend是不是挂起函数

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

重要成员说明
index参数的位置,下标从 0 开始
name参数的名称,源码当中参数的名称
type参数的类型,类型是 KType
kind参数的种类,对应三种情况:INSTANCE 是对象实例、EXTENSION_RECEIVER 是扩展接受者、VALUE 是实际的参数值。

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

重要成员说明
classifier类型对应的 Kotlin 类,即 KClass,我们前面的例子中,就是用的 classifier ==String::class 来判断它是不是 String 类型的;
arguments类型的类型参数,看起来好像有点绕,其实它就是这个类型的泛型参数
isMarkedNullable是否在源代码中标记为可空类型,即这个类型的后面有没有“?”修饰

主要参考了以下内容

注解与反射:进阶必备技能

个人学习笔记