简体   繁体   English

将哈希映射键/值对映射到Scala中的命名构造函数参数

[英]Mapping hash map key/value pairs to named constructor arguments in Scala

Is it possible to map the key value pairs of a Map to a Scala constructor with named parameters? 是否可以将Map的键值对映射到具有命名参数的Scala构造函数?

That is, given 也就是说,给定

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

... how can I create an instance of Person using a map like ...如何使用类似的地图创建Person的实例

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

What I am trying to achieve in the end is a nice way of mapping Node4J Node objects to Scala value objects. 我最终想要实现的是将Node4J Node对象映射到Scala值对象的好方法。

The key insight here is that the constructor arguments names are available, as they are the names of the fields created by the constructor. 这里的关键见解是构造函数参数名称可用的,因为它们是构造函数创建的字段的名称。 So provided that the constructor does nothing with its arguments but assign them to fields , then we can ignore it and work with the fields directly. 因此,如果构造函数不对其参数执行任何操作,而是将它们分配给字段 ,那么我们可以忽略它并直接使用这些字段。

We can use: 我们可以用:

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)
  }

Which we can call on a blank person: 我们可以打电话给一个空白的人:

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)
}

Of course we can do better with a little pimping: 当然,我们可以通过一点拉皮条来做得更好:

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 :_*))
}

so that you can call: 这样你就可以打电话:

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

If having to call the constructor is annoying, Objenesis lets you ignore the lack of a no-arg constructor: 如果必须调用构造函数很烦人,Objenesis允许您忽略缺少无参数构造函数:

val objensis = new ObjenesisStd 

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

Now we can combine the two to write 现在我们可以把两者结合起来写

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

You mentioned in the comments that you're looking for a reflection based solution. 您在评论中提到您正在寻找基于反射的解决方案。 Have a look at JSON libraries with extractors, which do something similar. 看看带有提取器的JSON库,它们可以做类似的事情。 For example, lift-json has some examples , 例如, lift-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)

To get what you want, you could convert your Map[String, String] into JSON format and then run the case class extractor. 要获得所需内容,可以将Map[String, String]转换为JSON格式,然后运行case类提取器。 Or you could look into how the JSON libraries are implemented using reflection . 或者您可以查看如何使用反射实现 JSON库。

I guess you have domain classes of different arity, so here it is my advice. 我猜你有不同arity的域类,所以这是我的建议。 (all the following is ready for REPL) (以下所有内容均可用于REPL)

Define an extractor class per TupleN , eg for Tuple2 (your example): 为每个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]) ...

You may define a helper function to make building extractors easier: 您可以定义辅助函数以使构建提取器更容易:

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

Let's make an extractor object 让我们做一个提取器对象

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

and build Person : 和建立Person

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

or 要么

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

Adapt to your taste. 适应您的口味。

PS Oops, I didn't realize you asked about named arguments specifically. PS哎呀,我没有意识到你特意询问了有关命名的论点。 While my answer is about positional arguments, I shall still leave it here just in case it could be of some help. 虽然我的答案是关于位置论证,但我仍然会留在这里,以防它可以提供一些帮助。

Since Map is essentially just a List of tuples you can treat it as such. 由于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)

I made Person a case class to have the toString method generated for me. 我让Person成为一个case类,为我生成了toString方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM