【问题标题】:Mapping hash map key/value pairs to named constructor arguments in Scala将哈希映射键/值对映射到 Scala 中的命名构造函数参数
【发布时间】:2023-03-15 02:26:01
【问题描述】:

是否可以将 Map 的键值对映射到具有命名参数的 Scala 构造函数?

也就是说,给定

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

...如何使用类似的地图创建 Person 实例

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

我最终想要实现的是一种将 Node4J Node 对象映射到 Scala 值对象的好方法。

【问题讨论】:

标签: scala


【解决方案1】:

这里的关键见解是构造函数参数名称​​是可用的,因为它们是构造函数创建的字段的名称。所以只要构造函数对它的参数不做任何事情,而是将它们分配给字段,那么我们可以忽略它并直接使用字段。

我们可以使用:

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)

【讨论】:

  • 这是我正在寻找的地方,尤其是使用Objenesis 来避免调用默认构造函数的最终解决方案。唯一让我有点不安的是依赖 java.lang.reflection API,因为我不确定 Scala 编译的字节码是否与 Java 的字节码 100% 兼容。但如果它有效......为什么不呢?非常感谢!
  • @Christoffer Soop:它与 Java 的字节码不“兼容”,它完全一样。它必须是为了在 JVM 上运行。
  • @Alexey 是的,字节码是 100% JVM 兼容的,但是你不能以 100% 的成功率将 Scala 编译的字节码反编译成 Java。我怎么知道反射有效?反射,据我所知,从字节码推断 Java 构造,但我不相信所有 scala 生成的字节码都有 Java 等价物。
【解决方案2】:

您在 cmets 中提到您正在寻找基于反射的解决方案。查看带有提取器的 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

【讨论】:

  • 不错的小费!然而,Duncan 的解决方案更完整,实际工作更完善,这就是我选择他的解决方案作为首选答案的原因。
【解决方案3】:

我猜你有不同的领域类别,所以这是我的建议。 (以下所有内容都已为 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")
}

适应你的口味。

附:糟糕,我没有意识到您专门询问了命名参数。虽然我的回答是关于位置参数的,但我还是把它留在这里,以防万一它有帮助。

【讨论】:

  • 是的,你是对的,问题的要点是关于映射命名参数,但感谢您的示例。我至少学会了一个新词“arity”... :-)
【解决方案4】:

由于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 方法。

【讨论】:

  • 谢谢 - 很优雅,但它仍然迫使我枚举 case 子句中的命名参数。我更多地考虑 Java 反射和 bean API,我可以在其中迭代所有设置器,使用属性名称作为映射中的键并调用设置器。
  • 我想显而易见的解决方案是创建一个 Person 对象,该对象将 Map 作为构造函数参数并手动创建设置器。就像 val 命名参数的优雅而简洁的语法一样...
  • 请注意,Map.toList 不保证返回列表的顺序,因此它很可能会返回 List(("ignored", "value"), ("firstname", "John"), ("lastname", "Doe"),而模式匹配将失败。
  • @Christoffer Soop:您需要构造函数参数名称,但它们似乎不可用。 download.oracle.com/javase/7/docs/api/java/lang/reflect/… Scala 特定的反射库可能在 2.10 中可用,但还没有。
  • @Alexey Romanov:你是对的,但也可以通过一些替代案例轻松解决。无论如何,谢谢。
猜你喜欢
  • 1970-01-01
  • 2021-06-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-21
  • 1970-01-01
相关资源
最近更新 更多