В Scala, как я могу программно определить имя поля класса?

в Scala предположим, что у меня есть такой класс case:

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

есть ли способ для меня, чтобы получить Seq[(String, Class[_])] или еще лучше Seq[(String, Manifest)], описывая параметры класса case?

3 ответов


это снова я (два года спустя). Вот другое, другое решение, использующее Scala рефлексия. Он вдохновлен блоге, который сам был вдохновлен Stack Overflow exchange. Решение ниже специализировано на вопросе оригинального плаката выше.

в одном блоке компиляции (REPL :paste или скомпилированный JAR), include 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 - признак с сопутствующим объектом, который определяет анонимный конкретный подкласс этого признака, используя макрос. Это макрос, который может проверять поля вашего класса case, потому что он имеет богатую информацию уровня компилятора о классе case.
  3. на CaseClassFieldsExtractor и его сопутствующий объект должен быть объявлен в предыдущем блоке компиляции тому, который проверяет ваш класс case, чтобы макрос существовал в то время, когда вы хотите использовать он.
  4. данные типа вашего класса case передаются через WeakTypeTag. Это оценивает структуру Scala с большим количеством сопоставления шаблонов и никакой документации, которую я мог бы найти.
  5. мы снова предполагаем, что есть только один ("первичный"?) конструктор, но я думаю, что все классы, определенные в Scala могут иметь только один конструктор. Поскольку этот метод рассматривает поля конструктора, а не все поля JVM в классе, поэтому он не подвержен отсутствию общности, которая испортил мое предыдущее решение.
  6. он использует quasiquotes для создания анонимного, конкретного подкласса CaseClassFieldsExtractor.
  7. все, что "неявный" бизнес позволяет макросу быть определенным и завернутым в вызов функции (caseClassFields) без вызова слишком рано, когда он еще не определен.

любые комментарии, которые могли бы уточнить это решение или объяснить, как именно "неявные преобразования" делать то, что они делают (или если они могут быть удалены) приветствуются.


я отвечаю на свой собственный вопрос, чтобы обеспечить базовое решение, но я также ищу альтернативы и улучшения.


один из вариантов, также совместимый с Java и не ограниченный классами case, - использовать ParaNamer. В Scala, другой вариант-парсить ScalaSig байт прилагается к созданным classfiles. Оба решения не будут работать в 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 С именем <init> чьи владелец имеет id 0.
  • имена параметров имеют в качестве владельца запись конструктора и всегда после этой записи.

он потерпит неудачу (из-за no ScalaSig) для вложенных классов case.

этот метод также возвращает только Class экземпляров, а не Manifests.

пожалуйста, не стесняйтесь предложить улучшения!


вот другое решение, которое использует простое отражение 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.