Scala中将case class转换为map

78

有没有一种简便的方法可以将Scala的case class实例转换成其他类型,例如

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

转换成某种映射,例如

getCCParams(x) returns "param1" -> "hello", "param2" -> "world"

这适用于任何情况类,不仅限于预定义的类。我发现可以通过编写一个检查底层Product类的方法来提取情况类名称,例如:

def getCCName(caseobj: Product) = caseobj.productPrefix 
getCCName(x) returns "MyClass"

我在寻找一个类似的解决方案,但是针对case class字段。我想这个解决方案可能需要使用Java反射,但如果case classes的底层实现发生更改,我不希望编写的代码在Scala未来版本中出现问题。

目前,我正在开发一个Scala服务器,并使用case classes定义协议及其所有消息和异常,因为它们非常适合这种美丽且简洁的结构。但是,我需要将它们转换成Java映射以便于发送给任何客户端实现的消息传输层。我的当前实现只是分别为每个case class定义了一个翻译,但是找到一个通用的解决方案会很好。


我发现了这篇博客文章,它展示了如何使用宏来完成此操作。 - Giovanni Botta
12个回答

96

这段代码应该可以工作:

def getCCParams(cc: AnyRef) =
  cc.getClass.getDeclaredFields.foldLeft(Map.empty[String, Any]) { (a, f) =>
    f.setAccessible(true)
    a + (f.getName -> f.get(cc))
  }

16
如果不麻烦的话,您能解释一下您写的内容吗? - den bardadym
现在有Scala反射!我不确定它现在是实验性的还是稳定的。无论如何,也许Scala反射API提供了一种自己的解决方案,或者至少提供了一种更Scala的方式来实现上述解决方案。顺便说一下:当你将setAccessible设置为true时,你也可以访问私有字段。这真的是你想要的吗?而且当SecurityManager处于活动状态时,它可能无法正常工作。 - user573215
2
@RobinGreen 案例类不能互相继承。 - Giovanni Botta
1
@GiovanniBotta 问题似乎在于访问器方法没有标记为可访问。奇怪的是,它是一个公共方法。无论如何,应该可以将isAccessible去掉,因为getMethod只返回公共方法。此外,accessor != null是错误的测试,因为如果找不到该方法,getMethod会抛出NoSuchMethodException异常。 - James_pic
2
可能这样更容易理解:`case class Person(name: String, surname: String)val person = new Person("Daniele", "DalleMule") val personAsMap = person.getClass.getDeclaredFields.foldLeft(MapString, Any)((map, field) => { field.setAccessible(true) map + (field.getName -> field.get(person)) } )` - DanieleDM
显示剩余4条评论

48

由于case类继承了Product,因此可以简单地使用.productIterator来获取字段值:

def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
                .zip( cc.productIterator.to ).toMap // zipped with all values

或者另外一种选择:

def getCCParams(cc: Product) = {          
      val values = cc.productIterator
      cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}

Product的一个优点是,您不需要在字段上调用setAccessible来读取其值。另一个是productIterator不使用反射。

请注意,此示例适用于简单的case类,这些类不会扩展其他类,并且不会在构造函数之外声明字段。


7
“getDeclaredFields”规范说明:“返回的数组元素没有排序,也没有任何特定的顺序。”为什么返回的字段按正确的顺序排列? - Giovanni Botta
是的,最好检查您的JVM / OS,但在实践中https://dev59.com/5W445IYBdhLWcg3wLXIK#5004929。 - Andrejs
3
我不会视之为理所当然。我不想开始编写非可移植代码。 - Giovanni Botta
如果 case class 嵌套在另一个对象中,这将抛出异常,因为 productIterator 不包含声明的 "$outer" 字段。 - ssice

40

Scala 2.13 开始,case class(作为 Product 实现)具有 productElementNames 方法,该方法返回一个迭代器,包含它们字段名称的信息。

通过将字段名称与使用 productIterator 获取的字段值进行压缩,我们可以通用地获得相关的 Map

// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")

2
这正是我在寻找的。对我的问题来说,这是一个漂亮而干净的解决方案。 - sentenza

12

如果有人在寻找递归版本,这是对 @Andrejs 的解决方案进行修改后的结果:

