Класс case для отображения в Scala

Есть ли хороший способ конвертировать экземпляр case class случая Scala, например

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

в какое-то отображение, например

getCCParams(x) returns "param1" -> "hello", "param2" -> "world"

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

def getCCName(caseobj: Product) = caseobj.productPrefix 
getCCName(x) returns "MyClass"

Поэтому я ищу подобное решение, но для полей класса случая. Я мог бы предположить, что решение могло бы использовать отражение Java, но я бы не хотел писать что-то, что может сломаться в будущем выпуске Scala, если изменится базовая реализация классов case.

В настоящее время я работаю на сервере Scala и определяю протокол и все его сообщения и исключения, используя классы падежей, поскольку они являются такой красивой и лаконичной конструкцией для этого. Но затем мне нужно перевести их в карту Java для отправки через уровень обмена сообщениями для использования любой клиентской реализацией. Моя текущая реализация просто определяет перевод для каждого класса case отдельно, но было бы неплохо найти обобщенное решение.

Ответ 1

Это должно работать:

def getCCParams(cc: AnyRef) =
  cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }

Ответ 2

Поскольку классы case расширяют Product, можно просто использовать .productIterator для получения значений полей:

def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
                .zip( cc.productIterator.to ).toMap // zipped with all values

Или, альтернативно:

def getCCParams(cc: Product) = {          
      val values = cc.productIterator
      cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}

Одним из преимуществ продукта является то, что вам не нужно вызывать setAccessible в поле, чтобы прочитать его значение. Другим является то, что productIterator не использует отражение.

Обратите внимание, что этот пример работает с простыми классами классов, которые не распространяют другие классы и не объявляют поля вне конструктора.

Ответ 3

Если кто-то ищет рекурсивную версию, это модификация решения @Andrejs:

def getCCParams(cc: Product): Map[String, Any] = {
  val values = cc.productIterator
  cc.getClass.getDeclaredFields.map {
    _.getName -> (values.next() match {
      case p: Product if p.productArity > 0 => getCCParams(p)
      case x => x
    })
  }.toMap
}

Он также расширяет вложенные классы case в карты на любом уровне вложенности.

Ответ 4

Вот простая вариация, если вы не заботитесь о том, чтобы сделать ее общей функцией:

case class Person(name:String, age:Int)

def personToMap(person: Person): Map[String, Any] = {
  val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
  val vals = Person.unapply(person).get.productIterator.toSeq
  fieldNames.zip(vals).toMap
}

scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)

Ответ 5

Вы можете использовать бесформенный.

Пусть

case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)

Определите представление LabelledGeneric

import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
  implicit val lgenX = LabelledGeneric[X]
}
object Y {
  implicit val lgenY = LabelledGeneric[Y]
}

Определите два типа, чтобы предоставить методы toMap

object ToMapImplicits {

  implicit class ToMapOps[A <: Product](val a: A)
    extends AnyVal {
    def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v }
  }

  implicit class ToMapOps2[A <: Product](val a: A)
    extends AnyVal {
    def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v.toString }
  }
}

Затем вы можете использовать его так.

object Run  extends App {
  import ToMapImplicits._
  val x: X = X(true, "bike",26)
  val y: Y = Y("first", "second")
  val anyMapX: Map[String, Any] = x.mkMapAny
  val anyMapY: Map[String, Any] = y.mkMapAny
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)

  val stringMapX: Map[String, String] = x.mkMapString
  val stringMapY: Map[String, String] = y.mkMapString
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)
}

который печатает

anyMapX = Map (c → 26, b → bike, a → true)

anyMapY = Map (b → second, a → first)

stringMapX = Карта (c → 26, b → bike, a → true)

stringMapY = Map (b → second, a → first)

Для вложенных классов case (таким образом, вложенные карты) проверьте другой ответ

Ответ 6

Начиная с Scala 2.13, case class es (как реализации Product) обеспечиваются методом productElementNames, который возвращает итератор по именам их полей.

Путем архивирования имен полей со значениями полей, полученными с помощью productIterator, мы можем в общем случае получить связанный Map:

// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")

Ответ 7

Решение с ProductCompletion из пакета интерпретатора:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}

Ответ 8

Если вы используете Json4s, вы можете сделать следующее:

import org.json4s.{Extraction, _}

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]

Ответ 9

Я не знаю о хорошем... но это, похоже, работает, по крайней мере, для этого очень простого примера. Это, вероятно, нуждается в некоторой работе, но может быть достаточно, чтобы вы начали? В основном он отфильтровывает все "известные" методы из класса case (или любого другого класса:/)

object CaseMappingTest {
  case class MyCase(a: String, b: Int)

  def caseClassToMap(obj: AnyRef) = {
    val c = obj.getClass
    val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
                          "toString")
    val casemethods = c.getMethods.toList.filter{
      n =>
        (n.getParameterTypes.size == 0) &&
        (n.getDeclaringClass == c) &&
        (! predefined.exists(_ == n.getName))

    }
    val values = casemethods.map(_.invoke(obj, null))
    casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
  }

  def main(args: Array[String]) {
    println(caseClassToMap(MyCase("foo", 1)))
    // prints: Map(a -> foo, b -> 1)
  }
}