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

发布于 2024-11-14 12:25:26 字数 215 浏览 3 评论 0原文

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

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

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

In Scala, suppose I have a case class like this:

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

Is there a way for me to obtain a Seq[(String, Class[_])], or better yet, Seq[(String, Manifest)], describing the case class's parameters?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

是你 2024-11-21 12:25:26

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


一种选择是使用 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 的结构,应该考虑这一点作为一种启发式方法。 特别是,此代码做出以下假设:

  • Case 类只有一个构造函数。
  • 位置零处的签名条目始终是一个ClassSymbol。
  • 该类的相关构造函数是第一个名为 MethodEntry,其所有者的 ID 为 0。
  • 参数名称将构造函数条目作为所有者,并且始终位于该条目之后。

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

此方法也仅返回 Class 实例,而不返回 Manifest

请随时提出改进建议!

I'm answering my own question to provide a base solution, but I'm looking for alternatives and improvements, too.


One option, also compatible with Java and not restricted to case classes, is to use ParaNamer. In Scala, another option is to parse the ScalaSig bytes attached to generated classfiles. Both solutions won't work in the REPL.

Here's my attempt at extracting the names of the fields from ScalaSig (which uses scalap and 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
}

Disclaimer: I don't really understand the structure of ScalaSig and this should be considered as a heuristics. In particular, this code makes the following assumptions:

  • Case classes have only one constructor.
  • The entry of the signature at position zero is always a ClassSymbol.
  • The relevant constructor of the class is the first MethodEntry with name <init> whose owner has id 0.
  • The parameter names have as owner the constructor entry and always after that entry.

It will fail (because of no ScalaSig) on nested case classes.

This method also only returns Class instances and not Manifests.

Please feel free to suggest improvements!

戏蝶舞 2024-11-21 12:25:26

这是使用纯 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

Here's a different solution that uses plain-Java reflection.

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

To get a Seq[(String, Class[_])], you can do this:

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

I'm not sure about how to get Manifests.

小糖芽 2024-11-21 12:25:26

又是我(两年后)。这是使用 Scala 反射的不同解决方案。它的灵感来自 博客文章,其本身的灵感来自于Stack Overflow 交换。下面的解决方案专门针对上述原始发帖者的问题。

在一个编译单元(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) 中,而不会在尚未定义时过早调用。

欢迎任何可以完善此解决方案或解释“隐式”如何准确执行其操作(或是否可以将其删除)的评论。

It's me again (two years later). Here's a different, different solution using Scala reflection. It is inspired by a blog post, which was itself inspired by a Stack Overflow exchange. The solution below is specialized to the original poster's question above.

In one compilation unit (a REPL :paste or a compiled JAR), include scala-reflect as a dependency and compile the following (tested in 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

And in another compilation unit (the next line in the REPL or code compiled with the previous as a dependency), use it like this:

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)

It seems like overkill, but I haven't been able to get it any shorter. Here's what it does:

  1. The caseClassFields function creates an intermediate CaseClassFieldsExtractor that implicitly comes into existence, reports its findings, and disappears.
  2. The CaseClassFieldsExtractor is a trait with a companion object that defines an anonymous concrete subclass of this trait, using a macro. It is the macro that can inspect your case class's fields because it has rich, compiler-level information about the case class.
  3. The CaseClassFieldsExtractor and its companion object must be declared in a previous compilation unit to the one that examines your case class so that the macro exists at the time you want to use it.
  4. Your case class's type data is passed in through the WeakTypeTag. This evaluates to a Scala structure with lots of pattern matching and no documentation that I could find.
  5. We again assume that there's only one ("primary"?) constructor, but I think all classes defined in Scala can have only one constructor. Since this technique examines the fields of the constructor, not all JVM fields in the class, so it's not susceptible to the lack of generality that marred my previous solution.
  6. It uses quasiquotes to build up an anonymous, concrete subclass of the CaseClassFieldsExtractor.
  7. All that "implicit" business allows the macro to be defined and wrapped up in a function call (caseClassFields) without being called too early, when it's not yet defined.

Any comments that could refine this solution or explain how exactly the "implicits" do what they do (or if they can be removed) are welcome.

鹤仙姿 2024-11-21 12:25:26

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

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

addition to Jim Pivarski answer, to have right order of fields, need to add sorted after decls

val fields = tpe.decls.sorted.collectFirst {
  case m: MethodSymbol if (m.isPrimaryConstructor) => m
}.get.paramLists.head
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文