要在Scala中映射的案例类


75

有没有一种我可以转换Scalacase class实例的好方法,例如

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

变成某种映射,例如

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

它适用于任何案例类,而不仅仅是预定义的案例类。我发现您可以通过编写查询基础产品类的方法来提取案例类名称,例如

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

因此,我在寻找类似的解决方案,但针对案例类字段。我以为解决方案可能必须使用Java反射,但是如果案例类的基础实现发生变化,我不希望编写可能会在将来的Scala版本中破坏的内容。

目前,我正在使用Scala服务器,并使用case类定义协议及其所有消息和异常,因为它们是一个如此漂亮,简洁的构造。但是,然后我需要将它们转换为Java映射,以便通过消息传递层发送给任何客户端实现使用。我当前的实现只为每个案例类分别定义了一个翻译,但是最好找到一个通用的解决方案。


我发现这篇博客文章显示了如何使用宏来执行此操作。
Giovanni Botta 2013年

Answers:


93

这应该工作:

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

15
如果这不难,您能解释您写的内容吗?
den bardadym 2011年

现在有斯卡拉反射!我不确定现在是否还在试验中还是稳定的。无论如何,scala Reflection API可能会提供自己的解决方案,或者至少采用一种更多scala的方式来实现上述解决方案。顺便说一句:将setAccessible设置为true时,您也可以访问私有字段。这真的是您想要的吗?当SecurityManager处于活动状态时,它可能不起作用。
user573215 2013年

2
@RobinGreen案例类不能彼此继承
Giovanni Botta

1
@GiovanniBotta问题似乎是访问器方法未标记为可访问。很奇怪,因为这是一种公共方法。在任何情况下,都可以删除isAccessible,因为getMethod它只返回公共方法。另外,如果没有找到该方法,则会抛出accessor != null错误的测试。getMethodNoSuchMethodException
James_pic

2
可能更容易理解: case class Person(name: String, surname: String) val person = new Person("Daniele", "DalleMule") val personAsMap = person.getClass.getDeclaredFields.foldLeft(Map[String, Any]())((map, field) => { field.setAccessible(true) map + (field.getName -> field.get(person)) } )
DanieleDM 2015年

42

由于案例类扩展产品一,因此可以简单地用于.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不使用反射。

请注意,此示例适用于简单案例类,这些简单案例类不扩展其他类,也不在构造函数外部声明字段。


7
getDeclaredFields规范说:“返回的数组中的元素没有排序并且不以任何特定的顺序。” 字段如何以正确的顺序返回?
Giovanni Botta 2013年

没错,最好检查一下您的jvm / os,但实际上 stackoverflow.com/a/5004929/1180621
Andrejs 2013年

2
是的,我不会认为这是理所当然的。我不想开始编写非可移植的代码。
Giovanni Botta 2013年

如果case类嵌套在另一个对象中,则将引发异常,因为productIterator将不包含声明的“ $ outer”字段。
ssice

19

Starting Scala 2.13case classes(作为的实现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")

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
}

它还可以在任何嵌套级别将嵌套的案例类扩展为地图。


6

如果您不希望将其设为通用函数,这是一个简单的变体:

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)

4

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 2013年

4

您可以使用无形的。

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->自行车,a-> true)

anyMapY = Map(b->第二,a->第一)

stringMapX = Map(c-> 26,b->自行车,a-> true)

stringMapY = Map(b->第二,a->第一)

对于嵌套的案例类,(因此嵌套了地图)检查另一个答案


4

如果您碰巧正在使用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]]

2

我不知道尼斯...但是这似乎可行,至少对于这个非常基本的例子而言。它可能需要一些工作,但足以让您入门?基本上,它从案例类(或任何其他类:/)中过滤掉所有“已知”方法。

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。
安德烈·拉斯洛


0

随着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)
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.