【问题标题】:How can I transform a Map to a case class in Scala?如何将 Map 转换为 Scala 中的案例类?
【发布时间】:2019-12-11 20:43:50
【问题描述】:

如果我有一个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"))
}

但是有没有一种方法可以为任何案例类的映射一般编写一次?

【问题讨论】:

    标签: class scala map case


    【解决方案1】:

    首先,如果您只想缩短代码,可以采取一些安全的替代方案。伴生对象可以被视为一个函数,因此您可以使用如下内容:

    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 中使用 ApplicativeBuilders,它在内部执行几乎相同的操作,但语法更好:

    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 的类型参数与字段数成正比。我认为不是那么整洁。
    【解决方案2】:

    这是使用内置 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"))
    

    【讨论】:

    • 这是一种有趣的方法,但是在实例化类时如何避免argument type mismatch 异常?
    • 这是我见过的这个问题最简洁的答案。但是它使用现在已弃用的 API。这很容易通过使用“ClassTag”而不是“ClassManifest”和“runtimeClass”而不是“erasure”来更新
    【解决方案3】:

    不是您问题的完整答案,而是一个开始……

    它可以做到,但它可能会比你想象的更棘手。每个生成的 Scala 类都使用 Java 注释 ScalaSignature 进行注释,可以解析其 bytes 成员以提供所需的元数据(包括参数名称)。但是,此签名的格式不是 API,因此您需要自己解析它(并且可能会随着每个新的主要 Scala 版本更改解析它的方式)。

    也许最好的起点是lift-json 库,它能够基于 JSON 数据创建案例类的实例。

    更新: 我认为lift-json 实际上使用Paranamer 来执行此操作,因此可能无法解析ScalaSignature 的字节......这使得该技术也适用于非Scala 类.

    更新 2: 请参阅 Moritz's answer,谁比我更了解情况。

    【讨论】:

    • 那么为什么不直接创建 json 数据,让 lift-json 完成剩下的工作呢?这样他就不必用每个新版本的 scala 自己更新它,也不必解析 ScalaSignature 字节。当然,性能不会达到最佳状态,但这对 OP 来说可能不是问题。我错过了什么吗?
    • @Kim 嘿,这是一个很好的方法——如果 OP 愿意为此目的创建 JSON。或者,也许可以使用 Map[String, String] 作为输入更直接地重用lift-json...
    • 感谢您的提示。从 Map 到 JSON 字符串再到 lift-json 解析到案例类似乎有很多不必要的序列化/反序列化处理。
    【解决方案4】:

    您可以将 map 转换为 json,然后再转换为 case 类。虽然有点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 build 中使用这个依赖:

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

    【讨论】:

      【解决方案5】:

      这是无法做到的,因为您需要获取伴随对象的应用方法的参数名称,而它们根本无法通过反射获得。如果你有很多这样的案例类,你可以解析它们的声明并生成 fromMap 方法。

      【讨论】:

      • 它们无法通过标准 Java 反射获得,但您可以尝试解析 ScalaSignature 字节...
      猜你喜欢
      • 2016-07-27
      • 2014-12-18
      • 1970-01-01
      • 2015-06-16
      • 1970-01-01
      • 1970-01-01
      • 2015-10-23
      • 1970-01-01
      • 2014-01-08
      相关资源
      最近更新 更多