Scala案例类的反思

12

我想写一个trait(在Scala 2.8中),可以混入到case类中,在特定的调试目的下,允许在运行时检查其字段。 我希望按照它们在源文件中声明的顺序将它们返回,并且我想省略case类中的任何其他字段。例如:

trait CaseClassReflector extends Product {

  def getFields: List[(String, Any)] = {
    var fieldValueToName: Map[Any, String] = Map()
    for (field <- getClass.getDeclaredFields) {
      field.setAccessible(true)
      fieldValueToName += (field.get(this) -> field.getName) 
    }
    productIterator.toList map { value => fieldValueToName(value) -> value }
  }

}

case class Colour(red: Int, green: Int, blue: Int) extends CaseClassReflector {
  val other: Int = 42
}

scala> val c = Colour(234, 123, 23)
c: Colour = Colour(234,123,23)

scala> val fields = c.getFields    
fields: List[(String, Any)] = List((red,234), (green,123), (blue,23))

上述实现明显存在缺陷,因为它通过比较字段位置和名称上的值之间的相等关系来猜测它们之间的关联,所以以下代码将无法正常工作:

Colour(0, 0, 0).getFields

这个能实现吗?


你的代码有一个bug。值不是唯一的,因此当多个字段具有相同名称时,你将使用(field.get(this) -> field.getName)覆盖值。 请参见下面重写后的代码。 - Sagie Davidovich
@SagieDavidovich 确实,正如所指出的,“上述实现明显存在缺陷”。 - Matt R
4个回答

10

在主干(trunk)中你会找到这个。请仔细阅读注释,不过这不被支持: 但是因为我也需要那些名称...

/** private[scala] so nobody gets the idea this is a supported interface.
 */
private[scala] def caseParamNames(path: String): Option[List[String]] = {
  val (outer, inner) = (path indexOf '$') match {
    case -1   => (path, "")
    case x    => (path take x, path drop (x + 1))
  }

  for {
    clazz <- getSystemLoader.tryToLoadClass[AnyRef](outer)
    ssig <- ScalaSigParser.parse(clazz)
  }
  yield {
    val f: PartialFunction[Symbol, List[String]] =
      if (inner.isEmpty) {
        case x: MethodSymbol if x.isCaseAccessor && (x.name endsWith " ") => List(x.name dropRight 1)
      }
      else {
        case x: ClassSymbol if x.name == inner  =>
          val xs = x.children filter (child => child.isCaseAccessor && (child.name endsWith " "))
          xs.toList map (_.name dropRight 1)
      }

    (ssig.symbols partialMap f).flatten toList
  }
}

10

这是一个简短有效的版本,基于上面的例子。

  trait CaseClassReflector extends Product {
    def getFields = getClass.getDeclaredFields.map(field => {
      field setAccessible true
      field.getName -> field.get(this)
    })
  }

我尝试了这个方法,至少对于一个简单的case class是有效的。 - Kenji Matsuoka

7
在我看到的每个示例中,字段的顺序都是相反的:在getFields数组中的最后一个项目是在case类中列出的第一个项目。如果您“好好”使用案例类,则应该能够将productElement(n)映射到getDeclaredFields()(getDeclaredFields.length-n-1)
但这相当危险,因为我不知道规范中是否坚持必须这样做,如果您在案例类中重写val,则它甚至不会出现在getDeclaredFields中(它将出现在该超类的字段中)。
您可以更改代码以假设事情是这样的,但请检查具有该名称的getter方法和productIterator返回相同的值,并在它们不同时引发异常(这意味着您实际上不知道对应于什么)。

1
我遇到了和Matt R一样的问题。作为Scala的相对新手,您能否请详细解释一下您的答案。那将非常有帮助。谢谢! - Core_Dumped
2
@Core_Dumped - 其实我认为,除非你能使用我上面的模糊提示来制定自己的解决方案,否则你更有可能遇到麻烦而不是解决问题。就像我说的,“这相当危险”。你有责任能够预见并避免出现问题,这可能需要让你从“相对新手”转变至“不是”至少在这方面是如此。在REPL中尝试使用反射和样例类,并看看是否可以找出解决方法! - Rex Kerr
getClass.getDeclaredFields.map(_.getName).zip(productIterator.toList).toMap - Ákos Vandra-Meyer
我在我的代码库中发现了@ÁkosVandra的评论。它适用于Scala 2.11,但如果一个case类包含一个lazy val,则在Scala 2.12上会出现问题,因此我不建议继续使用它。 - Philluminati

4

您也可以使用解释器包中的ProductCompletion来获取案例类的属性名称和值:

import tools.nsc.interpreter.ProductCompletion

// get attribute names
new ProductCompletion(Colour(1, 2, 3)).caseNames
// returns: List(red, green, blue)

// get attribute values
new ProductCompletion(Colour(1, 2, 3)).caseFields

编辑:由roland和virtualeyes提示

需要包括 scalap 库,这是 scala-lang 集合 的一部分。

感谢 roland 和 virtualeyes 的提示。


1
请注意,只有在类路径上找到scalap(http://www.scala-lang.org/node/292)时,才能调用`caseNames`。否则,在使用Scala 2.9.1时将返回一个空列表。 - Roland Ewald
1
+1 @Roland,你说得没错。考虑到在谷歌搜索中 scalap 下载并不是很明显,这里提供了 2.9.1 版本的下载链接:https://oss.sonatype.org/content/groups/scala-tools/org/scala-lang/scalap/2.9.1/。 - virtualeyes
需要在能够反射之前拥有一个 case class 实例的困境是一个进退两难的局面。希望 2.10 版本能够在这方面有所改善,因为在 2.9 版本中受到了限制,使用黑盒 case classes 是一件非常麻烦的事情,最终会导致在领域模型、ORM 映射和验证方面进行三次重复编码,真是令人困惑... - virtualeyes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接