Клас справи на карті в Скалі


75

Чи є хороший спосіб перетворити 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. Я вважаю, що рішення, можливо, має використовувати відображення Java, але я не хочу писати щось, що може зламатися в майбутньому випуску Scala, якщо основна реалізація класів кейсів зміниться.

В даний час я працюю на сервері Scala та визначаю протокол та всі його повідомлення та винятки, використовуючи класи case, оскільки вони є такою красивою, стислою конструкцією для цього. Але тоді мені потрібно перевести їх на карту Java, щоб переслати рівень обміну повідомленнями для використання будь-якою реалізацією клієнта. Моя поточна реалізація просто визначає переклад для кожного класу випадку окремо, але було б непогано знайти узагальнене рішення.


Я знайшов цю публікацію в блозі, де показано, як використовувати макроси для цього.
Джованні Ботта,

Відповіді:


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

Тепер є відбиття масштабу! Я не впевнений, що це все ще експериментально чи стабільно, зараз. У будь-якому випадку API Scala Refligence відображає власне рішення або, принаймні, більш масштабований спосіб реалізації такого рішення, як наведене вище. І до речі: коли ви використовуєте setAccessible to true, ви також можете отримати доступ до приватних полів. Це насправді те, що ти хочеш? І це може не спрацювати, коли активний SecurityManager.
user573215

2
Класи справи @RobinGreen не можуть успадковувати один одного
Джованні Ботта

1
@GiovanniBotta Проблема, схоже, полягає в тому, що метод доступу не позначений як доступний. Дивно, адже це публічний метод. У будь-якому випадку, витягнути файл має бути добре isAccessible, оскільки getMethodповертає лише загальнодоступні методи. Крім того, accessor != nullє неправильним тестом, оскільки getMethodвидає a, NoSuchMethodExceptionякщо метод не знайдений.
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

42

Оскільки класи 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
}

Однією з переваг Продукту є те, що Вам не потрібно телефонувати setAccessible в поле, щоб прочитати його значення. Іншим є те, що productIterator не використовує відображення.

Зверніть увагу, що цей приклад працює з простими класами регістрів, які не розширюють інші класи і не оголошують поля поза конструктором.


7
У getDeclaredFieldsспецифікації сказано: "Елементи у масиві, що повертаються, не сортуються і не перебувають у певному порядку". Як так поля повертаються у правильному порядку?
Джованні Ботта,

Правда, найкраще перевіряти свої jvm / os, але на практиці stackoverflow.com/a/5004929/1180621
Andrejs

2
Так, я б не сприймав це як належне. Я не хочу починати писати не переносний код.
Джованні Ботта,

Це призведе до винятку, якщо клас справи вкладений в інший об'єкт, оскільки productIterator не буде містити оголошене поле "$ external".
ssice

20

Починаючи 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")

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
Чи переміщено tools.nsc.interpreter.ProductCompletion кудись ще в Scala 2.10?
pdxleif

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 = Карта (c -> 26, b -> велосипед, a -> true)

anyMapY = Карта (b -> друга, a -> перша)

stringMapX = Карта (c -> 26, b -> велосипед, a -> true)

stringMapY = Карта (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

Я не знаю про приємне ... але це, здається, працює, принаймні для цього дуже базового прикладу. Ймовірно, для цього потрібно трохи попрацювати, але цього може бути достатньо для початку? В основному він відфільтровує всі "відомі" методи з класу 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.
Андре Ласло


0

З використанням відображення Java, але без зміни рівня доступу. Перетворює Product та клас регістрів у 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)
  }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.