Как я могу преобразовать Map в класс case в Scala? - PullRequest
16 голосов
/ 31 мая 2011

Если у меня есть 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"))
}

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

Ответы [ 5 ]

8 голосов
/ 31 мая 2011

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

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 s в 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)

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

6 голосов
/ 04 сентября 2013

Вот решение с использованием встроенного отражения 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"))
2 голосов
/ 31 мая 2011

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

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

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

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

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

0 голосов
/ 25 августа 2017

Есть хак, который вы можете преобразовать map в json, а затем в case-класс.Я использовал spray-json

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:

 "io.spray"          %%   "spray-json"     %   "1.3.3"
0 голосов
/ 31 мая 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...