Подтвердить что ты не робот

Как преобразовать карту в класс case в Scala?

Если у меня есть Map[String,String]("url" -> "xxx", "title" -> "yyy"), есть ли способ в целом преобразовать его в case class Image(url:String, title:String)?

Я могу написать помощника:

object Image{
  def fromMap(params:Map[String,String]) = Image(url=params("url"), title=params("title"))
}

но есть ли способ в целом написать это один раз для карты для любого класса case?

4b9b3361

Ответ 1

Прежде всего, есть несколько безопасных альтернатив, которые вы могли бы сделать, если хотите просто сократить код. Объект компаньона можно рассматривать как функцию, чтобы вы могли использовать что-то вроде этого:

def build2[A,B,C](m: Map[A,B], f: (B,B) => C)(k1: A, k2: A): Option[C] = for {
  v1 <- m.get(k1)
  v2 <- m.get(k2)
} yield f(v1, v2)

build2(m, Image)("url", "title")

Это вернет параметр, содержащий результат. В качестве альтернативы вы можете использовать ApplicativeBuilder в Scalaz, которые внутренне делают почти то же самое, но с более сильным синтаксисом:

import scalaz._, Scalaz._
(m.get("url") |@| m.get("title"))(Image)

Если вам действительно нужно сделать это через отражение, самым простым способом будет использование Paranamer (как это делает Lift-Framework). Paranamer может восстановить имена параметров, проверив байт-код, чтобы он попал в производительность, и он не будет работать во всех средах из-за проблем с загрузкой классов (например, REPL). Если вы ограничиваетесь классами только с параметрами конструктора String, вы можете сделать это следующим образом:

val pn = new CachingParanamer(new BytecodeReadingParanamer)

def fill[T](m: Map[String,String])(implicit mf: ClassManifest[T]) = for {
  ctor <- mf.erasure.getDeclaredConstructors.filter(m => m.getParameterTypes.forall(classOf[String]==)).headOption
  parameters = pn.lookupParameterNames(ctor)
} yield ctor.newInstance(parameters.map(m): _*).asInstanceOf[T]

val img = fill[Image](m)

(Обратите внимание, что этот пример может выбрать конструктор по умолчанию, поскольку он не проверяет количество параметров, которое вы хотите сделать)

Ответ 2

Здесь решение, использующее встроенное отражение scala/java:

  def createCaseClass[T](vals : Map[String, Object])(implicit cmf : ClassManifest[T]) = {
      val ctor = cmf.erasure.getConstructors().head
      val args = cmf.erasure.getDeclaredFields().map( f => vals(f.getName) )
      ctor.newInstance(args : _*).asInstanceOf[T]
  }

Чтобы использовать его:

val image = createCaseClass[Image](Map("url" -> "xxx", "title" -> "yyy"))

Ответ 3

Не полный ответ на ваш вопрос, но начало...

Это можно сделать, но это, вероятно, будет более сложным, чем вы думали. Каждый сгенерированный класс Scala аннотируется аннотацией Java ScalaSignature, чей член bytes может быть проанализирован, чтобы предоставить вам метаданные, которые вам понадобится (включая имена аргументов). Однако формат этой подписи не является API, поэтому вам нужно будет разобрать его самостоятельно (и, скорее всего, измените способ его разбора с каждой новой крупной версией Scala).

Возможно, лучшим местом для начала является библиотека lift-json, которая имеет возможность создавать экземпляры классов case на основе данных JSON.

Обновление: Я думаю, что lift-json фактически использует Paranamer, чтобы сделать это, и, следовательно, может не проанализируйте байты ScalaSignature... Что делает эту технику работать и для классов не Scala.

Обновить 2: Посмотрите ответ Мориц, который лучше информирован, чем я.

Ответ 4

Там вы можете преобразовать карту в json, а затем в класс case. Я использовал аэрозоль

import spray.json._

object MainClass2 extends App {
  val mapData: Map[Any, Any] =
    Map(
      "one" -> "1",
      "two" -> 2,
      "three" -> 12323232123887L,
      "four" -> 4.4,
      "five" -> false
    )

  implicit object AnyJsonFormat extends JsonFormat[Any] {
    def write(x: Any): JsValue = x match {
      case int: Int           => JsNumber(int)
      case long: Long          => JsNumber(long)
      case double: Double        => JsNumber(double)
      case string: String        => JsString(string)
      case boolean: Boolean if boolean  => JsTrue
      case boolean: Boolean if !boolean => JsFalse
    }
    def read(value: JsValue): Any = value match {
      case JsNumber(int) => int.intValue()
      case JsNumber(long) => long.longValue()
      case JsNumber(double) => double.doubleValue()
      case JsString(string) => string
      case JsTrue      => true
      case JsFalse     => false
    }
  }

  import ObjJsonProtocol._
  val json = mapData.toJson
  val result: TestObj = json.convertTo[TestObj]
  println(result)

}

final case class TestObj(one: String, two: Int, three: Long, four: Double, five: Boolean)

object ObjJsonProtocol extends DefaultJsonProtocol {
  implicit val objFormat: RootJsonFormat[TestObj] = jsonFormat5(TestObj)
}

и используйте эту зависимость в sbt build:

 "io.spray"          %%   "spray-json"     %   "1.3.3"

Ответ 5

Это невозможно сделать, так как вам нужно, чтобы объект-компаньон применял имена параметров метода метода, и они просто недоступны с помощью отражения. Если у вас много таких классов, вы можете анализировать свои объявления и генерировать методы fromMap.