在 Scala 中映射的 Case 类

有没有一种很好的方法可以转换一个 Scala case class实例,例如。

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

变成某种映射,例如。

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

它适用于任何 case 类,而不仅仅是预定义的 case 类。我发现您可以通过编写一个方法来查询底层 Product 类,例如,将 case 类的名称提取出来。

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

因此,我正在寻找一个类似的解决方案,但案例类字段。我可以想象一个解决方案可能必须使用 Java 反射,但是我讨厌编写一些东西,因为如果 case 类的底层实现发生变化,这些东西可能会在未来的 Scala 版本中中断。

目前,我正在使用 case 类定义 Scala 服务器和协议及其所有消息和异常,因为 case 类是一个非常漂亮、简洁的结构。但是接下来我需要将它们转换成 Java 映射,以便发送消息传递层供任何客户端实现使用。我目前的实现只是分别为每个 case 类定义了一个翻译,但是如果能找到一个通用的解决方案就更好了。

57490 次浏览

我不知道什么是好... 但是这似乎有效,至少对于这个非常基本的例子来说。它可能需要一些工作,但可能足以让你开始?基本上,它过滤掉来自 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)
}
}

这应该会奏效:

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

解决方案与 ProductCompletion从解释程序包:

import tools.nsc.interpreter.ProductCompletion


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

因为 case 类扩展了 产品,所以可以简单地使用 .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 类,它不扩展其他类,也不在构造函数之外声明字段。

如果你不想把它变成一个通用函数,这里有一个简单的变体:

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)

如果有人在寻找递归版本,这里是@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-class 扩展为嵌套级别的映射。

commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)

详情: https://github.com/hank-whu/common4s

你可以用无形的。

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 = 地图(c-> 26,b-> 自行车,a-> true)

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

StringMapX = Map (c-> 26,b-> motor,a-> true)

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

对于嵌套案例类,(因此是嵌套映射) 检查 < a href = “ https://stackoverflow. com/a/31638390”> 另一个答案

如果你碰巧在使用 Json4,你可以做以下事情:

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]]

Scala 2.13开始,case 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")

使用 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)
}
}