【问题标题】:In Scala, how can I programmatically determine the name of the fields of a case class?在 Scala 中,如何以编程方式确定案例类的字段名称?
【发布时间】:2011-06-08 17:00:52
【问题描述】:

在 Scala 中,假设我有一个这样的案例类:

case class Sample(myInt: Int, myString: String)

我有没有办法获得描述案例类参数的Seq[(String, Class[_])],或者更好的是Seq[(String, Manifest)]

【问题讨论】:

    标签: scala reflection case-class


    【解决方案1】:

    我正在回答自己的问题以提供基本解决方案,但我也在寻找替代方案和改进方案。


    一个选项是使用ParaNamer,也与Java 兼容并且不限于案例类。在 Scala 中,另一种选择是解析附加到生成的类文件的 ScalaSig 字节。这两种解决方案都不适用于 REPL。

    这是我从ScalaSig(使用 scalap 和 Scala 2.8.1)中提取字段名称的尝试:

    def valNames[C: ClassManifest]: Seq[(String, Class[_])] = {
      val cls = classManifest[C].erasure
      val ctors = cls.getConstructors
    
      assert(ctors.size == 1, "Class " + cls.getName + " should have only one constructor")
      val sig = ScalaSigParser.parse(cls).getOrElse(error("No ScalaSig for class " + cls.getName + ", make sure it is a top-level case class"))
    
      val classSymbol = sig.parseEntry(0).asInstanceOf[ClassSymbol]
      assert(classSymbol.isCase, "Class " + cls.getName + " is not a case class")
    
      val tableSize = sig.table.size
      val ctorIndex = (1 until tableSize).find { i =>
        sig.parseEntry(i) match {
          case m @ MethodSymbol(SymbolInfo("<init>", owner, _, _, _, _), _) => owner match {
            case sym: SymbolInfoSymbol if sym.index == 0 => true
            case _ => false
          }
          case _ => false
        }
      }.getOrElse(error("Cannot find constructor entry in ScalaSig for class " + cls.getName))
    
      val paramsListBuilder = List.newBuilder[String]
      for (i <- (ctorIndex + 1) until tableSize) {
        sig.parseEntry(i) match {
          case MethodSymbol(SymbolInfo(name, owner, _, _, _, _), _) => owner match {
            case sym: SymbolInfoSymbol if sym.index == ctorIndex => paramsListBuilder += name
            case _ =>
          }
          case _ =>
        }
      }
    
      paramsListBuilder.result zip ctors(0).getParameterTypes
    }
    

    免责声明:我不太了解 ScalaSig 的结构,这应该被视为一种启发式方法。 特别是,这段代码做了以下假设:

    • 案例类只有一个构造函数。
    • 位置零处的签名条目始终是ClassSymbol
    • 类的相关构造函数是第一个MethodEntry,名称为&lt;init&gt;,其所有者的id 为0。
    • 参数名称的所有者是构造函数条目,并且始终位于该条目之后。

    在嵌套案例类上它将失败(因为没有ScalaSig)。

    此方法也只返回 Class 实例而不是 Manifests。

    请随时提出改进建议!

    【讨论】:

    • @ziggystar 您愿意详细说明吗?
    • 嗯,我认为这是我永远不会尝试做的事情。从类文件中读取内容。我希望它可以帮助您节省大量工作。
    • @ziggystar 这类似于 lift-json 提供非常酷的 JSON-to-case-class 序列化,我认为值得讨论。
    • @ziggystar 他不是在读类文件,他只是在做反思。
    【解决方案2】:

    这是一个使用纯 Java 反射的不同解决方案。

    case class Test(unknown1: String, unknown2: Int)
    val test = Test("one", 2)
    
    val names = test.getClass.getDeclaredFields.map(_.getName)
    // In this example, returns Array(unknown1, unknown2).
    

    要获得Seq[(String, Class[_])],您可以这样做:

    val typeMap = test.getClass.getDeclaredMethods.map({
                    x => (x.getName, x.getReturnType)
                  }).toMap[String, Class[_]]
    val pairs = names.map(x => (x, typeMap(x)))
    // In this example, returns Array((unknown1,class java.lang.String), (two,int))
    

    我不确定如何获得Manifests

    【讨论】:

    • getDeclaredFields 确实有效(如果不在字段中,数据将存储在哪里?)并为您提供更有用且不太复杂的 IMO。
    • 嗯,哎呀——这正是我开始这个时所寻找的。 (getFields 是空的,但不知何故我错过了getDeclaredFields。)我指的是 Scala 将类成员呈现为函数,() 是可选的,而不是裸字段,我认为在内部它们必须有一些乱七八糟的名字。但是getFields 方法非常简单,我将彻底改变我的答案。
    • 鉴于您的评论,您似乎可以自己想出这个。没关系——我搜索并找到了这个问题,因为我也需要答案,而且我更愿意从反射 API 中获取它,而不是深入研究类文件。但是您一定有一些理由拒绝 Java 反射解决方案。这是为什么?这样做有什么隐藏的问题吗?
    • 我只对类本身的参数感兴趣,而不是对所有字段感兴趣,并且正在寻找一种只保留它们的方法。 (这些参数也是模式匹配中使用的参数。)
    • 知道了:使用case class Test(x: Int, y: Int) { val z: Int = 3 },上述方法将返回xyz,而不仅仅是xy。模式匹配只关心xy
    【解决方案3】:

    又是我(两年后)。这是使用 Scala 反射的不同解决方案。它的灵感来自blog post,而它本身的灵感来自Stack Overflow exchange。下面的解决方案专门针对上面原始发帖人的问题。

    在一个编译单元(REPL :paste 或已编译的 JAR)中,包含 scala-reflect 作为依赖项并编译以下内容(在 Scala 2.11 中测试,可能在 Scala 2.10 中工作):

    import scala.language.experimental.macros 
    import scala.reflect.macros.blackbox.Context
    
    object CaseClassFieldsExtractor {
      implicit def makeExtractor[T]: CaseClassFieldsExtractor[T] =
        macro makeExtractorImpl[T]
    
      def makeExtractorImpl[T: c.WeakTypeTag](c: Context):
                                  c.Expr[CaseClassFieldsExtractor[T]] = {
        import c.universe._
        val tpe = weakTypeOf[T]
    
        val fields = tpe.decls.collectFirst {
          case m: MethodSymbol if (m.isPrimaryConstructor) => m
        }.get.paramLists.head
    
        val extractParams = fields.map { field =>
          val name = field.asTerm.name
          val fieldName = name.decodedName.toString
          val NullaryMethodType(fieldType) = tpe.decl(name).typeSignature
    
          q"$fieldName -> ${fieldType.toString}"
        }
    
        c.Expr[CaseClassFieldsExtractor[T]](q"""
          new CaseClassFieldsExtractor[$tpe] {
            def get = Map(..$extractParams)
          }
        """)
      }
    }
    
    trait CaseClassFieldsExtractor[T] {
      def get: Map[String, String]
    }
    
    def caseClassFields[T : CaseClassFieldsExtractor] =
      implicitly[CaseClassFieldsExtractor[T]].get
    

    在另一个编译单元中(REPL 中的下一行或使用前一行编译的代码作为依赖项),像这样使用它:

    scala> case class Something(x: Int, y: Double, z: String)
    defined class Something
    
    scala> caseClassFields[Something]
    res0: Map[String,String] = Map(x -> Int, y -> Double, z -> String)
    

    这似乎有点矫枉过正,但我​​无法让它更短。它的作用如下:

    1. caseClassFields 函数创建了一个中间 CaseClassFieldsExtractor,它隐式地出现、报告其发现并消失。
    2. CaseClassFieldsExtractor 是一个带有伴随对象的特征,它使用宏定义了该特征的匿名具体子类。它是可以检查案例类字段的宏,因为它包含有关案例类的丰富的编译器级信息。
    3. CaseClassFieldsExtractor 及其伴随对象必须在之前的编译单元中声明给检查案例类的编译单元,以便宏在您想要使用它时存在。
    4. 案例类的类型数据通过WeakTypeTag 传递。这将评估为具有大量模式匹配且我找不到任何文档的 Scala 结构。
    5. 我们再次假设只有一个(“主”?)构造函数,但我认为 Scala 中定义的所有类都只能有一个构造函数。由于该技术检查的是构造函数的字段,而不是类中的所有 JVM 字段,因此它不会因缺乏通用性而影响我之前的解决方案。
    6. 它使用准引号来构建CaseClassFieldsExtractor 的匿名具体子类。
    7. 所有“隐式”业务都允许在函数调用 (caseClassFields) 中定义和包装宏,而不会在尚未定义时过早调用。

    欢迎任何可以改进此解决方案或解释“隐含”如何准确地做他们所做的事情(或者是否可以删除)的 cmets。

    【讨论】:

    • 这是我最喜欢的答案。比其他的更干净(如果不是更简单的话);感谢您回到这里!
    • 在 2018 年使用这个很棒的答案,但是我确实注意到案例类中字段 x、y、z 的顺序不一定保留在地图对象中。它适用于 OP 的 x、y、z 示例,但对于较长的案例类和其他字段名称,它是清晰可见的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-24
    • 2011-02-09
    • 2016-09-07
    • 1970-01-01
    相关资源
    最近更新 更多