Disons que j'ai cette classe d'exemple
case class Test(key1: Int, key2: String, key3: String)
Et j'ai une carte
myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")
Je dois convertir cette carte dans ma classe de cas à plusieurs endroits du code, quelque chose comme ceci:
myMap.asInstanceOf[Test]
Quel serait le moyen le plus simple de le faire? Puis-je en quelque sorte utiliser implicite pour cela?
Deux façons de faire cela avec élégance. La première consiste à utiliser unapply
, la seconde à utiliser une classe implicite (2.10+) avec une classe type pour effectuer la conversion à votre place.
1) L'inapplication est le moyen le plus simple et le plus direct d'écrire une telle conversion. Il ne fait aucune "magie" et peut facilement être trouvé si vous utilisez un IDE. Notez que faire ce genre de chose peut encombrer votre objet compagnon et faire en sorte que votre code engendre des dépendances dans des endroits que vous ne souhaitez peut-être pas:
object MyClass{
def unapply(values: Map[String,String]) = try{
Some(MyClass(values("key").toInteger, values("next").toFloat))
} catch{
case NonFatal(ex) => None
}
}
Ce qui pourrait être utilisé comme ceci:
val MyClass(myInstance) = myMap
soyez prudent, car cela jetterait une exception s'il n'était pas assorti complètement.
2) Faire une classe implicite avec une classe type crée plus de passe-partout, mais laisse également beaucoup de place pour développer le même motif et l’appliquer à d’autres classes de cas:
implicit class Map2Class(values: Map[String,String]){
def convert[A](implicit mapper: MapConvert[A]) = mapper conv (values)
}
trait MapConvert[A]{
def conv(values: Map[String,String]): A
}
et à titre d'exemple, vous feriez quelque chose comme ceci:
object MyObject{
implicit val new MapConvert[MyObject]{
def conv(values: Map[String, String]) = MyObject(values("key").toInt, values("foo").toFloat)
}
}
qui pourrait alors être utilisé exactement comme vous l'avez décrit ci-dessus:
val myInstance = myMap.convert[MyObject]
lancer une exception si aucune conversion ne peut être faite. L'utilisation de ce modèle pour convertir un Map[String, String]
en n'importe quel objet nécessiterait simplement un autre implicite (implicite dans la portée.)
Voici une méthode alternative non passe-partout qui utilise la réflexion de Scala (Scala 2.10 et plus) et ne nécessite pas de module compilé séparément:
import org.specs2.mutable.Specification
import scala.reflect._
import scala.reflect.runtime.universe._
case class Test(t: String, ot: Option[String])
package object ccFromMap {
def fromMap[T: TypeTag: ClassTag](m: Map[String,_]) = {
val rm = runtimeMirror(classTag[T].runtimeClass.getClassLoader)
val classTest = typeOf[T].typeSymbol.asClass
val classMirror = rm.reflectClass(classTest)
val constructor = typeOf[T].decl(termNames.CONSTRUCTOR).asMethod
val constructorMirror = classMirror.reflectConstructor(constructor)
val constructorArgs = constructor.paramLists.flatten.map( (param: Symbol) => {
val paramName = param.name.toString
if(param.typeSignature <:< typeOf[Option[Any]])
m.get(paramName)
else
m.get(paramName).getOrElse(throw new IllegalArgumentException("Map is missing required parameter named " + paramName))
})
constructorMirror(constructorArgs:_*).asInstanceOf[T]
}
}
class CaseClassFromMapSpec extends Specification {
"case class" should {
"be constructable from a Map" in {
import ccFromMap._
fromMap[Test](Map("t" -> "test", "ot" -> "test2")) === Test("test", Some("test2"))
fromMap[Test](Map("t" -> "test")) === Test("test", None)
}
}
}
Jonathan Chow implémente une macro Scala (conçue pour Scala 2.11) qui généralise ce comportement et élimine le passe-partout.
http://blog.echo.sh/post/65955606729/exploring-scala-macros-map-to-case-class-conversion
import scala.reflect.macros.Context
trait Mappable[T] {
def toMap(t: T): Map[String, Any]
def fromMap(map: Map[String, Any]): T
}
object Mappable {
implicit def materializeMappable[T]: Mappable[T] = macro materializeMappableImpl[T]
def materializeMappableImpl[T: c.WeakTypeTag](c: Context): c.Expr[Mappable[T]] = {
import c.universe._
val tpe = weakTypeOf[T]
val companion = tpe.typeSymbol.companionSymbol
val fields = tpe.declarations.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor ⇒ m
}.get.paramss.head
val (toMapParams, fromMapParams) = fields.map { field ⇒
val name = field.name
val decoded = name.decoded
val returnType = tpe.declaration(name).typeSignature
(q"$decoded → t.$name", q"map($decoded).asInstanceOf[$returnType]")
}.unzip
c.Expr[Mappable[T]] { q"""
new Mappable[$tpe] {
def toMap(t: $tpe): Map[String, Any] = Map(..$toMapParams)
def fromMap(map: Map[String, Any]): $tpe = $companion(..$fromMapParams)
}
""" }
}
}
Je n'aime pas ce code, mais je suppose que cela est possible si vous pouvez obtenir les valeurs de carte dans un tuple, puis utiliser le constructeur tupled
pour votre classe de cas. Cela ressemblerait à ceci:
val myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")
val params = Some(myMap.map(_._2).toList).flatMap{
case List(a:Int,b:String,c:String) => Some((a,b,c))
case other => None
}
val myCaseClass = params.map(Test.tupled(_))
println(myCaseClass)
Vous devez faire attention à vous assurer que la liste de valeurs contient exactement 3 éléments et qu'ils correspondent aux types corrects. Sinon, vous vous retrouvez avec un non. Comme je l'ai dit, pas génial, mais cela montre que c'est possible.
commons.mapper.Mappers.mapToBean[CaseClassBean](map)
Détails: https://github.com/hank-whu/common4s