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.

Était-ce utile?

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

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top