如何在Scala中将Map转换为case class?

17
如果我有一个Map[String,String]("url" -> "xxx", "title" -> "yyy"),是否有一种通用的方式将其转换为case class Image(url:String, title:String)? 我可以编写一个帮助类:
object Image{
  def fromMap(params:Map[String,String]) = Image(url=params("url"), title=params("title"))
}

但是否有一种通用的方法可以只编写一次,就能将其应用于任何case类的映射?

5个回答

8

首先,如果您只想缩短代码,有一些安全的替代方法可供选择。伴生对象可以被视为函数,因此您可以使用类似于以下内容的内容:

def build2[A,B,C](m: Map[A,B], f: (B,B) => C)(k1: A, k2: A): Option[C] = for {
  v1 <- m.get(k1)
  v2 <- m.get(k2)
} yield f(v1, v2)

build2(m, Image)("url", "title")

这将返回一个包含结果的选项。或者你可以使用Scalaz中的ApplicativeBuilder,它们在内部做几乎相同的事情,但语法更加优美:

import scalaz._, Scalaz._
(m.get("url") |@| m.get("title"))(Image)

如果您确实需要通过反射来实现这一点,那么最简单的方法是使用Paranamer(就像Lift-Framework一样)。Paranamer可以通过检查字节码来恢复参数名称,因此会有性能损失,并且由于类加载器问题(例如REPL),它不适用于所有环境。如果您限制自己只使用具有String构造函数参数的类,则可以按以下方式执行:

val pn = new CachingParanamer(new BytecodeReadingParanamer)

def fill[T](m: Map[String,String])(implicit mf: ClassManifest[T]) = for {
  ctor <- mf.erasure.getDeclaredConstructors.filter(m => m.getParameterTypes.forall(classOf[String]==)).headOption
  parameters = pn.lookupParameterNames(ctor)
} yield ctor.newInstance(parameters.map(m): _*).asInstanceOf[T]

val img = fill[Image](m)

(请注意,此示例可以选择默认构造函数,因为它不检查参数计数,这是您想要执行的操作)

build2的类型参数与字段数量成正比。我认为这不太整洁。 - Sudheer Aedama

7
这里提供一种使用Scala/Java内置反射的解决方案:
  def createCaseClass[T](vals : Map[String, Object])(implicit cmf : ClassManifest[T]) = {
      val ctor = cmf.erasure.getConstructors().head
      val args = cmf.erasure.getDeclaredFields().map( f => vals(f.getName) )
      ctor.newInstance(args : _*).asInstanceOf[T]
  }

如何使用:

val image = createCaseClass[Image](Map("url" -> "xxx", "title" -> "yyy"))

这是一个有趣的方法,但是在实例化类时如何避免“参数类型不匹配”异常? - bachr
这是我见过的对这个问题最简洁的答案。然而,它使用了现在已经被弃用的API。通过使用“ClassTag”代替“ClassManifest”和“runtimeClass”代替“erasure”,可以轻松更新此内容。 - Uncle Long Hair

2
这段文本是关于在Scala中获取类的元数据,以及如何解析它们。Scala类使用Java注释ScalaSignature进行注释,其bytes成员可以被解析以提供所需的元数据(包括参数名称)。然而,这个签名的格式不是API,因此您需要自己解析它(并且可能会随着每个新的主要Scala版本更改解析方式)。建议从lift-json库开始,该库具有根据JSON数据创建case类实例的功能。更新:我认为lift-json实际上使用Paranamer来完成这项工作,因此可能不会解析ScalaSignature的字节码... 这种技术也适用于非Scala类。
更新2:请查看莫里茨的回答,他比我更了解情况。

那为什么不只是创建JSON数据,让lift-json来完成剩下的工作呢?这样他就不必在每个新版本的Scala中自己更新它,也不必解析ScalaSignature字节。当然,性能可能不是最佳的,但这对于OP可能不是问题。我有什么遗漏吗? - Kim Stebel
@Kim 嗨,那是个好方法 —— 如果 OP 愿意仅为此创建 JSON 的话。或者,也许可以直接将 Map[String, String] 作为输入与 lift-json 更直接地重用... - Jean-Philippe Pellet
感谢您的建议。从Map到JSON字符串再到lift-json解析,最后转换为Case Class似乎需要进行很多不必要的序列化/反序列化处理。 - tommy chheng

0

你可以将 map 转换为 json,然后再转换为 case class。不过这有点 hacky。

import spray.json._

object MainClass2 extends App {
  val mapData: Map[Any, Any] =
    Map(
      "one" -> "1",
      "two" -> 2,
      "three" -> 12323232123887L,
      "four" -> 4.4,
      "five" -> false
    )

  implicit object AnyJsonFormat extends JsonFormat[Any] {
    def write(x: Any): JsValue = x match {
      case int: Int           => JsNumber(int)
      case long: Long          => JsNumber(long)
      case double: Double        => JsNumber(double)
      case string: String        => JsString(string)
      case boolean: Boolean if boolean  => JsTrue
      case boolean: Boolean if !boolean => JsFalse
    }
    def read(value: JsValue): Any = value match {
      case JsNumber(int) => int.intValue()
      case JsNumber(long) => long.longValue()
      case JsNumber(double) => double.doubleValue()
      case JsString(string) => string
      case JsTrue      => true
      case JsFalse     => false
    }
  }

  import ObjJsonProtocol._
  val json = mapData.toJson
  val result: TestObj = json.convertTo[TestObj]
  println(result)

}

final case class TestObj(one: String, two: Int, three: Long, four: Double, five: Boolean)

object ObjJsonProtocol extends DefaultJsonProtocol {
  implicit val objFormat: RootJsonFormat[TestObj] = jsonFormat5(TestObj)
}

并在sbt构建中使用此依赖项:

 "io.spray"          %%   "spray-json"     %   "1.3.3"

-2

这是不可能的,因为您需要获取伴生对象的apply方法的参数名称,而它们通过反射根本不可用。如果您有很多这些case类,您可以解析它们的声明并生成fromMap方法。


2
它们无法通过标准Java反射获得,但您可以尝试解析ScalaSignature字节,看看能否成功。 - Jean-Philippe Pellet

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