【问题标题】:Scala: convert map to case classScala:将地图转换为案例类
【发布时间】:2014-01-08 04:35:25
【问题描述】:

假设我有这个示例案例类

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

我有一张地图

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

我需要在代码的几个地方将此映射转换为我的案例类,如下所示:

myMap.asInstanceOf[Test]

最简单的方法是什么?我可以为此使用隐式吗?

【问题讨论】:

  • 我不明白为什么答案如此复杂。普通函数怎么样def map2Test(m: Map[String, Any]) = Test(m("k1').asInstanceOf[Int], m("k2") ... ) 如果你想要一个将映射转换为任何案例类的抽象,你可能必须使用反射。
  • @RobN:关键是要避免反射。你说答案很复杂,我说你的答案很脆弱。

标签: scala


【解决方案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】:

我不喜欢这段代码,但我想如果你可以将映射值放入一个元组中,然后为你的案例类使用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 个元素并且它们是正确的类型。如果没有,你最终会得到一个 None 。就像我说的,不是很好,但它表明这是可能的。

【讨论】:

  • 对于这个解决方案,我只会用myMap.values替换myMap.map(_._2)
【解决方案3】:

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)
      }
    """ }
  }
}

【讨论】:

    【解决方案4】:

    这是一种替代的非样板方法,它使用 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)
        }
      }
    }
    

    【讨论】:

    • 这应该是公认的解决方案。一个函数适用于任何类,它不需要宏,不需要外部依赖,并使用所有支持的特性。对我来说就像一个魅力。
    • 一个小建议。 fromMap 当前返回任何。将最后一行更改为 constructorMirror(constructorArgs:_*).asInstanceOf[T] 使其返回正确的类型。
    • 另一个需要注意的评论。如果您的案例类有一个 Option[String] 参数,则此代码期望 Map 包含一个字符串。如果它实际上包含 Option[String] 你最终会遇到问题。删除“if”语句会改变这一点。取决于你想要什么。
    • @DaveDeCaprio 实际上,使用宏的解决方案应该更快地执行,因为“反射”是在编译时进行的,而这个解决方案使用运行时反射(众所周知比编译时慢)
    • 你能做到让它处理嵌套结构吗?
    【解决方案5】:
    commons.mapper.Mappers.mapToBean[CaseClassBean](map)
    

    详情:https://github.com/hank-whu/common4s

    【讨论】:

    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
    【解决方案6】:

    这对我很有效,如果你使用jackson for 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

    【讨论】:

    • Circe 能做到这一点吗?
    猜你喜欢
    • 2016-07-27
    • 2014-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-16
    • 1970-01-01
    • 2020-03-07
    相关资源
    最近更新 更多