9.8 使用提取器进行匹配
使用Scala提取器(Extractor),可以将模式匹配带到下一个阶段:匹配任意模式。正如其名字所示,提取器会从输入中提取出匹配的部分。假定我们在写一个服务,处理股票相关的输入。对我们来说,手头的第一个工作就是接收股票代码,返回这个股票的价格(为了演示,这里打印出结果)。如下例所示:
StockService process "GOOG"
StockService process "IBM"
StockService process "ERR"
process()
方法需要校验给定的代码是否有效,如果有效,则返回其股价。代码如下:
object StockService {
def process(input : String) {
input match {
case Symbol() => println("Look up price for valid symbol " + input)
case _ => println("Invalid input " + input)
}
}
}
process()
方法使用了尚未定义的提取器Symbol
执行模式匹配。如果提取器确定股票代码有效,就会返回true
,否则,返回false
。如果返回true
,会执行同case
关联的表达式。否则,模式匹配继续下一个case
。一起来看看提取器示例:
object Symbol {
def unapply(symbol : String) : Boolean = symbol == "GOOG" || symbol == "IBM"
// you'd look up database above... here only GOOG and IBM are recognized
}
提取器有个方法叫unapply()
,接收要匹配的值。执行case Symbol() => …
时,match
表达式会自动把input
作为参数传入unapply()
。执行上面的3段代码,(记得把调用服务的那个样例放到文件底部。)会得到如下结果:
Look up price for valid symbol GOOG
Look up price for valid symbol IBM
Invalid input ERR
或许,你会觉得用unapply()
做方法名太诡异了。你可能认为,提取器应该有个类似于evaluate()
之类的方法。用unapply这个名字的原因是,提取器有个可选的apply()
方法。apply()
和unapply()
这两个方法会执行相反的动作。unapply()
将对象分解为用以匹配模式的片段,而apply()
则是为了提供一个把它们组合回去的选择。
现在,我们可以请求股票报价了,对于服务而言,下一个任务是设置股票价格。假设这个消息的格式是“SYMBOL:PRICE”。我们需要对这种格式进行模式匹配,然后采取行动。下面是修改过的process()
方法,处理了这个附加的任务:
PatternMatching/Extractor.scala
object StockService {
def process(input : String) {
input match {
case Symbol() => println("Look up price for valid symbol " + input)
case ReceiveStockPrice(symbol, price) =>
printf("Received price %f for symbol %s\n", price, symbol)
case _ => println("Invalid input " + input)
}
}
}
这里添加了一个新的case
,用到尚未编写的提取器ReceiveStockPrice
。这个提取器不同于之前编写的Symbol
提取器。后者只返回了一个boolean结果,而ReceiveStockPrice
则需要解析输入,返回两个值,symbol
和price
。在case
语句里,它们被指定成ReceiveStockPrice
的实参;不过,它们并不是传入的实参,而是从提取器中传出的实参。所以,symbol
和price
并不是用来传递值,而是用来接收值的。
我们看一下ReceiveStockPrice
提取器。你可能已经预料到了,它应该有个unapply()
,根据“:
”分割输入,返回一个元组,包含股票代码和价格。然而,这里还有个catch
,因为输入可能并不遵循“SYMBOL:PRICE”的格式。为了处理这种可能性,这个方法的返回值应该是Option[(String, Double)]
。在运行时,我们得到的要么是Some(String, Double)
,要么是None
①。下面就是提取器ReceiveStockPrice的代码:
①参见5.4节,“Option类型”,了解有关于
Option[T]
、Some[T]
和None
的内容。PatternMatching/Extractor.scala
object ReceiveStockPrice {
def unapply(input: String) : Option[(String, Double)] = {
try {
if (input contains ":"){
val splitQuote = input split ":"
Some(splitQuote(0), splitQuote(1).toDouble)
}
else {
None
}
}
catch {
case _ : NumberFormatException => None
}
}
}
下面是如何使用更新过的服务:
PatternMatching/Extractor.scala
StockService process "GOOG"
StockService process "GOOG:310.84"
StockService process "GOOG:BUY"
StockService process "ERR:12.21"
上面代码的输出如下:
Look up price for valid symbol GOOG
Received price 310.840000 for symbol GOOG
Invalid input GOOG:BUY
Received price 12.210000 for symbol ERR
对于前三个请求,这段代码都做了很好的处理。接收了有效的参数,拒绝了无效的参数。不过,最后一个请求处理得并不好。它应该拒绝掉无效的股票代码ERR
,即便输入的格式是有效的。有两种方式处理这种情况。一是在ReceiveStockPrice
里检查股票代码是否有效,不过这会导致重复的工作。另外,还可以在一个case
语句里应用多个模式。我们修改process()
方法来做到这一点:
case ReceiveStockPrice(symbol @ Symbol(), price) =>
printf("Received price %f for symbol %s\n", price, symbol)
这里会先应用ReceiveStockPrice
提取器,成功的话,会返回一对结果。对第一个结果(symbol
)进一步应用Symbol
提取器校验这个股票代码。我们可以使用后面跟着@
符号的模式变量,在symbol
从两个提取器之间传递的过程中把它拦截住,如上面代码所示。
现在,如果重新运行这个修改过的服务,会得到如下输出:
Look up price for valid symbol GOOG
Received price 310.840000 for symbol GOOG
Invalid input GOOG:BUY
Invalid input ERR:12.21
至此,我们已经见识到了提取器是多么的强大。用它可以匹配任意的模式。通过unapply()
方法几乎可以完全控制匹配,按照期望返回匹配的部分。虽然这种“绝对权力”非常有用,但如果可以用正则表达式替代模式,就不必非要单独创建一个单实例的提取器对象了。下面就会看到如何使用正则表达式。