Klasa Case do mapowania w Scali

Czy Jest jakiś dobry sposób na konwersję instancji Scali case class, np.

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

Do jakiegoś odwzorowania, np.

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

Który działa dla każdej klasy przypadków, a nie tylko predefiniowanych. Odkryłem, że można wyciągnąć nazwę klasy case, pisząc metodę, która przesłuchuje podstawową klasę produktu, np.

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

Więc szukam podobnego rozwiązania, ale dla pól case class. Wyobrażam sobie, że rozwiązaniem może być Java reflection, ale nie chciałbym napisz coś, co może się zepsuć w przyszłym wydaniu Scali, jeśli podstawowa implementacja klas przypadków ulegnie zmianie.

Obecnie pracuję nad serwerem Scala i definiuję protokół oraz wszystkie jego komunikaty i wyjątki używając klas case, ponieważ są one tak piękną, zwięzłą konstrukcją do tego celu. Ale potem muszę przetłumaczyć je na mapę Javy, aby wysłać warstwę wiadomości dla dowolnej implementacji klienta do użycia. Moja obecna implementacja definiuje tylko tłumaczenie dla każdego przypadku klasy osobno, ale miło byłoby znaleźć ogólne rozwiązanie.

Author: Xavier Guihot, 2009-08-04

11 answers

To powinno zadziałać:

def getCCParams(cc: AnyRef) =
  cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }
 93
Author: Walter Chang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-10-16 17:45:23

Ponieważ klasy case rozszerzają Produkt można po prostu użyć .productIterator, aby uzyskać wartości pól:

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

Lub alternatywnie:

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

Jedną z zalet produktu jest to, że nie trzeba wywoływać setAccessible na polu, aby odczytać jego wartość. Innym jest to, że productIterator nie używa odbicia.

Zauważ, że ten przykład działa z prostymi klasami przypadków, które nie rozszerzają innych klas i nie deklarują pól poza konstruktorem.

 42
Author: Andrejs,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-04 22:00:12

Początek Scala 2.13, case classes (jako implementacje Product) są dostarczane z metodą productElementNames , która zwraca iterator nad nazwami ich pól.

Poprzez zsynchronizowanie nazw pól z wartościami pól uzyskanymi za pomocą productIterator możemy ogólnie uzyskać powiązane 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")
 20
Author: Xavier Guihot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-18 22:27:05

Jeśli ktoś szuka wersji rekurencyjnej, oto modyfikacja rozwiązania @ 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
}

Rozszerza również zagnieżdżone klasy przypadków na mapy na dowolnym poziomie zagnieżdżania.

 12
Author: Piotr Krzemiński,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-18 08:23:18

Oto prosta odmiana, jeśli nie zależy ci na uczynieniu z niej funkcji ogólnej:

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)
 6
Author: ShawnFumo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-20 22:19:48

Rozwiązanie z ProductCompletion z pakietu interpretera:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}
 4
Author: Stefan Endrullis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-09-06 13:09:47

Przydałby Ci się bezkształtny.

Let

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

Define a LabelledGeneric representation

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]
}

Zdefiniuj dwa typy, aby zapewnić metody 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 }
  }
}

Wtedy możesz go użyć w ten sposób.

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

Które drukuje

AnyMapX = Map (c -> 26, b -> bike, a -> true)

AnyMapY = Map (b - > second, a - > first)

StringMapX = Map (c -> 26, b -> bike, a -> true)

StringMapY = Map (b - > second, a - > first)

Dla zagnieżdżonych klasy przypadków, (tak zagnieżdżone mapy) sprawdź kolejna odpowiedź

 4
Author: harrylaou,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-20 09:12:55

Jeśli przypadkiem używasz Json4s, możesz wykonać następujące czynności:

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]]
 4
Author: Barak BN,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-08 12:51:00

Nie wiem jak miło... ale to wydaje się działać, przynajmniej w tym bardzo podstawowym przykładzie. Prawdopodobnie wymaga trochę pracy, ale może wystarczyć, aby zacząć? Zasadniczo odfiltrowuje wszystkie" znane " metody z klasy case (lub dowolnej innej klasy :/ )

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)
  }
}
 2
Author: André Laszlo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-04 12:53:36
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)

Szczegóły: https://github.com/hank-whu/common4s

 2
Author: Kai Han,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-12 10:17:10

Z wykorzystaniem Java reflection, ale bez zmiany poziomu dostępu. Konwertuje Product i klasę case na Map[String, String]:

def productToMap[T <: Product](obj: T, prefix: String): Map[String, String] = {
  val clazz = obj.getClass
  val fields = clazz.getDeclaredFields.map(_.getName).toSet
  val methods = clazz.getDeclaredMethods.filter(method => fields.contains(method.getName))
  methods.foldLeft(Map[String, String]()) { case (acc, method) =>
    val value = method.invoke(obj).toString
    val key = if (prefix.isEmpty) method.getName else s"${prefix}_${method.getName}"
    acc + (key -> value)
  }
}
 0
Author: Artavazd Balayan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-09-03 09:36:38