def getCCParams(cc: Product): Map[String, Any] = {
  val values = cc.productIterator
  cc.getClass.getDeclaredFields.map {
    _.getName -> (values.next() match {
      case p: Product if p.productArity > 0 => getCCParams(p)
      case x => x
    })
  }.toMap
}

它还可以在任何嵌套级别将嵌套的case类扩展为映射。


7

如果您不需要将其变成通用函数,这是一个简单的变体:

case class Person(name:String, age:Int)

def personToMap(person: Person): Map[String, Any] = {
  val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
  val vals = Person.unapply(person).get.productIterator.toSeq
  fieldNames.zip(vals).toMap
}

scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)

6
如果你正在使用Json4s,你可以按照以下方式进行操作:
import org.json4s.{Extraction, _}

case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")

Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]

4

你可以使用Shapeless。

case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)

定义一个LabelledGeneric表示。
import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
  implicit val lgenX = LabelledGeneric[X]
}
object Y {
  implicit val lgenY = LabelledGeneric[Y]
}

定义两个类型类来提供toMap方法

object ToMapImplicits {

  implicit class ToMapOps[A <: Product](val a: A)
    extends AnyVal {
    def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v }
  }

  implicit class ToMapOps2[A <: Product](val a: A)
    extends AnyVal {
    def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
      a.toMap[Symbol, Any]
        .map { case (k: Symbol, v) => k.name -> v.toString }
  }
}

然后您可以像这样使用它。
object Run  extends App {
  import ToMapImplicits._
  val x: X = X(true, "bike",26)
  val y: Y = Y("first", "second")
  val anyMapX: Map[String, Any] = x.mkMapAny
  val anyMapY: Map[String, Any] = y.mkMapAny
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)

  val stringMapX: Map[String, String] = x.mkMapString
  val stringMapY: Map[String, String] = y.mkMapString
  println("anyMapX = " + anyMapX)
  println("anyMapY = " + anyMapY)
}

这段代码打印

anyMapX = Map(c -> 26, b -> bike, a -> true)

anyMapY = Map(b -> second, a -> first)

stringMapX = Map(c -> 26, b -> bike, a -> true)

stringMapY = Map(b -> second, a -> first)

对于嵌套的case classes(因此嵌套的maps),请查看另一个答案


4

interpreter包中使用ProductCompletion解决方案:

import tools.nsc.interpreter.ProductCompletion

def getCCParams(cc: Product) = {
  val pc = new ProductCompletion(cc)
  pc.caseNames.zip(pc.caseFields).toMap
}

5
在Scala 2.10中,tools.nsc.interpreter.ProductCompletion被移动到其他地方了吗? - pdxleif

2

我不知道nice是什么意思,但至少对于这个非常基础的例子,这似乎有效。它可能需要一些改进,但足以让你开始了吗?基本上,它会过滤掉一个case类(或任何其他类:/)中所有“已知”的方法。

object CaseMappingTest {
  case class MyCase(a: String, b: Int)

  def caseClassToMap(obj: AnyRef) = {
    val c = obj.getClass
    val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
                          "toString")
    val casemethods = c.getMethods.toList.filter{
      n =>
        (n.getParameterTypes.size == 0) &&
        (n.getDeclaringClass == c) &&
        (! predefined.exists(_ == n.getName))

    }
    val values = casemethods.map(_.invoke(obj, null))
    casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
  }

  def main(args: Array[String]) {
    println(caseClassToMap(MyCase("foo", 1)))
    // prints: Map(a -> foo, b -> 1)
  }
}

2
糟糕,我错过了 Class.getDeclaredFields。 - André Laszlo

2

利用Java反射技术,但不改变访问级别。将Product和case类转换为Map[String, String]

def productToMap[T <: Product](obj: T, prefix: String): Map[String, String] = {
  val clazz = obj.getClass
  val fields = clazz.getDeclaredFields.map(_.getName).toSet
  val methods = clazz.getDeclaredMethods.filter(method => fields.contains(method.getName))
  methods.foldLeft(Map[String, String]()) { case (acc, method) =>
    val value = method.invoke(obj).toString
    val key = if (prefix.isEmpty) method.getName else s"${prefix}_${method.getName}"
    acc + (key -> value)
  }
}

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