在Scala中,如何以编程方式确定case类的字段名称?

gcuhipw9  于 2023-08-05  发布在  Scala
关注(0)|答案(4)|浏览(117)

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

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

字符串
有没有一种方法可以让我获得一个Seq[(String, Class[_])],或者更好的Seq[(String, Manifest)],来描述case类的参数?

sqxo8psd

sqxo8psd1#

我回答自己的问题是为了提供一个基本的解决方案,但我也在寻找替代方案和改进方案。
一个选择是使用ParaNamer,它也与Java兼容,并且不限于case类。在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的结构,这应该被认为是一种启发式方法。**特别是,这段代码做了以下假设:

  • Case类只有一个构造函数。
  • 在位置0处的签名的条目总是ClassSymbol
  • 类的相关构造函数是第一个名为<init>MethodEntry,其所有者的id为0。
  • 参数名的所有者是构造函数项,并且总是在该项之后。

它将在嵌套的case类上失败(因为没有ScalaSig)。
此方法也只返回Class示例,而不返回Manifest s。
请随时提出改进建议!

5anewei6

5anewei62#

下面是一个使用普通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

4dbbbstv

4dbbbstv3#

又是我(两年后)。这里有一个不同的,使用 Scala 反射的不同解决方案。它的灵感来自blog post,而blog post本身的灵感来自Stack Overflow exchange。下面的解答是针对楼主上面的问题的。
在一个编译单元(一个REPL :paste或一个编译过的JAR)中,包含scala-reflect作为依赖项并编译以下代码(在Scala 2.11中测试,might work in 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是一个trait,它有一个伴随对象,使用宏定义了这个trait的匿名具体子类。它是可以检查case类字段的宏,因为它具有关于case类的丰富的编译器级信息。
  3. CaseClassFieldsExtractor及其伴随对象必须在检查case类的前一个编译单元中声明,以便宏在您想要使用它的时候存在。
  4. case类的类型数据通过WeakTypeTag传递。这是一个Scala结构,有很多模式匹配,但我找不到任何文档。
    1.我们再次假设只有一个(“主要”?)构造函数,但我认为Scala中定义的所有类都只能有一个构造函数。由于这种技术检查构造函数的字段,而不是类中的所有JVM字段,因此它不容易受到缺乏通用性的影响,而这正是我以前的解决方案的缺点。
    1.它使用quasiquotes构建CaseClassFieldsExtractor的匿名、具体子类。
    1.所有这些“隐式”业务都允许在函数调用(caseClassFields)中定义和 Package 宏,而不会在尚未定义时过早调用。
    欢迎任何可以改进这个解决方案或解释“隐含”是如何做的(或是否可以删除)的评论。
62o28rlo

62o28rlo4#

除了Jim Pivarski的答案外,要获得正确的字段顺序,需要在decls之后添加sorted

val fields = tpe.decls.sorted.collectFirst {
  case m: MethodSymbol if (m.isPrimaryConstructor) => m
}.get.paramLists.head

字符串

相关问题