В Scala, как я могу программно определить имя поля класса дела? - PullRequest
8 голосов
/ 08 июня 2011

В Scala, предположим, у меня есть класс case, подобный этому:

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

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

Ответы [ 3 ]

7 голосов
/ 08 июня 2011

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


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

Сбой (из-за отсутствия ScalaSig) во вложенных классах case.

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

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

3 голосов
/ 26 августа 2013

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

1 голос
/ 10 февраля 2016

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

В одном модуле компиляции (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 являетсячерта с сопутствующим объектом, который определяет анонимный конкретный подкласс этой черты, используя макрос.Это макрос, который может проверять поля вашего класса case, потому что он имеет богатую информацию уровня класса компилятора о классе case.
  3. CaseClassFieldsExtractor и его объект-компаньон должны быть объявлены в предыдущем модуле компиляции для одногокоторый проверяет ваш класс case, чтобы макрос существовал в то время, когда вы хотите его использовать.
  4. Данные типа вашего класса case передаются через WeakTypeTag.Это приводит к структуре Scala с большим количеством сопоставлений с образцом и отсутствием документации, которую я мог бы найти.
  5. Мы снова предполагаем, что существует только один («основной»?) Конструктор, но я думаю, что все классы, определенные в Scala, могут иметьтолько один конструктор.Так как этот метод проверяет поля конструктора, а не все поля JVM в классе, поэтому он не подвержен общности, которая омрачила мое предыдущее решение.
  6. Он использует квазицитаты для создания анонимного конкретного подклассаCaseClassFieldsExtractor.
  7. Все это «неявное» дело позволяет макросу быть определенным и обернутым в вызов функции (caseClassFields) без слишком раннего вызова, когда он еще не определен.

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

...