Is there any way to iterate all fields of a data class without using reflection?
我知道使用javassist的反射的替代方法,但是使用javassist有点复杂。 而且由于lambda或koltin中的某些其他功能,javassist有时不能很好地工作。 因此,有没有其他方法可以不使用反射来迭代数据类的所有字段。
有两种方法。第一个相对容易,基本上就是注释中提到的内容:假设您知道有多少个字段,可以将其拆包并将其放入列表中,然后遍历这些字段。或者直接使用它们:
好。
1 2 3 | data class Test(val x: String, val y: String) { fun getData() : List<Any> = listOf(x, y) } |
1 2 3 4 5 | 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调用,我不保证这在编译时会起作用。还要注意,这很复杂,但是避免了运行时反射。
好。
说明
这里的主要问题是一定的内存问题。每次调用该方法时,都会创建一个新列表,这使其与枚举所使用的方法非常相似。
好。
经过10000次迭代的本地测试还表明,执行我的方法的总体一致性约为200毫秒,而反射的一致性约为600毫秒。但是,对于一次迭代,我的使用约20毫秒,而反射使用400到500毫秒。在一次运行中,反射花费了1500(!)毫秒,而我的方法花费了18毫秒。
好。
另请参见Java Reflection:为什么这么慢?这似乎也影响了Kotlin。
每次创建新列表时,对内存的影响可能是显而易见的,但是也会被收集,因此这不会成为一个大问题。
好。
作为参考,用于基准测试的代码(在其余文章之后才有意义):
好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @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) } |
从头开始设置
这基于两个模块:一个消费者模块和一个处理器模块。处理器必须位于单独的模块中。它需要与使用者分开编译,以使注释正常工作。
好。
首先,您的使用者项目需要注释处理器:
好。
1 | apply plugin: 'kotlin-kapt' |
此外,您需要添加存根生成。它抱怨编译时未使用它,但是如果没有它,生成器似乎对我来说就坏了:
好。
1 2 3 | kapt { generateStubs = true } |
现在就可以了,为拆包器创建一个新模块。如果尚未添加Kotlin插件。在此项目中,您不需要注释处理器Gradle插件。这仅是消费者需要的。但是,您确实需要kotlinpoet:
好。
1 | implementation"com.squareup:kotlinpoet:1.2.0" |
这是为了简化代码生成本身的各个方面,这是这里的重要部分。
好。
现在,创建注释:
好。
1 2 3 | @Retention(AnnotationRetention.SOURCE) @Target(AnnotationTarget.CLASS) annotation class AutoUnpack |
这几乎就是您所需要的。保留设置为源,因为它在运行时没有任何价值,并且仅针对编译时。
好。
接下来是处理器本身。这有点复杂,请耐心等待。作为参考,它使用
好。
无论如何,生成器:
好。
因为我找不到在不接触其余部分的情况下将方法生成到类中的方法(并且根据此方法,这是不可能的),所以我将采用扩展函数生成方法。
好。
您需要一个
好。
1 2 | override fun getSupportedAnnotationTypes(): MutableSet<String> = mutableSetOf(AutoUnpack::class.java.name) override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() |
继续,有处理。覆盖流程功能:
好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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; } |
这只是建立了一些更高版本的框架。真正的魔力发生在
好。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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")) } |
所有相关行都有注释,以解释用法,以防您不了解其用途。
好。
最后,为了使处理器能够处理,您需要对其进行注册。在生成器的模块中,在
好。
1 | com.package.of.UnpackCodeGenerator |
在这里,您需要使用
好。
1 2 | kapt project(":ClassUnpacker") compileOnly project(":ClassUnpacker") |
备用源设置:
就像我之前提到的,为了方便起见,我将其捆绑在一个罐子里。它与SO使用相同的许可证(CC-BY-SA 3.0),并且包含与答案中完全相同的代码(尽管已编译到单个项目中)。
好。
如果要使用此版本,只需添加Jitpack存储库:
好。
1 2 3 4 | repositories { // Other repos here maven { url 'https://jitpack.io' } } |
并将其与:
好。
1 2 | kapt 'com.github.LunarWatcher:KClassUnpacker:v1.0.1' compileOnly"com.github.LunarWatcher:KClassUnpacker:v1.0.1" |
请注意,此处的版本可能不是最新的:此处提供了最新版本的列表。帖子中的代码仍然旨在反映仓库,但是版本并不十分重要,以至于每次都不能编辑。
好。
用法
无论最终使用哪种方式获取注释,用法都相对容易:
好。
1 2 3 4 5 6 7 8 | @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) } } |
打印:
好。
1 2 3 | example 42 false |
现在您有了一种无反射的迭代字段的方法。
好。
请注意,本地测试已使用IntelliJ进行了部分测试,但是IntelliJ似乎并不喜欢我-我有各种失败的构建,其中命令行中的
好。
另外,如果构建失败,则可能会出错。 IntelliJ linter在某些目录的构建目录的顶部构建,因此,如果构建失败并且未生成具有扩展功能的文件,则将导致它显示为错误。 当我测试时(使用这两个模块以及来自Jitpack),Building通常可以解决此问题。
好。
如果您使用的是Android Studio或IntelliJ,则可能还必须启用注释处理器设置。
好。
好。
这是我想到的另一个想法,但不满意...但是它有一些优点和缺点:
-
优点:
- 在数据类中添加字段/从数据类中删除字段会导致字段迭代站点出现编译器错误
- 无需样板代码
-
缺点:
- 如果为参数定义了默认值,则无法使用
宣言:
1 2 3 4 5 6 7 8 9 | 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 |
遍历所有字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 } ) |