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.

È stato utile?

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top