Existe-t-il un moyen intéressant de convertir une instance Scala case class
, par exemple.
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
en une cartographie quelconque, par exemple.
getCCParams(x) returns "param1" -> "hello", "param2" -> "world"
Ce qui fonctionne pour n'importe quelle classe de cas, pas seulement ceux prédéfinis. J'ai découvert que vous pouvez extraire le nom de la classe de cas en écrivant une méthode qui interroge la classe de produit sous-jacente, par exemple.
def getCCName(caseobj: Product) = caseobj.productPrefix
getCCName(x) returns "MyClass"
Je recherche donc une solution similaire mais pour les champs de classe de cas. J'imagine qu'une solution pourrait devoir utiliser la réflexion Java, mais je ne voudrais pas écrire quelque chose qui pourrait tomber en panne dans une future version de Scala si l'implémentation sous-jacente des classes de cas change.
Actuellement, je travaille sur un serveur Scala et je définis le protocole ainsi que tous ses messages et exceptions à l'aide de classes de cas, car elles constituent une construction aussi belle et concise que celle-ci. Mais je dois ensuite les traduire en une carte Java pour envoyer la couche de messagerie pour une implémentation cliente à utiliser. Mon implémentation actuelle ne fait que définir une traduction pour chaque classe de cas séparément, mais il serait bien de trouver une solution généralisée.
Cela devrait fonctionner:
def getCCParams(cc: AnyRef) =
(Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Parce que les classes de cas étendent Produit , on peut simplement utiliser .productIterator
pour obtenir les valeurs de champ:
def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
.Zip( cc.productIterator.to ).toMap // zipped with all values
Ou bien:
def getCCParams(cc: Product) = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}
Un des avantages de Product est qu'il n'est pas nécessaire d'appeler setAccessible
sur le terrain pour lire sa valeur. Une autre est que productIterator n'utilise pas de réflexion.
Notez que cet exemple fonctionne avec des classes de cas simples qui ne développent pas d'autres classes et ne déclarent pas de champs en dehors du constructeur.
Si quelqu'un cherche une version récursive, voici la modification de la solution de @ 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
}
Il étend également les classes de cas imbriquées en cartes à n'importe quel niveau d'imbrication.
Voici une variante simple si vous ne voulez pas en faire une fonction générique:
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)
Vous pourriez utiliser sans forme.
Laisser
case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)
Définir une représentation générique étiquetée
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]
}
Définir deux classes de types pour fournir les méthodes 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 }
}
}
Ensuite, vous pouvez l'utiliser comme ça.
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)
}
qui imprime
anyMapX = Carte (c -> 26, b -> vélo, a -> vrai)
anyMapY = Map (b -> seconde, a -> première)
stringMapX = Carte (c -> 26, b -> vélo, a -> vrai)
stringMapY = Map (b -> seconde, a -> première)
Pour les classes de cas imbriquées, (donc les cartes imbriquées) Check une autre réponse _
Solution avec ProductCompletion
du paquet interprète:
import tools.nsc.interpreter.ProductCompletion
def getCCParams(cc: Product) = {
val pc = new ProductCompletion(cc)
pc.caseNames.Zip(pc.caseFields).toMap
}
Si vous utilisez des Json4, vous pouvez effectuer les opérations suivantes:
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]]
Je ne sais pas pour Nice ... mais cela semble fonctionner, du moins pour cet exemple très très basique. Cela nécessite probablement un peu de travail mais pourrait être suffisant pour vous aider à démarrer? Fondamentalement, il filtre toutes les méthodes "connues" d'une classe de cas (ou de toute autre 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)
Détails: https://github.com/hank-whu/common4s
À partir de Scala 2.13
, case class
es (en tant qu'implémentations de Product
), une méthode productElementNames est renvoyée qui renvoie un itérateur sur le nom de leur champ.
En compressant les noms de champs avec les valeurs de champs obtenues avec productIterator , nous pouvons obtenir de manière générique la Map
associée:
// 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")