Fallklasse in Scala abzubilden

75

Gibt es eine gute Möglichkeit, eine Scala- case classInstanz zu konvertieren , z

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

in eine Art Mapping, z

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

Was für jede Fallklasse funktioniert, nicht nur für vordefinierte. Ich habe festgestellt, dass Sie den Namen der Fallklasse herausziehen können, indem Sie eine Methode schreiben, die die zugrunde liegende Produktklasse abfragt, z

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

Ich suche also nach einer ähnlichen Lösung, aber nach den Feldern für Fallklassen. Ich würde mir vorstellen, dass eine Lösung möglicherweise Java-Reflektion verwenden muss, aber ich würde es hassen, etwas zu schreiben, das in einer zukünftigen Version von Scala möglicherweise kaputt geht, wenn sich die zugrunde liegende Implementierung von Fallklassen ändert.

Derzeit arbeite ich an einem Scala-Server und definiere das Protokoll und alle seine Nachrichten und Ausnahmen mithilfe von Fallklassen, da diese ein so schönes, prägnantes Konstrukt dafür sind. Aber ich muss sie dann in eine Java-Map übersetzen, um sie über die Messaging-Schicht zu senden, damit jede Client-Implementierung sie verwenden kann. Meine aktuelle Implementierung definiert nur eine Übersetzung für jede Fallklasse separat, aber es wäre schön, eine verallgemeinerte Lösung zu finden.

Wille
quelle
Ich habe diesen Blog-Beitrag gefunden , der zeigt, wie man Makros verwendet, um dies zu tun.
Giovanni Botta

Antworten:

93

Das sollte funktionieren:

def getCCParams(cc: AnyRef) =
  cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }
Walter Chang
quelle
15
Wenn dies nicht schwierig ist, können Sie erklären, was Sie geschrieben haben?
Den Bardadym
Jetzt gibt es Scala Reflexion! Ich bin mir nicht sicher, ob es noch experimentell ist oder ob es jetzt stabil ist. Auf jeden Fall bietet die Scala Reflection API möglicherweise eine eigene Lösung oder zumindest eine bessere Möglichkeit, eine Lösung wie die oben beschriebene zu implementieren. Übrigens: Wenn Sie setAccessible to true verwenden, können Sie auch auf private Felder zugreifen. Willst du das wirklich? Und es funktioniert möglicherweise nicht, wenn ein SecurityManager aktiv ist.
user573215
2
@ RobinGreen Fallklassen können nicht voneinander erben
Giovanni Botta
1
@GiovanniBotta Das Problem scheint zu sein, dass die Accessor-Methode nicht als zugänglich markiert ist. Seltsam, da es eine öffentliche Methode ist. In jedem Fall sollte es in Ordnung sein, die zu entfernen isAccessible, da getMethodnur öffentliche Methoden zurückgegeben werden. Auch accessor != nullist der falsche Test, da a getMethodauslöst, NoSuchMethodExceptionwenn die Methode nicht gefunden wird.
James_pic
2
wahrscheinlich ist auf diese Weise leichter zu verstehen: case class Person(name: String, surname: String) val person = new Person("Daniele", "DalleMule") val personAsMap = person.getClass.getDeclaredFields.foldLeft(Map[String, Any]())((map, field) => { field.setAccessible(true) map + (field.getName -> field.get(person)) } )
DanieleDM
42

Da Fallklassen das Produkt erweitern, können Sie einfach .productIteratorFeldwerte abrufen:

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

Oder alternativ:

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

Ein Vorteil von Product ist, dass Sie nicht anrufen müssen setAccessible das Feld , um seinen Wert zu lesen. Ein weiterer Grund ist, dass productIterator keine Reflexion verwendet.

Beachten Sie, dass dieses Beispiel mit einfachen Fallklassen funktioniert, die andere Klassen nicht erweitern und keine Felder außerhalb des Konstruktors deklarieren.

Andrejs
quelle
6
In der getDeclaredFieldsSpezifikation heißt es: "Die zurückgegebenen Elemente im Array sind nicht sortiert und befinden sich nicht in einer bestimmten Reihenfolge." Wie kommt es, dass die Felder in der richtigen Reihenfolge zurückgegeben werden?
Giovanni Botta
Es stimmt, es ist am besten, Ihr jvm / os zu überprüfen, aber in der Praxis stackoverflow.com/a/5004929/1180621
Andrejs
1
Ja, das würde ich allerdings nicht für selbstverständlich halten. Ich möchte nicht anfangen, nicht portablen Code zu schreiben.
Giovanni Botta
Dies löst eine Ausnahme aus, wenn die Fallklasse in einem anderen Objekt verschachtelt ist, da der productIterator das deklarierte Feld "$ Outer" nicht enthält.
ssice
19

Ab dem Start Scala 2.13werden case classes (als Implementierungen von Product) mit einem productElementNames versehen Methode , die einen Iterator über die Namen ihrer Felder zurückgibt.

Durch Zippen von Feldnamen mit Feldwerten, die mit productIterator erhalten wurden , können wir generisch Folgendes erhalten 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")
Xavier Guihot
quelle
12

Wenn jemand nach einer rekursiven Version sucht, ist hier die Modifikation der Lösung von @ 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
}

Außerdem werden die verschachtelten Fallklassen auf jeder Verschachtelungsebene zu Karten erweitert.

Piotr Krzemiński
quelle
6

Hier ist eine einfache Variante, wenn Sie nicht daran interessiert sind, sie zu einer generischen Funktion zu machen:

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)
ShawnFumo
quelle
4

Lösung mit ProductCompletionaus Dolmetscherpaket:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}
Stefan Endrullis
quelle
5
Wurde tools.nsc.interpreter.ProductCompletion in Scala 2.10 an einen anderen Ort verschoben?
pdxleif
4

Sie könnten formlos verwenden.

Lassen

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

Definieren Sie eine LabelledGeneric-Darstellung

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

Definieren Sie zwei Typklassen, um die toMap-Methoden bereitzustellen

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

Dann können Sie es so verwenden.

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

welche druckt

anyMapX = Karte (c -> 26, b -> Fahrrad, a -> wahr)

anyMapY = Map (b -> Sekunde, a -> zuerst)

stringMapX = Map (c -> 26, b -> Fahrrad, a -> true)

stringMapY = Map (b -> zweite, a -> erste)

Überprüfen Sie für verschachtelte Fallklassen (also verschachtelte Karten) eine andere Antwort

Harrylaou
quelle
4

Wenn Sie Json4s verwenden, können Sie Folgendes tun:

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]]
Barak BN
quelle
2

Ich weiß nicht, wie schön es ist ... aber das scheint zu funktionieren, zumindest für dieses sehr, sehr einfache Beispiel. Es braucht wahrscheinlich etwas Arbeit, könnte aber ausreichen, um Ihnen den Einstieg zu erleichtern? Grundsätzlich werden alle "bekannten" Methoden aus einer Fallklasse (oder einer anderen Klasse: /) herausgefiltert.

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)
  }
}
André Laszlo
quelle
2
Hoppla. Ich habe Class.getDeclaredFields verpasst.
André Laszlo
0

Mit der Verwendung von Java Reflection, aber ohne Änderung der Zugriffsebene. Konvertiert Product und Fallklasse in 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)
  }
}
Artavazd Balayan
quelle