ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kotlin反射判断属性泛型:KType与KTypeParameter详解

Kotlin反射判断属性泛型:KType与KTypeParameter详解 先把自己扔回一个常见场景你在写一个通用解析框架或者自制一个依赖注入容器反正就是那种需要扫描类的字段、逐个判断类型的活儿。Kotlin反射包一引入property.returnType拿回来一看怎么有些类型长得和别人不一样T、ListT、ClassT这堆东西和普通的String、Int到底该怎么区分我说的就是Kotlin泛型判断里最基础也最容易被绕晕的问题怎么判断一个属性的类型是不是泛型。这篇东西不聊虚的直接围绕“属性类型判断”这个点拆开揉碎。你会搞明白Kotlin反射里KType是怎么构成的、KClass和KTypeParameter有什么区别、怎么写一个能递归识别ListT这种嵌套泛型的判断函数以及最常见的几个反射大坑。适合正在写框架、做代码生成器或者单纯想把Kotlin反射搞明白的人。1. 把问题翻译成人话你说的“泛型”到底是哪一种1.1 泛型参数和参数化类型是两码事很多人卡在这一步是因为“属性类型是泛型”这句话天然有歧义。我至少见过三种理解方式对应的判断逻辑完全不同。第一种属性类型直接就是一个类型参数T。比如class ContainerT { val value: T }这里的value类型就是TKotlin反射里它的classifier会是一个KTypeParameter。第二种属性类型是一个带有泛型参数的类型比如val items: ListT或者val items: ListString。这种类型在反射里的classifier是List本身不是类型参数但它的arguments里面装着T或者String。第三种你其实想知道的是某个泛型参数最终被具体类型填成了什么。比如有个类UserHandler继承BaseHandlerUser你想知道父类里的T是不是User。这属于“解析实际类型参数”的范畴跟单纯的“是不是泛型”已经不是一个难度等级。所以动手写代码之前先问自己一句我要判断的是哪一种如果连问题的粒度都没定好后面写出的判断函数大概率是一坨只在特定场景下才能跑的代码。1.2 什么时候你会需要这个判断判断属性类型是否为泛型最典型的场景是写通用框架。我做协议解析器的时候需要把网络返回的JSON自动填充进数据模型模型里经常会出现PageResultT、ListData、MapString, ListItem这种字段。解析器就必须知道这个字段是具体类型还是泛型占位如果是泛型占位我暂时没法直接实例化得放到后面等具体类型信息补全了再处理。依赖注入容器也一样。构造函数参数如果是UserService我直接按类查找Bean如果参数是ListUserService我不仅要找到UserService的Bean还得把多个Bean组装成List塞进去。这种“谁的参数带泛型”的判断躲不开反射。只要你在跟反射、序列化、代码生成打交道迟早都会撞上这题。与其到时候在Stack Overflow上翻半天不如现在就把原理搞清楚。2. 判断前必须搞懂的三个Kotlin反射概念2.1 KType的构成classifier和argumentsKotlin反射里一个属性的类型对应的类是KType。它负责描述“类型的完整模样”核心信息就两块classifier和arguments。classifier描述的是这个类型的“主体”是谁。拿ListString举例classifier就是List::class。拿String举例classifier就是String::class。拿T举例classifier就变成了类型参数T对应的KTypeParameter对象而不是某个具体的KClass。arguments描述的是类型参数的实际填充内容。ListString的arguments就包含一个元素这个元素指向StringMapString, Int的arguments包含两个元素分别指向String和Int。我习惯这样类比classifier是“盒子”的标签标签上写着“这是List还是String还是T”arguments是盒子里面装的东西装的是“List里面到底是什么”。判断属性是否泛型本质就是看这两块信息里有没有藏着类型参数。2.2 classifier里藏着KClass和KTypeParameter两条分支这是整个判断逻辑的核心分支点。拿到一个KType之后你去看它的classifier只有两种结果是KClass说明这一层是某个具体的类比如String、List、User这种不是类型参数本身。是KTypeParameter说明这一层直接就是一个泛型参数比如T、E这种。判断代码只有一行if (type.classifier is KTypeParameter) { // 这一层就是泛型参数 }但是光判断这一层不够。ListT这个类型的classifier是List不是KTypeParameter可它内部确实带着T。如果只做一层判断ListT会被误判成“不是泛型”这在很多场景下都不可接受。所以要用递归的方式把arguments也翻一遍。至于ListString这种它的arguments里是具体的String翻到底也找不到KTypeParameter自然就判成“不包含泛型”逻辑是自洽的。2.3 在代码里拿到属性的KType聊完原理先明确最基础的一步怎么拿到一个属性对应的KType。第一种方式是Kotlin反射的属性引用。假设有一个类ContainerT里面声明了若干属性class ContainerT { val value: T? null val items: ListT? null val fixed: ListString? null val name: String val clazz: ClassT? null }遍历成员属性的写法如下import kotlin.reflect.full.declaredMemberProperties fun main() { Container::class.declaredMemberProperties.forEach { prop - val returnType prop.returnType println(${prop.name}: ${returnType}) } }输出大致是value: T? items: kotlin.collections.ListT? fixed: kotlin.collections.ListString? name: kotlin.String clazz: java.lang.ClassT?注意这里的打印结果value的returnType.classifier就是KTypeParameter而items的returnType.classifier是List::class它内部藏着T。这个差异正好对应前面说的两种粒度。第二种方式是用typeOfT()直接构造一个KType这在写单测、验证反射逻辑时特别方便。但注意typeOf只能用于具体类型比如typeOfListString()没问题如果想在泛型函数里写typeOfT()除非把T声明成inline配合reified否则编译器直接不让你过。3. 核心实现写一个函数彻底判断属性类型是否含泛型3.1 基础版只看当前层是不是泛型参数先把最简版本写出来。如果你只需要判断属性类型“是不是类型参数本身”那就很直接。import kotlin.reflect.KType import kotlin.reflect.KTypeParameter fun KType.isTypeParameter(): Boolean { return classifier is KTypeParameter }这个函数的判断粒度很窄只回答一个问题这一层是不是T。用它判断Container里的value会返回true判断items会返回false因为items这一层是List。实际项目中这个函数的用处有限因为大多数时候我们想判断的是“这个属性类型里有没有泛型参数”而不是“它这一层是不是泛型”。3.2 递归版连嵌套泛型List 一起识别把判断范围扩到整个类型结构写成递归版本import kotlin.reflect.KType import kotlin.reflect.KTypeParameter fun KType.containsTypeParameter(): Boolean { if (classifier is KTypeParameter) return true return arguments.any { it.type?.containsTypeParameter() true } }这段代码的意图很清晰先看自己这层如果自己就是类型参数直接返回true否则翻自己的arguments看看有没有哪一层参数里包含了类型参数。注意arguments里的元素类型是KTypeProjection它代表“类型参数位置上的投影”可能是具体类型、泛型参数也可能是*。当遇到List*这种星号投影时投影里的type是null这里用了?.空安全调用和 true兜底不会崩溃也能正确判断成“不包含明确的泛型参数”。用它来跑Container里的四个属性结果就是value: T?classifier是KTypeParameter判为true。items: ListT?classifier是List但arguments里有T所以也是true。fixed: ListString?classifier是Listarguments里是具体String翻遍也没有类型参数判为false。clazz: ClassT?classifier是Classarguments里有T所以true。这个函数已经可以覆盖大部分业务场景了。你问我“ListString算不算泛型”那要看你业务里的定义。如果是想判断“是否需要后续补全类型信息”那ListString其实不需要补全类型信息已经全了containsTypeParameter()返回false是合理行为。3.3 带缓存和star投影处理的完整版本反射调用本身有性能开销如果你的框架会频繁扫描同一个类的属性建议加一层缓存。写一个对象封装起来避免每次判断都重新遍历整个KType结构。import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KType import kotlin.reflect.KTypeParameter object TypeInspector { private val cache ConcurrentHashMapString, Boolean() fun containsTypeParameter(type: KType): Boolean { val key type.toString() cache[key]?.let { return it } return cache.getOrPut(key) { doContainsTypeParameter(type) } } private fun doContainsTypeParameter(type: KType): Boolean { if (type.classifier is KTypeParameter) return true return type.arguments.any { projection - projection.type?.let { doContainsTypeParameter(it) } true } } }用type.toString()做缓存key不算严谨同一个类型理论上应该有同构的toString结果实际用下来足够稳定。如果你想要更健壮的key可以用classifier的名字加上arguments的递归结构去拼但一般情况下没必要。加上缓存后属性上面那个Container的例子可以这样输出fun main() { Container::class.declaredMemberProperties.forEach { prop - val type prop.returnType println(${prop.name}: containsTypeParameter ${TypeInspector.containsTypeParameter(type)}) } }输出结果value: containsTypeParameter true items: containsTypeParameter true fixed: containsTypeParameter false name: containsTypeParameter false clazz: containsTypeParameter true4. 进阶从“是不是泛型”到“泛型到底被填充成了什么”4.1 override和泛型继承会悄悄替换类型如果只是扫描一个类自己声明的字段上面的判断函数已经够用了。但真实项目里经常遇到继承和override这时候一个重要的陷阱会出现你反射到的属性类型可能不是你想的那个。看这段代码open class BaseT { open val content: T? null } class StringChild : BaseString() { override val content: String? hello }如果直接扫描StringChild::class.declaredMemberProperties拿content属性的returnType得到的会是kotlin.String?而不是T?。原因很简单——这个属性在子类里已经被override成具体类型了。Kotlin编译器在生成字节码时会把override后确定的类型写进签名反射自然拿到的是String。但换个写法就有趣了class ChildT : BaseT() { override val content: T? null }这里ChildT本身还是泛型类content的类型依然写的是T。用Child::class.declaredMemberProperties反射它拿到的returnType.classifier还是KTypeParameter。所以判断“属性类型是否为泛型”之前先想清楚你这个属性是声明在泛型类内部还是被某个具体类型override了不同情况下反射结果完全不同。4.2 解析父接口/父类的实际类型参数现在说第三种理解想知道泛型参数最终被填成了什么。最常见的是处理泛型父类。比如abstract class BaseHandlerT { abstract fun handle(entity: T) } class UserHandler : BaseHandlerUser() { override fun handle(entity: User) {} }我想知道UserHandler里的T对应的是User该怎么做Kotlin反射提供了一条路径直接看UserHandler::class.supertypes里面是UserHandler所有父类型的KType列表其中classifier为BaseHandler::class的那个KType就是参数化之后的父类型。import kotlin.reflect.full.supertypes import kotlin.reflect.KType fun findParameterizedSupertype(clazz: kotlin.reflect.KClass*, target: kotlin.reflect.KClass*): KType? { return clazz.supertypes.firstOrNull { it.classifier target } } fun main() { val superType findParameterizedSupertype(UserHandler::class, BaseHandler::class) println(superType) // BaseHandlerUser println(superType?.arguments?.firstOrNull()?.type) // User }Java反射也有对应的做法而且对很多老框架来说更顺手fun getSuperclassTypeArguments(clazz: Class*): ListType { val superclass clazz.genericSuperclass return if (superclass is ParameterizedType) { superclass.actualTypeArguments.toList() } else { emptyList() } }genericSuperclass返回一个Type如果父类是带泛型参数的这个Type就是ParameterizedType它的actualTypeArguments就是被填充后的类型参数列表。UserHandler的例子中这个列表里就是User::class。如果actualTypeArguments里的元素还实现了TypeVariable接口说明父类的泛型参数没有被完全填充比如class GenericChildT : BaseHandlerT这种那这里拿到的就是T对应的TypeVariable不是具体类。这个判断可以继续往后做比如把TypeVariable和子类的泛型参数对应起来这就是很多框架里“泛型传递解析”的底层原理。4.3 Java反射和Kotlin反射的区别写到这里顺便把Java和Kotlin反射的对应关系理清楚。两者的底层是同一套字节码里的泛型签名信息但API长得不一样。Java反射里java.lang.reflect.Type有三个重要实现Class表示具体类型ParameterizedType表示带泛型参数的类型TypeVariable表示类型参数。Kotlin反射里KClass对应ClassKType大致对应ParameterizedType加一层包装KTypeParameter对应TypeVariable。用Kotlin反射判断类型参数看classifier is KTypeParameter用Java反射判断类型参数看type is TypeVariable。两套体系要做的事是一样的。实际操作的时候我的习惯是能直接用Java反射解决的比如只取类名、拿泛型父类参数优先用Java反射稳定、依赖少、性能好需要处理Kotlin特有的可空性、协变逆变、reified信息时再用Kotlin反射。反正kotlin-reflect引入之后两套可以混着用没必要死守其中一种。5. 实战现场与高频坑位笔记5.1 一个真实的扫描调用场景结合前面的函数做一个完整的属性扫描器把类型信息全部列出来import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.KTypeParameter import kotlin.reflect.full.declaredMemberProperties fun inspect(clazz: KClass*) { clazz.declaredMemberProperties.forEach { prop - val type prop.returnType val directParam type.classifier is KTypeParameter val nestedParam TypeInspector.containsTypeParameter(type) val argumentInfo type.arguments.joinToString { projection - val inner projection.type?.let { it.toString() } ?: * $inner (variance${projection.variance}) } println(${prop.name} | directlyParam$directParam | containsParam$nestedParam | args[$argumentInfo]) } }拿前面ContainerT跑一遍输出大概是value | directlyParamtrue | containsParamtrue | args[] items | directlyParamfalse | containsParamtrue | args[T? (varianceINVARIANT)] fixed | directlyParamfalse | containsParamfalse | args[kotlin.String (varianceINVARIANT)] name | directlyParamfalse | containsParamfalse | args[] clazz | directlyParamfalse | containsParamtrue | args[T (varianceINVARIANT)]这个输出表放在文档里当参考很直观。实际写框架时directParam和containsParam两条判断往往都要保留前者用于“能不能直接实例化默认值”后者用于“这个类型是否还需要后续泛型信息注入”。5.2 最容易被绕晕的4个坑第一个坑是只判断classifier就下结论。很多新人写判断函数只看classifier is KTypeParameter结果ListT被判成“不是泛型”之后处理PageResultT这种类型时彻底懵了。解决办法就是用递归遍历arguments也就是前面写的containsTypeParameter()。第二个坑是star投影引起的NPE。List*在反射里对应的KTypeProjection它的type是null因为它表示“不确定具体类型”。如果你在遍历arguments时直接写it.type!!.classifier遇到List*立刻炸。记得用it.type?.let { ... }这种空安全写法把它当成“不包含明确泛型参数”处理。第三个坑是R8/ProGuard把泛型签名干掉了。在Android项目里跑反射默认开启代码混淆后类的泛型签名Signature属性可能被移除拿到手的returnType.arguments就变成空列表了。这不是代码写错是编译配置问题。解决办法是给需要反射的类加上混淆规则保留Signature-keepattributes Signature -keep class com.example.model.** { *; }Signature属性对于泛型反射来说就是命根子删了之后所有泛型判断都会退化。第四个坑是分不清属性声明所在的类。反射属性时用declaredMemberProperties只能拿到当前类自己声明的属性拿不到父类声明的属性。如果你用UserHandler去反射handle方法里的T参数但方法是父类BaseHandler里声明的你需要先解析父类类型参数然后再匹配。很多人栽在这里是因为不知道declaredMemberProperties和memberProperties的区别前者只管当前类后者会包含继承来的public属性但后者的类型解析结果同样会受到override影响。5.3 排查思路速查表现象可能原因排查方向T被识别成了具体类override把类型替换成了具体类型看属性声明在哪个类是否被子类overrideListT判断成“不是泛型”只做了单层判断改用递归遍历argumentsList*直接抛NPEstar投影的type为null用空安全访问不直接调!!反射拿不到泛型参数混淆移除了Signature属性检查R8/ProGuard规则保留SignaturetypeOfT()编译不过T不是reified只能配合inline fun使用或改用反射属性在父类里扫描不到用了declaredMemberProperties换成memberProperties或先解析父类再自行递归这张表我建议收藏。遇到反射泛型问题先对号入座再决定往哪个方向查比自己从零debug快得多。最后再分享一点个人习惯写这类类型判断函数时我会先把“泛型”这个词在业务里的语义定义清楚是要判断“这一层是类型参数”还是要判断“整个类型结构里藏了类型参数”还是“把泛型参数解析成具体类型”。三者代码完全不同但经常被混在一起讨论。你把粒度定清楚了代码结构自然就清晰了。判断函数本身只做纯逻辑不掺业务真正决定“拿这个判断结果干什么”的代码放到外层去写。以后Kotlin反射API就算有变动需要改的也只是最里面那个纯函数而已。
返回列表