Classe di caso da mappare in Scala
-
22-07-2019 - |
Domanda
È possibile convertire un'istanza case class
in Scala, ad esempio
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
in una mappatura di qualche tipo, ad esempio
getCCParams(x) returns "param1" -> "hello", "param2" -> "world"
Che funziona per qualsiasi classe di casi, non solo per quelle predefinite. Ho scoperto che puoi estrarre il nome della classe del caso scrivendo un metodo che interroga la classe Prodotto sottostante, ad esempio
def getCCName(caseobj: Product) = caseobj.productPrefix
getCCName(x) returns "MyClass"
Quindi sto cercando una soluzione simile ma per i campi della classe case. Immagino che una soluzione potrebbe usare la riflessione Java, ma odio scrivere qualcosa che potrebbe rompersi in una versione futura di Scala se l'implementazione sottostante delle classi di casi cambia.
Attualmente sto lavorando su un server Scala e definendo il protocollo e tutti i suoi messaggi ed eccezioni usando le classi case, in quanto sono un costrutto così bello e conciso per questo. Ma poi ho bisogno di tradurli in una mappa Java per inviare il livello di messaggistica per qualsiasi implementazione client da utilizzare. La mia attuale implementazione definisce solo una traduzione per ogni classe di casi separatamente, ma sarebbe bello trovare una soluzione generalizzata.
Soluzione
Questo dovrebbe funzionare:
def getCCParams(cc: AnyRef) =
(Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Altri suggerimenti
Poiché le classi di casi estendono Prodotto si può semplicemente usare . productIterator
per ottenere i valori dei campi:
def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
.zip( cc.productIterator.to ).toMap // zipped with all values
O in alternativa:
def getCCParams(cc: Product) = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}
Un vantaggio del Prodotto è che non è necessario chiamare setAccessible
sul campo per leggere il suo valore. Un altro è che ProductIterator non usa la riflessione.
Nota che questo esempio funziona con classi di casi semplici che non estendono altre classi e non dichiarano campi all'esterno del costruttore.
Se qualcuno cerca una versione ricorsiva, ecco la modifica della soluzione di @ 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
}
Espande anche le classi nidificate nidificate in mappe a qualsiasi livello di annidamento.
Ecco una semplice variante se non ti interessa renderla una funzione generica:
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)
Potresti usare informe.
Sia
case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)
Definisci una rappresentazione generica etichettata
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]
}
Definisci due caratteri tipografici per fornire i metodi 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 }
}
}
Quindi puoi usarlo in questo modo.
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)
}
che stampa
anyMapX = Mappa (c - > 26, b - > bike, a - > true)
anyMapY = Mappa (b - > secondo, a - > primo)
stringMapX = Mappa (c - > 26, b - > bike, a - > true)
stringMapY = Map (b - > second, a - > first)
Per le classi di casi nidificati, (quindi mappe nidificate) controlla un'altra risposta
Soluzione con ProductCompletion
dal pacchetto interprete:
import tools.nsc.interpreter.ProductCompletion
def getCCParams(cc: Product) = {
val pc = new ProductCompletion(cc)
pc.caseNames.zip(pc.caseFields).toMap
}
Se ti capita di usare Json4s, puoi fare quanto segue:
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]]
Avvio di Scala 2.13
, case class
es (come implementazioni di Product
) sono forniti con un productElementNames che restituisce un iteratore sui nomi dei loro campi.
Comprimendo i nomi dei campi con i valori dei campi ottenuti con productIterator possiamo ottenere genericamente la Mappa
associata:
// 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")
Non so nulla di carino ... ma questo sembra funzionare, almeno per questo esempio molto basilare. Probabilmente ha bisogno di un po 'di lavoro ma potrebbe essere abbastanza per iniziare? Fondamentalmente filtra tutto "noto" metodi da una classe case (o qualsiasi altra classe: /)
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)
}
}
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)
Dettagli: https://github.com/hank-whu/common4s