深入理解并学会运用Kotlin注解

公众号「稀有猿诉」

注解(Annotations)允许我们在代码中添加元数据(Meta data),提供代码以外的信息,这些元数据可以在编译时被编译器或其他工具读取和处理。 Kotlin作为一种功能强大且易于使用的多范式通用编程语言,注解(Annotations)是其核心特性之一。在Kotlin中,注解的使用非常广泛,可以用于框架设计、代码生成、测试、依赖注入等多个方面。今天就来学习一下Kotlin中注解的使用方法。

Kotlin是基于JVM的编程语言,并且可以与Java互通使用,因此事先了解一下Java的注解对于学习Kotlin的注解是非常有帮助的。可以阅读一下前面的文章来回顾Java语言的注解

什么是注解

注解是元编程的一种实现方式,它并不直接改变代码,而是为代码提供额外的数据。注解不能单独存在,必须与代码中的其他元素一起使用。在Kotlin中,注解要使用符号『@』后面加一个已定义的注解名字,如『@Deprecated』。注解在Kotlin中的使用非常广泛的,相信有过代码经验的同学都至少看过大量的注解。

注解的使用方法

注解的使用是非常的直观的,在需要的代码元素(类,变量,属性,函数,参数等等)加上想要使用的注解就可以了:

Kotlin 复制代码
@Fancy class Foo {
    @Fancy fun baz(@Fancy foo: Int): Int {
        return (@Fancy 1)
    }
}

Kotlin的注解也可以用在lambda上面,这实际上相当于应用于lambda函数生成的函数实例的invoke()上面:

Kotlin 复制代码
annotation class Suspendable

val f = @Suspendable { Fiber.sleep(10) }

注解的使用点目标

由于Kotlin最终要编译成为字节码,运行在JVM上,所以它必须符合Java的规范。但语法上Kotlin与Java还是不一样的,比如一句Kotlin代码可能会相当于Java的好几句,换句话说一个Kotlin语句中的元素可能会对应着Java中的好几个。这可能会带来问题。

注解并不能单独出现,它必须作用到某一个语法上的元素,因为Kotlin语法元素可能会对应着几个Java语法元素,那么注解可能会被用在多个目标元素上面。为了能精确的指定注解的作用目标,可以使用『使用点目标』(use-site targets)来标记具体的目标元素:

Kotlin 复制代码
class Example(@field:Ann val foo,    // annotate Java field
              @get:Ann val bar,      // annotate Java getter
              @param:Ann val quux)   // annotate Java constructor parameter

这里面『Ann』是一个注解,其前面的『field/get/param』就用以指定具体的注解目标元素。可用的使用点目标有这些:

  • file
  • property
  • field
  • get 属性的getter
  • set 属性的setter
  • receiver 扩展函数或者扩展属性的底层对象
  • param 构造函数的参数
  • setparam 属性setter的参数
  • delegate 指存储着受托对象实例的域成员

『receiver』指的是扩展函数发生作用的实例,比如说:

Kotlin 复制代码
fun @receiver:Fancy String.myExtension() { ... }

那么,这个注解『Fancy』将作用于具体调用这个扩展方法myExtension的String实例上面。

这些具体的使用点目标可以精确的指定JVM认识的元素上面,可以发现,它们远比定义注解时的@Target要丰富。如果不指定具体的使用点目标,那么就会按照@Target指定的目标,如果有多个目标,会按如下顺序选择:

  • param
  • property
  • field

兼容Java注解

Kotlin是完全兼容Java注解,也就是说Java中定义的注解,在Kotlin中都可以直接使用。

Java 复制代码
// Java
public @interface Ann {
    int intValue();
    String stringValue();
}
Kotlin 复制代码
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C

虽然可以直接用,但毕竟Kotlin的语法要丰富得多,所以为了避免歧义,要使用前面介绍的使用点目标来精确指定注解的作用目标。

自定义注解

使用关键字『annotation』来声明自定义注解,如:

Kotlin 复制代码
annotation class Fancy

之后就可以使用注解了:

Kotlin 复制代码
@Fancy class Foo {
    @Fancy fun baz(@Fancy foo: Int): Int {
        return (@Fancy 1)
    }
}

光这样声明还不够,还需要定义注解具体的内容,如可修饰的目标和行为特点,这就需要用到元注解(Meta annotations),也即定义注解时所需要的注解。

元注解(Meta annotations)

@MustBeDocumented

用于指定此注解是公开API的一部分,必须包含在文档中。

@Repeatable

允许在同一个地方多次使用注解。

@Target

用于指定此注解可以应用到哪些程序元素上面,如类和接口,函数,属性和表达式。

@Retention

指定注解信息保存到代码生命周期的哪一阶段,编译前,编译时还是运行时。默认值是运行时,也即在运行时注解是可见的。

  • AnnotationRetention.SOURCE - 只在源码过程中保留,并不会出现在编译后的class中(二进制文件中)。
  • AnnotationRetention.BINARY - 会在class中保留,但对于运行时并不可见,也就是通过反射无法得到注解。
  • AnnotationRetention.RUNTIME - 注解会保留到运行时,运行时的操作如反射可以解析注解,这是默认的@Rentention值。

构造方法(Constructors)

