《Kotlin核心编程》阅读笔记
第八章 元编程
Java的反射只是元编程的一种方式。
示例:将data class 转换成 Map的例子。
data class User (val name:String,val age:Int){
fun toMap(a:User):Map<String,Any> {
return hashMapOf("name" to name,"age" to age)
}
}
这样实现有一个缺点:对每一个新的类型我们都需要重复实现toMap函数,因为每个类型都拥有不同的属性。
程序和数据
通过反射实现:
data class User (val name:String,val age:Int){
fun toMap():Map<String,Any> {
return hashMapOf("name" to name,"age" to age)
}
}
object Mapper{
fun <A:Any> toMap(a:A) : Map<String,Any?> {
//获取A中的所有属性
return a::class.memberProperties.map{ m->
val p = m as KProperty<*>
p.name to p.call(a)
}.toMap()
}
fun <A:Any> toMap1(a:A) = run { //获取A中的所有属性
a::class.memberProperties.map{ m->
val p = m as KProperty<*>
p.name to p.call(a)
}.toMap()
}
}
fun main() {
for (entry1 in User("Kar",19).toMap()) {
println("${entry1.key}---->${entry1.value}")
}
for (entry in Mapper.toMap(User("Jack",20))) {
println("${entry.key}---->${entry.value}")
}
for (entry in Mapper.toMap1(User("Tom",18))) {
println("${entry.key}---->${entry.value}")
}
}
上面代码中的两个toMap方法,是类似的。
这里的 a::class 的类型是KClass,是Kotlin中描述类型的类型(也称为metaclass);在传入参数类型为User时,a::class则可以描述User类型的数据。这样描述数据的数据就称为元数据。
什么是元编程
操作元数据的编程就是元编程。
程序即是数据,数据即是程序。这句话包含两个意思:前半句指的是访问描述程序的数据,如我们通过反射获取类型信息。后半句则是指:将这些数据转化成对应的程序,也就是所谓的代码生成。
元编程就像高阶函数一样,是一种更高阶的抽象,高阶函数将函数作为输入或输出,而元编程则是将程序本身作为输入或输出。
常见的元编程技术
- 运行时通过API暴露程序信息
反射; - 动态执行代码
多见于脚本语言。如JavaScript就有eval函数,可以动态地将文本作为代码执行。 - 通过外部程序实现目的
如编译器,在将源文件解析成AST之后,可以针对这些AST做各种转化。这种实现思路最典型的例子是我们常常谈论的语法糖,编译器会将这部分代码AST转化为相应的等价的AST,这个过程通常被称为desuger(解语法糖)。
AST:抽象语法树是源代码语法结构的一种抽象表示;它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每一个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于if-condition-then 这样的条件跳转语句可以使用带有两个分支的节点来表示。
反射,有时候也称为自反,是指元语言(即前文提到的描述程序的数据结构)和要描述的语言是同一种语言的特性。
除了运行时反射之外,也有许多语言支持编译期反射,编译期反射通常需要宏等技术结合实现,编译器将当前程序的信息作为输入传入给宏,将其结果作为程序的一部分。
宏:Koltin不支持宏。
模板元编程:和Kotlin关系并不大。
Kotlin的反射
kotlin和Java 反射
Kotlin的KClass 和Java的Class 可以看作同一个含义的类型,并且通过.java 和 .kotlin方法在KClass 和Class 之间相互转化。
Kotlin中的KCallable 和 Java 的 AccessiableObject 都可以理解为可调用元素。Java中构造方法为一个独立的类型,而Kotlin则统一作为KFunction处理。
Koltin中的KProperty 和Java的Filed 不太相同,Koltin的KProperty 通常指相应的Getter 和 Setter (只有可变属性Setter)整体作为一个KProperty (通常情况Kotlin并不存在字段的概念),而Java的Field 通常仅仅指字段本身。
koltin的KClass
KCLass的特别属性或函数
特别属性或函数 | 含义 |
---|---|
isCompanion | 是否伴生对象 |
isData | 是否数据类 |
isSealed | 是否密封类 |
objectInstance | object实例(如果是object) |
companionObjectInstance | 伴生对象实例 |
declareMemberExtensionFunctions | 扩展函数 |
declareMemberExtensionProperties | 扩展属性 |
memberExtensionFunction | 本类及超类扩展函数 |
memberExtensionProperties | 本类及超类扩展属性 |
starProjectedType | 泛型通配类型 |
示例:获取object实例
sealed class Nat {
companion object{ object Zero:Nat()}
val Companion._0
get() = Zero
fun <A:Nat> Succ<A>.proceed():A{
return this.prev
}
}
data class Succ<N :Nat>(val prev:N):Nat()
fun <A:Nat> Nat.plus(other:A):Nat = when{
other is Succ<*> -> Succ(plus(other.prev))
else -> this
}
fun main() {
println(Nat.Companion::class.isCompanion) //true
println(Nat::class.isSealed) //true
println(Nat.Companion::class.objectInstance)//chapter8.Nat$Companion@646be2c3
println(Nat::class.companionObjectInstance) //chapter8.Nat$Companion@646be2c3
println(Nat::class.declaredMemberExtensionFunctions.map { it.name }) // [proceed]
println(Nat::class.declaredMemberExtensionProperties.map { it.name }) //[_0]'
println(Succ::class.declaredMemberExtensionProperties.map { it.name }) //[]
println(Succ::class.declaredMemberExtensionFunctions.map { it.name }) //[]
println(Succ::class.declaredMemberExtensionProperties.map{it.name}) //[]
println(Succ::class.memberExtensionProperties.map { it.name }) //[_0]
println(Succ::class.memberExtensionFunctions.map { it.name }) // [proceed]
println(Succ::class.starProjectedType) //chapter8.Succ<*>
println(Nat::class.memberExtensionFunctions.map { it.name }) //[proceed]
}
kotlin的KCallable
如何获取Class的成员:通过members 方法,它的返回值是一个Collection<KCallable<*>>。
KCallable提供的API
KCallable提供的API | 含义 |
---|---|
isAbstract:Boolean< KParameter> | 此KCallable 是否为抽象的 |
isFinal:Boolean | 此KCallable 是否为final |
isOpen:Boolean | 此KCallable 是否为open |
name:string | 此KCallable 的名称 |
parameters :List< KParameter> | 调用此KCallable需要的参数 |
returnType:KType | 此KCallable 的返回类型 |
typeParameters :List< KTypeParameter> | 此KCallable 的类型参数 |
visibility:KVisibility? | 此KCallable 的可见性 |
call(vararg args:Any?):R | 给定参数调用此KCallable |
fun main() {
val _1 = Succ(Nat.Companion.Zero)
val preceed = _1::class.members.find { it.name=="proceed" }
println(preceed?.call(_1,_1) == Nat.Companion.Zero)
}
上面代码通过反射执行Succ的扩展函数proceed,这里传入了两个Nat对象,因为执行扩展函数时,除了参数实例外,需要传入接受者实例。
在Kotlin中,并不是所有的属性都是可变的,因此我们只能对那些可变的属性进行修改操作。
KMutableProperty 是 KProperty 的一个子类。使用When表达式来识别属性。
data class Person(val name:String,val age:Int,var address:String)
fun KMutablePropertyShow(){
val p = Person("YanLq",22,"SWPU")
val props = p::class.memberProperties
for(prop in props){
when(prop){
is KMutableProperty<*> -> prop.setter.call(p,"DZ")
else -> prop.call(p)
}
}
println(p.address)
}
fun main() {
KMutablePropertyShow()
}
获取参数信息
Koltin把参数分为3个类别,分别是函数的参数(KParameter),函数的返回值(KType)以及类型参数(KTypeParameter)
-
KParameter
使用KCallable.parameters 即可获取一个List< KParameter>,它代表的是函数(包括扩展函数)的参数。
API | 描述 |
---|---|
index:Int | 返回该参数在参数列表里面的index |
isOptiona:Boolean | 该参数是否为Optional |
isVarags:Boolean | 该参数是否为 varags |
kind:Kind | 该参数的kind |
name:String? | 该参数的名称 |
type:KType | 该参数的类型 |
fun KParameterShow(){
for(c in Person::class.members){
print("${c.name} ->")
for( p in c.parameters){
print("${p.type} --")
}
println()
}
}
fun main() {
KParameterShow()
}
- KType
获取KCallable的返回值类型。每一个KCallable 都可以使用returnType来获取返回值类型,它的结果类型是KType。
API | 描述 |
---|---|
arguments:List< KtypeProjection> | 该类型的类型参数 |
classifier:KClassifier | 该类型在类声明层面的参数,如该类型为List< String>,那么通过classifier 得到结果为List(忽略类型参数) |
isMarkedNullable:Boolean | 该类型是否标记为可空类型 |
data class Person(val name:String,val age:Int,var address:String){
fun friendsName():List<String>{
return listOf("Yison","Jilen")
}
}
fun main() {
Person::class.members.forEach{
println("${it.name} -> ${it.returnType.classifier}")
}
}
>>>
address -> class kotlin.String
age -> class kotlin.Int
name -> class kotlin.String
component1 -> class kotlin.String
component2 -> class kotlin.Int
component3 -> class kotlin.String
copy -> class chapter8.Person
equals -> class kotlin.Boolean
friendsName -> class kotlin.collections.List
hashCode -> class kotlin.Int
toString -> class kotlin.String
classifier API 其实就是获取该参数在类层面对应的类型。
- KTypeParameter
对于函数和类来说,还有一个重要的参数——类型参数,在KClass 和 KCallable 中我们可以通过typeParameters 来获取 class 和 callable 的类型参数,它的返回结果集是List< KTypeParameter>,不存在类型参数时就返回一个空的List。
data class Person(val name:String,val age:Int,var address:String){
fun friendsName():List<String>{
return listOf("Yison","Jilen")
}
fun <A> get (a:A):A{
return a
}
}
fun KTypeParameterShow(){
for(c in Person::class.members){
if(c.name .equals("get")){
println(c.typeParameters)
}
}
val list = listOf<String>("How")
println(list::class.typeParameters)
}
fun main() {
KTypeParameterShow()
}
>>>
[A]
[E]
Kotlin 注解
Kotlin的注解创建语法:
annotation class FooAnnotation(val bar:String)
注解koltin.Metadata 这是实现Kotlin大部分独特特性反射的关键,Koltin将这些信息直接以注解形式存储在字节码文件中,以便运行时发射可以获取这些数据。
和Java一样注解的参数只能是常量,并且仅支持下列类型:
- 与Java对应的基本类型
- 字符串
- Class对象(KClass 或者Java的Class)
- 其他注解
- 上述类型数组。基本类型数组需要指定为对应的XXXArray,如:IntArray而不是Array< Int>/
标注在注解上的注解我们称之为元注解。Java有下列5个元注解。
- Documented文档(通常是API文档)中必须出现给注解。
- Inherited 如果超类标注了该类型,那么其子类型也将自动标注该注解而无需指定。
- Repeatable 这个注解在同一位置可以出现多次。
- Retention 表述注解用途。有三种取值。
- Sourc 仅在源代码中存在,编译后的class文件不包含该注解信息。
- CLASS class文件中存在该注解,但不能被反射读取。
- RUNTIME 注解信息同样保存在class文件中并且可以在运行时通过反射获取。
- Target 表明注解可应用于何处。
Kotlin也有相应类似的元注解在kotlin.annotation包下。
Kotlin和Java的注解整体上保持一致。Koltin目前不支持Inherited。
无处不在的注解
Kotlin(AnnotationTarget) | Java(Target) | 说明 |
---|---|---|
CLASS | TYPE | 作用于类 |
ANNOTATION_CLASS | ANNOTATION_TYPE | 作用于注解本身(即元注解) |
TYPE_PARAMETER | TYPE_PARAMETER | 作用于类型参数 |
PROPERTY | NA | 作用于属性 |
FIELD | FIELD | 作用于字段(属性通常包含字段Getter 以及 Setter) |
LOCAL_VARIABLE | LOCAL_VARIABLE | 作用于局部变量 |
VALUE_PARAMETER | NA | 作用于val 参数 |
CONSTRUCTOR | CONSTRUCTOR | 作用于构造函数 |
FUNCTION | METHOD | 作用于函数(Java只有method) |
PROPERTY_GETTER | NA | 作用于Getter |
PROPERTY_SETTER | NA | 作用于Setter |
TYPE | TYPE_USE | 作用于类型 |
EXPRESSION | NA | 作用于表达式 |
FILE | PACKAGE | 作用于文件开头/包声明 |
TYPEALIAS | NA | 作用于类型别名 |
annotation class Cache(val namespace:String,val expires:Int)
annotation class CacheKey(val keyName:String,val buckets:IntArray)
@Cache(namespace = "hero",expires = 3600)
data class Hero(
@CacheKey(keyName = "heroName",buckets = intArrayOf(1,2,3))
val name:String,
val attack:Int,
val defense:Int,
val initHp:Int
)
精准控制注解位置
假设我们有注解 annotation class CacheKey。
精准的注释控住语法:
用法 | 含义 |
---|---|
@file:CacheKey | CacheKey注解作用于文件 |
@property:CacheKey | CacheKey注解作用于属性 |
@field:CacheKey | CacheKey注解作用于字段 |
@get:CacheKey | CacheKey注解作用于Getter |
@set:CacheKey | CacheKey注解作用于Setter |
@receiver:CacheKey | CacheKey注解作用于扩展函数或属性 |
@param:CacheKey | CacheKey注解作用于构造函数参数 |
@setParam:CacheKey | CacheKey注解作用Setter的参数 |
@delegate:CacheKey | CacheKey注解作用于存储代理实例的字段 |
@Cache(namespace = "hero",expires = 3600)
data class Hero(
@property:CacheKey(keyName = "heroName",buckets = [1, 2, 3])
val name:String,
@field:CacheKey(keyName = "atk",buckets = [1, 2, 3])
val attack:Int,
@get:CacheKey(keyName = "def",buckets = [1, 2, 3])
val defense:Int,
val initHp:Int
)
获取注解信息
代码标记上注解以后,注解本身也成为了代码的一部分。我们可以获取注解信息。
- 通过反射获取注解信息
但是前提是这个注解的Retention 标注为Runtime 或者没有显示注定(默认为Runtime)。
annotation class Cache(val namespace:String,val expires:Int)
annotation class CacheKey(val keyName:String,val buckets:IntArray)
@Cache(namespace = "hero",expires = 3600)
data class Hero(
@CacheKey(keyName = "heroName",buckets = [1, 2, 3])
val name:String,
val attack:Int,
val defense:Int,
val initHp:Int
)
fun main(args: Array<String>) {
val cacheAnnotation = Hero::class.annotations.find{it is Cache } as Cache?
println("namespace ->${cacheAnnotation?.namespace}")
println("expires ->${cacheAnnotation?.expires}")
}
>>>
namespace hero
expires 3600
通过反射获取注解信息是在运行时发生的。注解标准位置也会影响注解信息的获取。
- 注解处理器
JSR269 引入了注解处理器(annotation processors),允许在编译过程中挂钩子实现代码生成,得益于此,如dagger 之类的框架实现了编译时依赖注入这样原本只能通过运行时反射支持的特性。
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.element.ElementKind
import javax.lang.model.element.TypeElement
import kotlin.reflect.full.memberProperties
annotation class MapperAnnotation
class MapperProcessor :AbstractProcessor() {
private fun genMapperClass(pkg:String,clazzName:String,props:List<String>):String{
return """
package $pkg
import java.utils.*;
public class ${clazzName}Mapper{
public Map<String,Object> toMap($clazzName a){
Map<String,Object> m = new HashMap<String,Object>();
${
props.map {
"m.put(\"${it}\",a.${it})"
}
}
}
}
""".trimIndent()
}
override fun process(annotations: MutableSet<out TypeElement>?, roundEnv: RoundEnvironment?): Boolean {
val el = roundEnv?.getElementsAnnotatedWith(MapperAnnotation::class.java)?.firstOrNull()
if(el?.kind == ElementKind.CLASS){
val pkg = el.javaClass.`package`.name
val cls = el.javaClass.simpleName
val props = el.javaClass.kotlin.memberProperties.map { it.name}
val mapperClass = genMapperClass(pkg,cls,props)
val jfo = processingEnv.filer.createSourceFile("${cls}Mapper")
val writer = jfo.openWriter()
writer.write(mapperClass)
writer.close()
}
return true
}
}
我还不太会注解。不知道咋运行。