Scala:将Map转换为case class

33

假设我有这个示例类:

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

而我拥有一张地图

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

我需要在代码的多个位置将此地图(map)转换为我的案例类(case class),就像这样:

myMap.asInstanceOf[Test]

最简单的方法是什么?我能以某种方式使用implicit来完成吗?


2
我不明白为什么答案如此复杂。那么,使用一个普通的函数 def map2Test(m: Map[String, Any]) = Test(m("k1').asInstanceOf[Int], m("k2") ... ) 如何呢?如果你想要一个将映射转换为任何 case class 的抽象,你可能需要使用反射。 - Rob N
3
@RobN: 关键在于避免反思。你说答案很复杂,我说你的答案很脆弱。 - Sudheer Aedama
请参见 https://stackoverflow.com/questions/55049985/how-to-convert-generic-potentially-nested-mapstring-any-to-case-class-using-a/55050038。 - samthebest
7个回答

30

有两种优雅的方法可以实现这个。第一种是使用 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] 与任何对象之间的转换只需要另一个隐式转换(且该隐式转换处于作用域内)。


@flavian 是的,但我认为他希望异常不被转换,因为在调用它的时候不转换是一个意外的结果。至少,我希望有异常。 - wheaties
1
我的代码中 object MyObject { ... } 无法编译通过。 - user3685285
@wheaties 假设 MyClass 是一个带有伴生对象的 case 类,那么你的 unapply 方法是否需要使用不同的名称来表示该对象 因为默认情况下已经定义了 unapply 方法?例如,我只能编译出类似这样的代码:val MyClassOfMap(myInstance) = myMap - ecoe
@ecoe 如果需要的话,您可以使用“覆盖”功能来进行操作。或者,我应该说,您曾经可以这样做。不过,请注意,此问题和答案已有5年历史了! - wheaties
1
@wheaties,我是Scala的新手,我正在尝试理解为什么您在第一个解决方案中使用了unapply而不是apply。Scala文档说“apply方法就像一个构造函数,它接受参数并创建对象,而unapply接受一个对象并尝试返回参数。”。既然我们正在将参数(Map)转换为对象(MyClass),那么应该使用apply吗? - greenTea

18

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

1
这应该是被接受的解决方案。一个函数适用于任何类,不需要宏,不需要外部依赖,并使用所有支持的功能。对我来说非常好用。 - Dave DeCaprio
1
一个小建议。fromMap当前返回Any类型。将最后一行更改为constructorMirror(constructorArgs:_*).asInstanceOf[T],使其返回正确的类型。 - Dave DeCaprio
1
还有一点需要注意。如果你的 case class 里面有一个 Option[String] 参数,那么这段代码期望 Map 中包含一个 String 类型的值。如果实际上它包含了 Option[String],结果就会出现问题。移除 "if" 语句就可以解决这个问题,但具体取决于你想要什么结果。 - Dave DeCaprio
@DaveDeCaprio 实际上,使用宏的解决方案应该更快执行,因为“反射”是在编译时完成的,而这个解决方案使用运行时反射(比编译时慢得多)。 - acidghost
1
你能否处理嵌套结构? - samthebest

5
乔纳森·周实现了一个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 中使用 jackson,这对我来说很有效:

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

参考来源:将Map<String, String>转换为POJO


Circe 能够做到这个吗? - WeiChing 林煒清

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。就像我说的那样,这不是很好,但它表明这是可能的。


1
对于这个解决方案,我只会将myMap.map(_._2)替换为myMap.values - Mestre San

1
这是对Jonathon的Scala 3答案的更新(不再有TypeTag)。请注意,这对于嵌套在其他类中的case类不起作用。但对于顶级case类似乎可以正常工作。
import scala.reflect.ClassTag

object Reflect:

  def fromMap[T <: Product : ClassTag](m: Map[String, ?]): T = 

    val classTag = implicitly[ClassTag[T]]
    val constructor = classTag.runtimeClass.getDeclaredConstructors.head
    val constructorArgs = constructor.getParameters()
      .map { param =>
        val paramName = param.getName
        if (param.getType == classOf[Option[_]])
          m.get(paramName)
        else
          m.get(paramName)
            .getOrElse(throw new IllegalArgumentException(s"Missing required parameter: $paramName"))
      }
    constructor.newInstance(constructorArgs: _*).asInstanceOf[T]

以下是上述内容的测试:

case class Foo(a: String, b: Int, c: Option[String] = None)
case class Bar(a: String, b: Int, c: Option[Foo])

class ReflectSuite extends munit.FunSuite:

  test("fromMap") {
    
    val m = Map("a" -> "hello", "b" -> 42, "c" -> "world")
    val foo = Reflect.fromMap[Foo](m)
    assertEquals(foo, Foo("hello", 42, Some("world")))

    val n = Map("a" -> "hello", "b" -> 43)
    val foo2 = Reflect.fromMap[Foo](n)
    assertEquals(foo2, Foo("hello", 43))

    val o = Map("a" -> "yo", "b" -> 44, "c" -> foo)
    val bar = Reflect.fromMap[Bar](o)
    assertEquals(bar, Bar("yo", 44, Some(foo)))
  }

  test("fromMap should fail when required parameter is missing") {
    val m = Map("a" -> "hello", "c" -> "world")
    intercept[java.lang.IllegalArgumentException] {
      Reflect.fromMap[Foo](m)
    }
  }
  

0

1
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。仅有链接的答案如果链接页面发生更改可能会变得无效。 - Mamoun Benghezal

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接