使用有状态的PartialFunction和collectFirst时出现NullPointerException

时间:2011-08-17 21:52:50

标签: regex xml scala configuration

考虑这个(非常难看的代码):

object ExternalReferences2 {
  import java.util.regex._

  implicit def symbol2string(sym: Symbol) = sym.name

  object Mapping {
    def fromXml(mapping: scala.xml.NodeSeq) = {
      new Mapping(mapping \ 'vendor text, 
                  mapping \ 'match text, 
                  mapping \ 'format text)
    }
  }
  case class Mapping(vendor: String, 
                     matches: String,
                     format: String) extends PartialFunction[String, String] {
    private val pattern = Pattern.compile(matches)
    private var _currentMatcher: Matcher = null
    private def currentMatcher = 
      { println("Getting matcher: " + _currentMatcher); _currentMatcher }
    private def currentMatcher_=(matcher: Matcher) = 
      { println("Setting matcher: " + matcher); _currentMatcher = matcher }

    def isDefinedAt(entity: String) = 
      { currentMatcher = pattern.matcher(entity); currentMatcher.matches }

    def apply(entity: String) = apply

    def apply = {
      val range = 0 until currentMatcher.groupCount()
      val groups = range 
                     map (currentMatcher.group(_)) 
                     filterNot (_ == null) 
                     map (_.replace('.', '/'))
      format.format(groups: _*)
    }
  }

  val config =
    <external-links>
      <mapping>
        <vendor>OpenJDK</vendor>
        <match>{ """^(javax?|sunw?|com.sun|org\.(ietf\.jgss|omg|w3c\.dom|xml\.sax))(\.[^.]+)+$""" }</match>
        <format>{ "http://download.oracle.com/javase/7/docs/api/%s.html" }</format>
      </mapping>
    </external-links>

  def getLinkNew(entity: String) =
     (config \ 'mapping) 
       collectFirst({ case m => Mapping.fromXml(m)})
       map(_.apply)

  def getLinkOld(entity: String) =
    (config \ 'mapping).view 
      map(m => Mapping.fromXml(m)) 
      find(_.isDefinedAt(entity)) 
      map(_.apply)
}

我尝试使用getLinkOld改进collectFirst方法,如getLinkNew所示,但我总是得到NullPointerException,因为_currentMatcher仍设置为null

scala> ExternalReferences2.getLinkNew("java.util.Date")
Getting matcher: null
java.lang.NullPointerException
    at ExternalReferences2$Mapping.apply(<console>:32)
    at ExternalReferences2$$anonfun$getLinkNew$2.apply(<console>:58)
    at ExternalReferences2$$anonfun$getLinkNew$2.apply(<console>:58)
    at scala.Option.map(Option.scala:131)
    at ExternalReferences2$.getLinkNew(<console>:58)
    at .<init>(<console>:13)
    at .<clinit>(<console>)
    at .<init>(<console>:11)
    at .<clinit>(<console>)

虽然它与getLinkOld完美配合。

这里有什么问题?

1 个答案:

答案 0 :(得分:3)

您的匹配器在isDefined中被创建为副作用。将副作用函数传递给诸如map之类的例程通常会导致灾难,但这甚至不是这里发生的事情。您的代码需要在isDefined之前调用apply,并使用相同的参数。这使你的代码非常脆弱,这就是你应该改变的。

PartialFunction的客户端通常不必遵循该协议。想象一下,例如

if (f.isDefinedAt(x) && f.isDefinedAt(y)) {fx = f(x); fy = f(y)}. 

这里调用apply的代码甚至不是你的代码,而是集合类',所以你无法控制发生的事情。

getLinkNew中的具体问题是isDefined从未被调用。PartialFunction的{​​{1}}参数为collectFirst。调用的{case m => ...}是此函数的isDefined。由于isDefined是一个无可辩驳的模式,它总是正确的,而且如果有第一个元素,则collectFirst将始终返回第一个元素。部分函数返回另一个不在m定义的部分函数(Mapping),这是无关紧要的。

修改 - 可能的解决方法

一个非常轻微的变化是检查m是否可用,如果不可用则创建它。更好的是,保留用于创建它的matcher字符串,以便您可以检查它是否正确。只要没有多线程,这应该使副作用变得良性。但是,不要使用entity,请使用null,因此编译器不会忽略它可能是Option的可能性。

None

再次修改。愚蠢我

抱歉,所谓的解决方法确实使类更安全,但它不会使var _currentMatcher : Option[(String, Matcher)] = None def currentMatcher(entity: String) : Matcher = _currentMatcher match{ case Some(e,m) if e == entity => m case _ => { _currentMatcher = (entity, pattern.matcher(entity)) _currentmatcher._2 } } 解决方案起作用。同样,始终定义collectFirst部分功能(注意:case m =>甚至不会出现在您的entity代码中,这应该令人担忧。问题是需要一个NodeSeq的PartialFunction(不是实体,它将为函数所知,但不作为参数传递)。将调用isDefined,然后应用。模式和匹配器取决于NodeSeq,因此无法事先创建它们,但仅限于isDefined和/或apply。本着同样的精神,您可以缓存isDefined中计算的内容以在Apply中重用。这绝对不是很好

getLinkNew

您将其与def linkFor(entity: String) = new PartialFunction[NodeSeq, String] { var _matcher : Option[String, Matcher] = None def matcher(regexp: String) = _matcher match { case Some(r, m) where r == regexp => m case None => { val pattern = Pattern.compile(regexp) _matcher = (regexp, pattern.matcher(entity)) _matcher._2 } } def isDefined(mapping: NodeSeq) = { matcher(mapping \ "match" text).matches } def apply(mapping: NodeSeq) = { // call matcher(...), it is likely to reuse previous matcher, build result } }

一起使用