【问题标题】:Is there any way to iterate all fields of a data class without using reflection?有没有办法在不使用反射的情况下迭代数据类的所有字段?
【发布时间】:2017-12-09 16:04:39
【问题描述】:

我知道使用 javassist 的反射替代方案,但使用 javassist 有点复杂。由于 lambda 或 koltin 中的一些其他特性,javassist 有时无法正常工作。那么有没有其他方法可以在不使用反射的情况下迭代数据类的所有字段。

【问题讨论】:

  • kotlinlang.org/docs/reference/data-classes.html: componentN() 函数对应属性的声明顺序;
  • 如果你不知道属性的数量,你需要反射,见stackoverflow.com/a/38688203/4465208
  • 你可以使用像val (a, b, c) = Triple(meow1, meow2, meow3)这样的destruct声明
  • 您是否有不想使用反射的原因?反射是专门为此创建的,因为没有其他方法。您找到的任何替代方案都将在引擎盖下使用反射,或者比反射更hacky。正如其他人所提到的,如果您乐于在每次字段更改时手动更新代码,则有一些解决方案。

标签: kotlin


【解决方案1】:

有两种方法。第一个相对容易,本质上就是 cmets 中提到的内容:假设您知道有多少字段,您可以将其解包并将其放入列表中,然后对其进行迭代。或者直接使用它们:

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 非常相似。

超过 10000 次迭代的本地测试也表明,执行我的方法大约需要 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"))
}

所有相关行都有 cmets 解释使用,以防您不熟悉它的作用。

最后,为了让处理器进行处理,您需要注册它。在生成器的模块中,在main/resources/META-INF/services 下添加一个名为javax.annotation.processing.Processor 的文件。在那里你写:

com.package.of.UnpackCodeGenerator

从这里,您需要使用compileOnlykapt 链接它。如果你将它作为一个模块添加到你的项目中,你可以这样做:

kapt project(":ClassUnpacker")
compileOnly project(":ClassUnpacker")

替代来源设置:

就像我之前提到的,为了方便起见,我将它捆绑到一个罐子里。它与 SO 使用的许可证相同(CC-BY-SA 3.0),并且包含与答案中完全相同的代码(尽管编译到单个项目中)。

如果你想使用这个,只需添加 Jitpack repo:

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 获得。帖子中的代码仍然旨在反映 repo,但版本并没有真正重要到每次都编辑。

用法

无论你最终使用哪种方式来获取注解,使用都相对简单:

@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 &amp;&amp; gradlew build 奇怪地工作正常。我不确定这是本地问题,还是一般问题,但是如果您从 IntelliJ 构建,您可能会遇到类似的问题。

此外,如果构建失败,您可能会收到错误消息。 IntelliJ linter 在某些源的构建目录之上构建,因此如果构建失败并且未生成具有扩展功能的文件,则会导致它显示为错误。在我测试时,构建通常会解决这个问题(使用两个模块和来自 Jitpack)。

如果您使用Android StudioIntelliJ,您可能还必须启用注释处理器设置。

【讨论】:

  • 无法解决:com.github.LunarWatcher:KClassUnpacker:v1.0.2 显示在项目结构对话框中受影响的模块:app
  • @Fortran 你添加了 JitPack 存储库吗? JitPack 的控制台显示应该没问题。如果一切都失败了,所有代码也在答案中(以及链接的仓库)。虽然有一段时间没有接触过代码,但新的更改可能已经破坏了一些东西。 (我不再使用 Java、Kotlin 或 Android)
【解决方案2】:

这是我想出的另一个想法,但不满意......但它有一些优点和缺点:

  • 优点:
    • 在数据类中添加/删除字段会导致字段迭代站点出现编译器错误
    • 不需要样板代码
  • 缺点:
    • 如果为参数定义了默认值,则将不起作用

声明:

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

遍历所有字段(可以直接在调用点使用,也可以应用访问者模式,在accept方法中使用,调用所有访问方法):

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
    }
)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多