我知道使用javassist的反射替代方法,但使用javassist有点复杂。而且由于koltin中的lambda或其他一些特性,javassist有时候效果不佳。那么有没有其他方法来迭代数据类的所有字段而不使用反射。
答案 0 :(得分:2)
有两种方法。第一个相对容易,基本上就是注释中提到的内容:假设您知道有多少个字段,可以将其拆包并将其放入列表中,然后遍历这些字段。或者直接使用它们:
data class Test(val x: String, val y: String) {
fun getData() : List<Any> = listOf(x, y)
}
data class Test(val x: String, val y: String)
...
val (x, y) = Test("x", "y")
// And optionally throw those in a list
尽管像这样迭代是一个额外的步骤,但这至少是您可以相对轻松地解压缩数据类的一种方法。
如果您不知道有多少个字段(或者您不想重构),则有两个选择:
第一个是使用反射。但是正如您提到的,您不想要这个。
这留下了第二个更复杂的预处理选项:注解。 请注意,这仅适用于您控制的数据类-除此之外,您还无法使用库/框架编码器的反射或实现。
注释可用于多种用途。其中之一是元数据,还有代码生成。这是一个稍微复杂的选择,并且需要一个附加模块才能正确获得编译顺序。如果编译顺序不正确,您将得到未处理的注释,这有点违反了目的。
我还创建了一个可与Gradle一起使用的版本,但这已在文章结尾处,它是您自己实现该版本的捷径。
请注意,我仅在一个纯Kotlin项目中对此进行了测试-我个人在Java和Kotlin之间存在注释问题(尽管在Lombok中是这样),因此我不保证这样做如果从Java调用,则在编译时工作。还要注意,这很复杂,但是避免了运行时反射。
这里的主要问题是与内存有关。每次您调用该方法时,都会创建一个新列表,这使其与the method used by enums非常相似。
执行10,000次迭代的本地测试还显示,执行我的方法的一致性约为200毫秒,而反射的一致性约为600毫秒。但是,对于一次迭代,我的使用了约20毫秒,而反射使用了400到500毫秒。在一次运行中,反射花费了1500(!)毫秒,而我的方法花费了18毫秒。
另请参见Java Reflection: Why is it so slow?。这似乎也影响了Kotlin。 每次创建新列表时,对内存的影响都是显而易见的,但是也会被收集起来,因此这不会成为一个大问题。
作为参考,用于基准测试的代码(在其余文章之后才有意义):
@AutoUnpack data class ExampleDataClass(val x: String, val y: Int, var m: Boolean)
fun main(a: Array<String>) {
var mine = 0L
var reflect = 0L
// for(i in 0 until 10000) {
var start = System.currentTimeMillis()
val cls = ExampleDataClass("example", 42, false)
for (field in cls) {
println(field)
}
mine += System.currentTimeMillis() - start
start = System.currentTimeMillis()
for (prop in ExampleDataClass::class.memberProperties) {
println("${prop.name} = ${prop.get(cls)}")
}
reflect += System.currentTimeMillis() - start
// }
println(mine)
println(reflect)
}
这基于两个模块:消费者模块和处理器模块。 处理器必须位于单独的模块中 。它需要与使用者分开编译,以使注释正常工作。
首先,您的消费者项目需要注释处理器:
apply plugin: 'kotlin-kapt'
此外,您需要添加存根生成。它抱怨编译时未使用它,但是如果没有它,生成器似乎对我来说就坏了:
kapt {
generateStubs = true
}
现在就可以了,为拆包器创建一个新模块。如果尚未添加Kotlin插件。在此项目中,您不需要注释处理器Gradle插件。这仅是消费者需要的。但是,您确实需要kotlinpoet:
implementation "com.squareup:kotlinpoet:1.2.0"
这是为了简化代码生成本身的各个方面,这是这里的重要部分。
现在,创建注释:
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class AutoUnpack
这几乎是您所需要的。保留设置为源,因为它在运行时没有任何价值,并且仅针对编译时。
接下来,有处理器本身。这有点复杂,请耐心等待。作为参考,它使用javax.*
包进行注释处理。 Android注意:假设您可以在compileOnly
范围内插入Java模块而没有受到Android SDK限制,那么这可能会起作用。如前所述,这主要是针对纯Kotlin; Android可能可以使用,但我尚未测试过。
无论如何,生成器:
因为我找不到在不接触其余部分的情况下将方法生成到类中的方法(并且根据this,这是不可能的),所以我将采用扩展函数生成方法。
您将需要一个class UnpackCodeGenerator : AbstractProcessor()
。在其中,您首先需要两行样板:
override fun getSupportedAnnotationTypes(): MutableSet<String> = mutableSetOf(AutoUnpack::class.java.name)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
继续前进,正在处理中。覆盖过程函数:
override fun process(annotations: MutableSet<out TypeElement>, roundEnv: RoundEnvironment): Boolean {
// Find elements with the annotation
val annotatedElements = roundEnv.getElementsAnnotatedWith(AutoUnpack::class.java)
if(annotatedElements.isEmpty()) {
// Self-explanatory
return false;
}
// Iterate the elements
annotatedElements.forEach { element ->
// Grab the name and package
val name = element.simpleName.toString()
val pkg = processingEnv.elementUtils.getPackageOf(element).toString()
// Then generate the class
generateClass(name,
if (pkg == "unnamed package") "" else pkg, // This is a patch for an issue where classes in the root
// package return package as "unnamed package" rather than empty,
// which breaks syntax because "package unnamed package" isn't legal.
element)
}
// Return true for success
return true;
}
这只是建立了一些更高版本的框架。真正的魔力发生在generateClass
函数中:
private fun generateClass(className: String, pkg: String, element: Element){
val elements = element.enclosedElements
val classVariables = elements
.filter {
val name = if (it.simpleName.contains("\$delegate"))
it.simpleName.toString().substring(0, it.simpleName.indexOf("$"))
else it.simpleName.toString()
it.kind == ElementKind.FIELD // Find fields
&& Modifier.STATIC !in it.modifiers // that aren't static (thanks to sebaslogen for issue #1: https://github.com/LunarWatcher/KClassUnpacker/issues/1)
// Additionally, we have to ignore private fields. Extension functions can't access these, and accessing
// them is a bad idea anyway. Kotlin lets you expose get without exposing set. If you, by default, don't
// allow access to the getter, there's a high chance exposing it is a bad idea.
&& elements.any { getter -> getter.kind == ElementKind.METHOD // find methods
&& getter.simpleName.toString() ==
"get${name[0].toUpperCase().toString() + (if (name.length > 1) name.substring(1) else "")}" // that matches the getter name (by the standard convention)
&& Modifier.PUBLIC in getter.modifiers // that are marked public
}
} // Grab the variables
.map {
// Map the name now. Also supports later filtering
if (it.simpleName.endsWith("\$delegate")) {
// Support by lazy
it.simpleName.subSequence(0, it.simpleName.indexOf("$"))
} else it.simpleName
}
if (classVariables.isEmpty()) return; // Self-explanatory
val file = FileSpec.builder(pkg, className)
.addFunction(FunSpec.builder("iterator") // For automatic unpacking in a for loop
.receiver(element.asType().asTypeName().copy()) // Add it as an extension function of the class
.addStatement("return listOf(${classVariables.joinToString(", ")}).iterator()") // add the return statement. Create a list, push an iterator.
.addModifiers(KModifier.PUBLIC, KModifier.OPERATOR) // This needs to be public. Because it's an iterator, the function also needs the `operator` keyword
.build()
).build()
// Grab the generate directory.
val genDir = processingEnv.options["kapt.kotlin.generated"]!!
// Then write the file.
file.writeTo(File(genDir, "$pkg/${element.simpleName.replace("\\.kt".toRegex(), "")}Generated.kt"))
}
所有相关行都有注释,以解释用法,以防您不了解其用途。
最后,为了使处理器能够处理,您需要注册它。在生成器的模块中,在javax.annotation.processing.Processor
下添加一个名为main/resources/META-INF/services
的文件。在其中输入:
com.package.of.UnpackCodeGenerator
在这里,您需要使用compileOnly
和kapt
进行链接。如果将其作为模块添加到项目中,则可以执行以下操作:
kapt project(":ClassUnpacker")
compileOnly project(":ClassUnpacker")
就像我之前提到的,为了方便起见,我将其捆绑在一个罐子里。它具有与SO使用相同的许可(CC-BY-SA 3.0),并且包含与答案中完全相同的代码(尽管已编译为单个项目)。
如果要使用此版本,只需添加Jitpack存储库:
repositories {
// Other repos here
maven { url 'https://jitpack.io' }
}
并通过以下方式进行连接:
kapt 'com.github.LunarWatcher:KClassUnpacker:v1.0.1'
compileOnly "com.github.LunarWatcher:KClassUnpacker:v1.0.1"
请注意,此处的版本可能不是最新版本:here提供了最新版本列表。帖子中的代码仍然旨在反映存储库,但是版本并不十分重要,以至于每次都不能编辑。
不管最终使用哪种方式获取注释,用法都相对容易:
@AutoUnpack data class ExampleDataClass(val x: String, val y: Int, var m: Boolean)
fun main(a: Array<String>) {
val cls = ExampleDataClass("example", 42, false)
for(field in cls) {
println(field)
}
}
此打印:
example
42
false
现在,您可以使用无反射方式迭代字段。
请注意,本地测试已使用IntelliJ进行了部分测试,但是IntelliJ似乎不喜欢我-我遇到了各种失败的构建,其中命令行中的gradlew clean && gradlew build
可以正常工作。我不确定这是本地问题还是普遍问题,但是如果从IntelliJ进行构建,可能会遇到类似这样的问题。
此外,如果构建失败,则可能会出错。 IntelliJ linter在某些目录的构建目录的顶部构建,因此,如果构建失败并且未生成带有扩展功能的文件,则将导致它显示为错误。当我进行测试(使用两个模块以及来自Jitpack)时,Building通常可以解决此问题。
如果您使用Android Studio或IntelliJ,则还可能必须启用注释处理器设置。
答案 1 :(得分:0)
这是我想到的另一个想法,但不满意...但是它有一些优点和缺点:
声明:
data class Memento(
val testType: TestTypeData,
val notes: String,
val examinationTime: MillisSinceEpoch?,
val administeredBy: String,
val signature: SignatureViewHolder.SignatureData,
val signerName: String,
val signerRole: SignerRole
) : Serializable
在所有字段中重复:
val iterateThroughAllMyFields: Memento = someValue
Memento(
testType = iterateThroughAllMyFields.testType.also { testType ->
// do something with testType
},
notes = iterateThroughAllMyFields.notes.also { notes ->
// do something with notes
},
examinationTime = iterateThroughAllMyFields.examinationTime.also { examinationTime ->
// do something with examinationTime
},
administeredBy = iterateThroughAllMyFields.administeredBy.also { administeredBy ->
// do something with administeredBy
},
signature = iterateThroughAllMyFields.signature.also { signature ->
// do something with signature
},
signerName = iterateThroughAllMyFields.signerName.also { signerName ->
// do something with signerName
},
signerRole = iterateThroughAllMyFields.signerRole.also { signerRole ->
// do something with signerRole
}
)