Класс Case для отображения в Scala - PullRequest
71 голосов
/ 04 августа 2009

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

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 отдельно, но было бы неплохо найти обобщенное решение.

Ответы [ 10 ]

84 голосов
/ 04 августа 2009

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

def getCCParams(cc: AnyRef) =
  (Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }
35 голосов
/ 05 сентября 2013

Поскольку классы расширений 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
}

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

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

12 голосов
/ 17 сентября 2014

Если кто-нибудь ищет рекурсивную версию, вот модификация решения @ 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-классы в карты на любом уровне вложенности.

5 голосов
/ 18 августа 2016

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

Пусть

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

Определить обобщенное представление Labelled

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 = Карта (c -> 26, b -> велосипед, a -> правда)

anyMapY = Карта (b -> вторая, a -> первая)

stringMapX = Карта (c -> 26, b -> велосипед, a -> правда)

stringMapY = Карта (b -> вторая, a -> первая)

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

5 голосов
/ 21 февраля 2014

Вот простой вариант, если вам не нужно делать его универсальной функцией:

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)
4 голосов
/ 06 сентября 2011

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

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}
3 голосов
/ 19 декабря 2018

Начиная с 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")
3 голосов
/ 08 мая 2017

Если вы используете 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]]
2 голосов
/ 12 августа 2015
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)

Подробности: https://github.com/hank-whu/common4s

2 голосов
/ 04 августа 2009

Я не знаю о хорошем ... но, похоже, это работает, по крайней мере, для этого очень простого примера. Это, вероятно, требует некоторой работы, но может быть достаточно, чтобы вы начали? По сути, он отфильтровывает все «известные» методы из класса 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)
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...