Класс Case для отображения в Scala
Есть ли хороший способ конвертировать 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"
Поэтому я ищу подобное решение, но для полей класса случая. Я мог бы предположить, что решение могло бы использовать отражение Java, но я бы не хотел писать что-то, что может сломаться в будущем выпуске Scala, если изменится базовая реализация классов case.
В настоящее время я работаю на сервере Scala и определяю протокол и все его сообщения и исключения, используя классы падежей, поскольку они являются такой красивой и лаконичной конструкцией для этого. Но затем мне нужно перевести их в карту Java для отправки через уровень обмена сообщениями для использования любой клиентской реализацией. Моя текущая реализация просто определяет перевод для каждого класса case отдельно, но было бы неплохо найти обобщенное решение.
12 ответов
Это должно работать:
def getCCParams(cc: AnyRef) =
(Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Поскольку тематические классы расширяют 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
}
Одним из преимуществ продукта является то, что вам не нужно звонить setAccessible
на поле, чтобы прочитать его значение. Другое - это то, что productIterator не использует отражения.
Обратите внимание, что этот пример работает с простыми классами case, которые не расширяют другие классы и не объявляют поля вне конструктора.
Начало Scala 2.13
, case class
es (как реализации 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")
Если кто-нибудь ищет рекурсивную версию, вот модификация решения @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-классы в карты на любом уровне вложенности.
Вот простой вариант, если вам не нужно делать его универсальной функцией:
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)
Если вы используете 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]]
Решение с ProductCompletion
из пакета переводчика:
import tools.nsc.interpreter.ProductCompletion
def getCCParams(cc: Product) = {
val pc = new ProductCompletion(cc)
pc.caseNames.zip(pc.caseFields).toMap
}
Вы могли бы использовать бесформенный.
Позволять
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 -> правда)
anyMapY = Карта (b -> вторая, a -> первая)
stringMapX = Map (c -> 26, b -> bike, a -> true)
stringMapY = Map (b -> second, a -> first)
Для вложенных классов вариантов (таким образом, вложенных карт) проверьте другой ответ
Я не знаю о хорошем... но это, кажется, работает, по крайней мере, для этого очень простого примера. Это, вероятно, требует некоторой работы, но может быть достаточно, чтобы вы начали? В основном он отфильтровывает все "известные" методы из класса 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)
}
}
С использованием отражения 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)
}
}
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)
Подробности: https://github.com/hank-whu/common4s
Современный вариант со Scala 3 также может быть немного упрощен, как в следующем примере, который похож на ответ, опубликованный Уолтером user144628 выше.
def getCCParams(cc: AnyRef): Map[String, Any] =
cc.getClass.getDeclaredFields
.tapEach(_.setAccessible(true))
.foldLeft(Map.empty)((a, f) => a + (f.getName -> f.get(cc)))