Scala:将地图转换为案例类

时间:2013-12-19 14:43:09

标签: scala

假设我有这个示例案例类

case class Test(key1: Int, key2: String, key3: String)

我有一张地图

myMap = Map("k1" -> 1, "k2" -> "val2", "k3" -> "val3")

我需要在代码的几个地方将此地图转换为我的case类,如下所示:

myMap.asInstanceOf[Test]

最简单的方法是什么?我可以以某种方式使用隐含的吗?

6 个答案:

答案 0 :(得分:24)

两种优雅地做到这一点的方法。第一种是使用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]到任何对象之间进行转换只需要另一个隐式(并且隐含在范围内)。

答案 1 :(得分:14)

以下是使用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)
    }
  }
}

答案 2 :(得分:5)

Jonathan Chow实现了一个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)
      }
    """ }
  }
}

答案 3 :(得分:2)

我不喜欢这段代码,但我认为如果您可以将地图值放入元组然后使用tupled构造函数作为案例类,则可以实现这一点。这看起来像这样:

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个元素,并且它们是正确的类型。如果没有,你最终会得到一个无。就像我说的那样,并不好,但它表明它是可能的。

答案 4 :(得分:0)

commons.mapper.Mappers.mapToBean[CaseClassBean](map)

详细信息:https://github.com/hank-whu/common4s

答案 5 :(得分:0)

这对我来说很好,如果您使用jackson做scala:

def from[T](map: Map[String, Any])(implicit m: Manifest[T]): T = {
  val mapper = new ObjectMapper() with ScalaObjectMapper
  mapper.convertValue(map)
}

引用来自:Convert a Map<String, String> to a POJO