《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是否密封类
objectInstanceobject实例(如果是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)

  1. 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()
}

  1. 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 其实就是获取该参数在类层面对应的类型。

  1. 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)说明
CLASSTYPE作用于类
ANNOTATION_CLASSANNOTATION_TYPE作用于注解本身(即元注解)
TYPE_PARAMETERTYPE_PARAMETER作用于类型参数
PROPERTYNA作用于属性
FIELDFIELD作用于字段(属性通常包含字段Getter 以及 Setter)
LOCAL_VARIABLELOCAL_VARIABLE作用于局部变量
VALUE_PARAMETERNA作用于val 参数
CONSTRUCTORCONSTRUCTOR作用于构造函数
FUNCTIONMETHOD作用于函数(Java只有method)
PROPERTY_GETTERNA作用于Getter
PROPERTY_SETTERNA作用于Setter
TYPETYPE_USE作用于类型
EXPRESSIONNA作用于表达式
FILEPACKAGE作用于文件开头/包声明
TYPEALIASNA作用于类型别名
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:CacheKeyCacheKey注解作用于文件
@property:CacheKeyCacheKey注解作用于属性
@field:CacheKeyCacheKey注解作用于字段
@get:CacheKeyCacheKey注解作用于Getter
@set:CacheKeyCacheKey注解作用于Setter
@receiver:CacheKeyCacheKey注解作用于扩展函数或属性
@param:CacheKeyCacheKey注解作用于构造函数参数
@setParam:CacheKeyCacheKey注解作用Setter的参数
@delegate:CacheKeyCacheKey注解作用于存储代理实例的字段
@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
)
获取注解信息

代码标记上注解以后,注解本身也成为了代码的一部分。我们可以获取注解信息。

  1. 通过反射获取注解信息
    但是前提是这个注解的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

通过反射获取注解信息是在运行时发生的。注解标准位置也会影响注解信息的获取。

  1. 注解处理器
    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
   }
}

我还不太会注解。不知道咋运行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值