Сопоставление пар ключ/значение хэш-карты именованным аргументам конструктора в Scala

можно ли сопоставить пары ключевых значений карты конструктору Scala с именованными параметрами?

то есть, учитывая

class Person(val firstname: String, val lastname: String) {
    ...
}

... как я могу создать экземпляр Person с помощью карты, такой как

val args = Map("firstname" -> "John", "lastname" -> "Doe", "ignored" -> "value")

то, что я пытаюсь достичь в конце концов, является хорошим способом отображения Node4J Node объекты для объектов значения Scala.

4 ответов


ключевым моментом здесь является то, что имена аргументов конструктора are доступно, так как они являются именами полей, созданных конструктором. Так что при условии, что конструктор ничего не делает со своими аргументами, но назначает их полям, тогда мы можем игнорировать его и работать с полями напрямую.

мы можем использовать:

def setFields[A](o : A, values: Map[String, Any]): A = {
  for ((name, value) <- values) setField(o, name, value)
  o
}

def setField(o: Any, fieldName: String, fieldValue: Any) {
  // TODO - look up the class hierarchy for superclass fields
  o.getClass.getDeclaredFields.find( _.getName == fieldName) match {
    case Some(field) => {
      field.setAccessible(true)
      field.set(o, fieldValue)
    }
    case None =>
      throw new IllegalArgumentException("No field named " + fieldName)
  }

который мы можем вызвать на пустой человек:

test("test setFields") {
  val p = setFields(new Person(null, null, -1), Map("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44))
  p.firstname should be ("Duncan")
  p.lastname should be ("McGregor")
  p.age should be (44)
}

конечно, мы можем сделать лучше с маленькой сутенерство:

implicit def any2WithFields[A](o: A) = new AnyRef {
  def withFields(values: Map[String, Any]): A = setFields(o, values)
  def withFields(values: Pair[String, Any]*): A = withFields(Map(values :_*))
}

так что вы можете позвонить:

new Person(null, null, -1).withFields("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44)

если необходимость вызова конструктора раздражает, Objenesis позволяет игнорировать отсутствие конструктора no-arg:

val objensis = new ObjenesisStd 

def create[A](implicit m: scala.reflect.Manifest[A]): A = 
  objensis.newInstance(m.erasure).asInstanceOf[A]

теперь мы можем объединить два, чтобы написать

create[Person].withFields("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44)

Вы упомянули в комментариях, что ищете решение на основе отражения. Посмотрите на библиотеки JSON с экстракторами, которые делают что-то подобное. Например, лифт-JSON-формате некоторые примеры,

case class Child(name: String, age: Int, birthdate: Option[java.util.Date])

val json = parse("""{ "name": null, "age": 5, "birthdate": null }""")
json.extract[Child] == Child(null, 5, None)

чтобы получить то, что вы хотите, вы можете превратить вашу Map[String, String] в формат JSON, а затем запустите экстрактор класса case. Или вы можете посмотреть, как библиотеки JSON реализовано с помощью отражения.


Я думаю, у вас есть доменные классы разных arity, так что вот мой совет. (все следующее готово для REPL)

определите класс экстрактора per TupleN, например,Tuple2 (ваш пример):

class E2(val t: Tuple2[String, String]) {
  def unapply(m: Map[String,String]): Option[Tuple2[String, String]] =
    for {v1 <- m.get(t._1)
         v2 <- m.get(t._2)}
    yield (v1, v2)
}

// class E3(val t: Tuple2[String,String,String]) ...

вы можете определить вспомогательную функцию, чтобы сделать строительные экстракторы проще:

def mkMapExtractor(k1: String, k2: String) = new E2( (k1, k2) )
// def mkMapExtractor(k1: String, k2: String, k3: String) = new E3( (k1, k2, k3) )

давайте сделаем объект экстрактора

val PersonExt = mkMapExtractor("firstname", "lastname")

и построить Person:

val testMap = Map("lastname" -> "L", "firstname" -> "F")
PersonExt.unapply(testMap) map {Person.tupled}

или

testMap match {
  case PersonExt(f,l) => println(Person(f,l))
  case _ => println("err")
}

адаптироваться к вашему вкус.

П. С. упс, я не понял, ты спросил про именованные аргументы конкретно. Хотя мой ответ касается позиционных аргументов, я все равно оставлю его здесь на случай, если это может помочь.


С Map по существу только List кортежей вы можете рассматривать его как таковой.

scala> val person = args.toList match {
   case List(("firstname", firstname), ("lastname", lastname), _) => new Person(firstname, lastname)
   case _ => throw new Exception
}
person: Person = Person(John,Doe)

Я Person класс case, чтобы иметь toString метод, созданный для меня.