考虑这个(非常难看的代码):
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
完美配合。
这里有什么问题?
答案 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
}
}