是否可以将Map的键值对映射到具有命名参数的Scala构造函数?
即,给定
class Person(val firstname: String, val lastname: String) {
...
}
...如何使用像
这样的地图创建Person的实例val args = Map("firstname" -> "John", "lastname" -> "Doe", "ignored" -> "value")
我最终想要实现的是将Node4J Node
对象映射到Scala值对象的好方法。
答案 0 :(得分:11)
这里的关键见解是构造函数参数名称是可用,因为它们是构造函数创建的字段的名称。所以如果构造函数对其参数没有任何作用,只是将它们分配给字段,那么我们可以忽略它并直接使用这些字段。
我们可以使用:
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会让你忽略缺少无参数的构造函数:
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)
答案 1 :(得分:1)
您在评论中提到您正在寻找基于反射的解决方案。看看带有提取器的JSON库,它们可以做类似的事情。例如,lift-json有一些examples,
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格式,然后运行案例类提取器。或者您可以查看JSON库是如何implemented using reflection。
答案 2 :(得分:1)
我猜你有不同arity的域类,所以这是我的建议。 (以下所有内容已准备好进行REPL)
按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")
}
适应您的口味。
P.S。糟糕,我没有意识到你特别询问了有关命名的论点。虽然我的答案是关于位置论证,但我仍然会留在这里,以防它可以提供一些帮助。
答案 3 :(得分:0)
由于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
创建了一个案例类,以便为我生成toString
方法。