与Java很不同的是Kotlin的注解更加的像常规的类(class),注解也可以有构造函数:

Kotlin 复制代码
annotation class Special(val why: String)

@Special("example") class Foo {}

构造函数可以使用的参数包括:

  • 基础数据类型Int,Long,Float和String等
  • 类型原型(即class,如Foo::class)
  • 枚举类型
  • 其他注解类型
  • 由以上类型组成的数组

注意不能有可能为空(如String?)的类型,当然也不可以传递null给注解的构造函数。还有,如果用其他注解作为参数时,注解名字前就不用再加『@』了:

Kotlin 复制代码
annotation class ReplaceWith(val expression: String)

annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""))

注解的实例化(Instantiation)

在Kotlin中可以通过调用注解的构造函数来实例化一个注解来使用。而不必非要像Java那样用反射接口去获取。

Kotlin 复制代码
annotation class InfoMarker(val info: String)

fun processInfo(marker: InfoMarker): Unit = TODO()

fun main(args: Array<String>) {
    if (args.isNotEmpty())
        processInfo(getAnnotationReflective(args))
    else
        processInfo(InfoMarker("default"))
}

注解解析

Kotlin是基于JVM的编程语言,最终要编译成为字节码运行在JVM上面,所以注解的解析与Java语言注解解析是一样的,可以在运行时用反射API来解析注解。关于Java注解解析可以参考另一篇文章,因为运行时注解解析用处并不大,并且也不复杂,看一个简单就可以了:

Kotlin 复制代码
class Item(
  @Positive val amount: Float, 
  @AllowedNames(["Alice", "Bob"]) val name: String)
  
val fields = item::class.java.declaredFields
for (field in fields) {
    for (annotation in field.annotations) {
        if (field.isAnnotationPresent(AllowedNames::class.java)) {
            val allowedNames = field.getAnnotation(AllowedNames::class.java)?.names
         }
    }
}

注解处理器

注解是元编程的一种方式,它最大的威力是在编译前进行代码处理和代码生成。除了注解的定义和使用外,更为关键的注解的处理需要用到注解处理器(Annotation Processor),并且要配合编译器插件kaptKSP来使用。

需要注意,因为注解是JVM支持的特性,在编译时需要借助javac编译器,所以只有运行目标是JVM时注解才有效。因为Kotlin是支持编译为不同运行目标的,除了JVM外,还有JavaScript和Native。

实现注解处理器

与Java的注解处理器类似,在定义好注解后,还需要实现一个注解处理器,以对注解进行处理。一般情况下实现AbstractProcessor就可以了。在其process方法中过滤出来想要处理的注解进行处理,比如使用KotlinPoet生成代码。

另外,还要注意,注解处理器必须在一个单独的module中,然后添加为使用此注解module的依赖,这是因为注解的处理是在编译前,所以处理器需要在正式编译前就已经编译好。

kotlin 复制代码
package net.toughcoder

import javax.annotation.processing.*
import javax.lang.model.element.*
import javax.tools.Diagnostic

@SupportedAnnotationTypes("com.example.MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class MyAnnotationProcessor : AbstractProcessor() {

    override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
        for (annotation : annotations) {
            for (element : roundEnv.getElementsAnnotatedWith(annotation)) {
                val myAnnotation = element.getAnnotation(MyAnnotation::class.java)
                val message = "Processing element with annotation MyAnnotation(value = ${myAnnotation.value})"
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message, element)
            }
        }
        return true
    }
}

从例子中可以看到,其实Kotlin中的注解处理器(Processor)直接就是用的Java的,所以在用的时候最好加上Java语言的版本。

注册注解处理器

为能正常使用注解处理器,需要把注解处理器放在一个单独的Module里,并作为其他module的依赖,这样能确保它在编译被依赖项时正常使用,被依赖项也即注解使用的地方。

需要在处理器module中与代码平级的文件夹创建resources文件夹,创建一个子文件夹META-INF,再在META-INF创建一个子文件services,在里面创建一个文件名为『javax.annotation.processing.Processor』,然后把实现的注解处理器的完整类名,写在这个文件的第一行:

Kotlin 复制代码
// file: resources/META-INF/services/javax.annotation.processing.Processor
net.toughcoder.MyAnnotationProcessor

使用注解处理器

需要做两个事情,一个是把注解处理器添加为其他项目或者module的依赖。然后再用专门处理注解处理器的编译器插件使用注解处理器。

gradle 复制代码
dependencies {
    implementation(kotlin('stdlib'))
    kapt 'net.toughcoder:my-annotation-processor:1.0.0'
}

kapt {
    useBuildCache = true
    annotationProcessors = ['net.toughcoder:my-annotation-processor:1.0.0']
}

总结

本文介绍了Kotlin中注解的基本语法、使用方法和处理过程。通过自定义注解处理器,我们可以在编译时处理注解并生成相应的代码或执行其他任务。注解是Kotlin编程中的核心特性,它可以帮助我们提高代码的可读性、可维护性和可扩展性。大部分的注解都在编译时,也不会对性能产生影响,所以可以放心大胆的用注解来提升开发效率。

参考资料

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!

相关推荐
阿巴斯甜16 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker17 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952718 小时前
Andorid Google 登录接入文档
android
黄林晴19 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_2 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android