Scala: преобразовать карту в класс case
Скажем, у меня есть этот примерный класс case
case class Test(key1: Int, key2: String, key3: String)
И у меня есть карта
myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")
Мне нужно преобразовать эту карту в мой класс case в нескольких местах кода, примерно так:
myMap.asInstanceOf[Test]
Каким будет самый простой способ сделать это? Могу ли я каким-то образом использовать для этого неявное?
Ответы
Ответ 1
Два способа сделать это элегантно. Во-первых, используйте unapply
, второй - использовать неявный класс (2.10+) с классом типа для преобразования для вас.
1) Unapply - это самый простой и самый прямой способ написать такое преобразование. Он не делает никаких "магии" и может быть легко обнаружен при использовании IDE. Помните, что подобные вещи могут загромождать ваш сопутствующий объект и заставлять ваш код прорастать зависимости в местах, которые вам могут не понадобиться:
object MyClass{
def unapply(values: Map[String,String]) = try{
Some(MyClass(values("key").toInteger, values("next").toFloat))
} catch{
case NonFatal(ex) => None
}
}
Что можно использовать следующим образом:
val MyClass(myInstance) = myMap
будьте осторожны, так как это вызовет исключение, если оно не полностью соответствует.
2) Выполнение неявного класса с классом типа создает для вас больше шаблонов, а также позволяет расширить пространство для того же шаблона, который применим к другим классам классов:
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
}
и в качестве примера вы бы сделали что-то вроде этого:
object MyObject{
implicit val new MapConvert[MyObject]{
def conv(values: Map[String, String]) = MyObject(values("key").toInt, values("foo").toFloat)
}
}
который затем можно использовать так же, как вы описали выше:
val myInstance = myMap.convert[MyObject]
выброс исключения, если преобразование не может быть выполнено. Используя этот шаблон, преобразование между Map[String, String]
в любой объект потребует просто еще одного неявного (и подразумеваемого для видимости в области).
Ответ 2
Вот альтернативный метод без шаблонов, который использует отражение Scala (Scala 2.10 и выше) и не требует отдельного скомпилированного модуля:
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)
}
}
}
Ответ 3
Джонатан Чоу реализует макрос Scala (разработанный для Scala 2.11), который обобщает это поведение и устраняет шаблон.
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)
}
""" }
}
}
Ответ 4
Мне не нравится этот код, но я предполагаю, что это возможно, если вы можете получить значения карты в кортеж, а затем использовать конструктор tupled
для вашего класса case. Это будет выглядеть примерно так:
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)
Вы должны быть осторожны, чтобы убедиться, что список значений составляет ровно 3 элемента и что они являются правильными типами. Если нет, вы в конечном итоге получаете вместо него None. Как я уже сказал, не очень, но это показывает, что это возможно.
Ответ 5
commons.mapper.Mappers.mapToBean[CaseClassBean](map)
Подробности: https://github.com/hank-whu/common4s
Ответ 6
Это хорошо работает для меня, если вы используете Джексона для Scala:
def from[T](map: Map[String, Any])(implicit m: Manifest[T]): T = {
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.convertValue(map)
}
Ссылка из: Преобразовать карту <String, String> в POJO