Programing

Scala에서 매핑 할 케이스 클래스

crosscheck 2020. 10. 29. 07:49
반응형

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의 향후 릴리스에서 중단 될 수있는 내용을 작성하고 싶지 않습니다.

현재 저는 Scala 서버에서 작업하고 있으며이를위한 아름답고 간결한 구조이기 때문에 케이스 클래스를 사용하여 프로토콜과 모든 메시지 및 예외를 정의하고 있습니다. 그러나 그런 다음 클라이언트 구현이 사용할 메시징 계층을 통해 전송하기 위해 Java 맵으로 변환해야합니다. 내 현재 구현은 각 케이스 클래스에 대한 번역을 개별적으로 정의하지만 일반화 된 솔루션을 찾는 것이 좋습니다.


이것은 작동합니다.

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
}

Product의 한 가지 장점은 setAccessible값을 읽기 위해 필드를 호출 할 필요가 없다는 것 입니다. 또 다른 것은 productIterator가 리플렉션을 사용하지 않는다는 것입니다.

이 예제는 다른 클래스를 확장하지 않고 생성자 외부에서 필드를 선언하지 않는 간단한 케이스 클래스에서 작동합니다.


재귀 버전을 찾는 사람이 있다면 @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 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)

무형을 사용할 수 있습니다.

허락하다

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-> 첫 번째)

중첩 된 케이스 클래스의 경우 (따라서 중첩 된 맵) 다른 답변을 확인하십시오.


Starting Scala 2.13, case classes (as implementations of Product) are provided with a productElementNames method which returns an iterator over their field's names.

By zipping field names with field values obtained with productIterator we can generically obtain the associated 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")

Solution with ProductCompletion from interpreter package:

import tools.nsc.interpreter.ProductCompletion

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

If you happen to be using Json4s, you could do the following:

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

I don't know about nice... but this seems to work, at least for this very very basic example. It probably needs some work but might be enough to get you started? Basically it filters out all "known" methods from a case class (or any other class :/ )

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

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

Details: https://github.com/hank-whu/common4s

참고URL : https://stackoverflow.com/questions/1226555/case-class-to-map-in-scala

반응형