Classe de cas à mapper en Scala
-
22-07-2019 - |
Question
Existe-t-il un moyen intéressant de convertir une instance Scala classe de cas
, par exemple
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
dans un mappage d'une certaine sorte, par exemple
getCCParams(x) returns "param1" -> "hello", "param2" -> "world"
Ce qui fonctionne pour n'importe quelle classe de cas, pas seulement celles prédéfinies. J'ai découvert que vous pouvez extraire le nom de la classe de cas en écrivant une méthode qui interroge la classe Product 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 devrait utiliser la réflexion Java, mais je détesterais écrire quelque chose qui pourrait tomber en panne dans une future version de Scala si la mise en œuvre sous-jacente des classes de cas change.
Actuellement, je suis en train de travailler sur un serveur Scala et de définir 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 toute implémentation client à 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.
La solution
Cela devrait fonctionner:
def getCCParams(cc: AnyRef) =
(Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Autres conseils
Etant donné que les classes de cas étendent les Produit , il est tout simplement possible d'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 alternativement:
def getCCParams(cc: Product) = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}
L'un des avantages de Product est qu'il n'est pas nécessaire d'appeler setAccessible
sur le champ 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 prolongent pas d'autres classes et ne déclarent pas de champs en dehors du constructeur.
Si quelqu'un recherche 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 pouvez 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éfinissez 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 ceci.
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 - & second, a - > premier)
stringMapX = Carte (c - & 26, b - > vélo, a - & true)
stringMapY = Map (b - & second, a - > premier)
Pour les classes de cas imbriquées, (donc les cartes imbriquées) vérifiez une autre réponse
Solution avec ProductCompletion
à partir du package interpréteur:
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, procédez comme suit:
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]]
Démarrage de Scala 2.13
, classe de cas
es (en tant qu'implémentations de Produit
) est fourni avec un productElementNames qui renvoie un itérateur sur le nom de leur champ.
En compressant les noms de champ avec les valeurs de champ obtenues avec productIterator , nous pouvons obtenir de manière générique le mappage
correspondant:
// 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")
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 tous les " connus " les méthodes 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