2011-08-17 48 views
0

考慮這個(非常醜陋的代碼)工作:NullPointerException異常而有狀態PartialFunction和collectFirst

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

我試圖通過對如圖getLinkNew使用collectFirst改善getLinkOld的方法,但我總是得到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完美。

這裏有什麼問題?

回答

3

你的匹配器作爲isDefined副作用產生。將副作用函數傳遞給例程(例如map)通常是災難的祕訣,但這甚至不會發生在這裏。您的代碼需要isDefinedapply之前被調用,具有相同的參數。這使得你的代碼非常脆弱,這就是你應該改變的。

PartialFunction的客戶通常不必遵循該協議。想象一下,例如

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

這裏調用apply代碼甚至沒有你的,但是集合類,所以你無法控制會發生什麼。

getLinkNew你的具體問題是:isDefined簡直是從來沒有的collectFirst called.The PartialFunction參數{case m => ...}。被調用的isDefined是這個函數的isDefined。由於m是一個無可辯駁的模式,它總是真實的,collectFirst將始終返回第一個元素,如果有的話。部分函數返回另一個不在m處定義的部分函數(Mapping)是無關緊要的。

編輯 - 可能的解決方法

一個很輕的變化將是檢查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甚至不出現在您的getLinkNew代碼中,這應該令人擔憂)。問題是人們需要一個NodeSeq的PartialFunction(不是實體,它將被函數所知,但不會作爲參數傳遞)。 isDefined將被調用,然後應用。模式和匹配器取決於NodeSeq,因此它們不能事先創建,但只能在定義和/或應用中使用。本着同樣的精神,您可以緩存isDefined中計算的內容以在Apply中重複使用。這絕對是不漂亮

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 
    } 

} 

您使用與

+0

謝謝!你認爲什麼樣的解決方案不會計算兩次? – soc

+0

提出了一種解決方法,可以對代碼進行最小限度的更改,而不是作爲有更多空間的評論。 –