9.8 使用提取器进行匹配

使用Scala提取器(Extractor),可以将模式匹配带到下一个阶段:匹配任意模式。正如其名字所示,提取器会从输入中提取出匹配的部分。假定我们在写一个服务,处理股票相关的输入。对我们来说,手头的第一个工作就是接收股票代码,返回这个股票的价格(为了演示,这里打印出结果)。如下例所示:

  1. StockService process "GOOG"
  2. StockService process "IBM"
  3. StockService process "ERR"

process()方法需要校验给定的代码是否有效,如果有效,则返回其股价。代码如下:

  1. object StockService {
  2. def process(input : String) {
  3. input match {
  4. case Symbol() => println("Look up price for valid symbol " + input)
  5. case _ => println("Invalid input " + input)
  6. }
  7. }
  8. }

process()方法使用了尚未定义的提取器Symbol执行模式匹配。如果提取器确定股票代码有效,就会返回true,否则,返回false。如果返回true,会执行同case关联的表达式。否则,模式匹配继续下一个case。一起来看看提取器示例:

  1. object Symbol {
  2. def unapply(symbol : String) : Boolean = symbol == "GOOG" || symbol == "IBM"
  3. // you'd look up database above... here only GOOG and IBM are recognized
  4. }

提取器有个方法叫unapply(),接收要匹配的值。执行case Symbol() => …时,match表达式会自动把input作为参数传入unapply()。执行上面的3段代码,(记得把调用服务的那个样例放到文件底部。)会得到如下结果:

  1. Look up price for valid symbol GOOG
  2. Look up price for valid symbol IBM
  3. Invalid input ERR

或许,你会觉得用unapply()做方法名太诡异了。你可能认为,提取器应该有个类似于evaluate()之类的方法。用unapply这个名字的原因是,提取器有个可选的apply()方法。apply()unapply()这两个方法会执行相反的动作。unapply()将对象分解为用以匹配模式的片段,而apply()则是为了提供一个把它们组合回去的选择。

现在,我们可以请求股票报价了,对于服务而言,下一个任务是设置股票价格。假设这个消息的格式是“SYMBOL:PRICE”。我们需要对这种格式进行模式匹配,然后采取行动。下面是修改过的process()方法,处理了这个附加的任务:

PatternMatching/Extractor.scala

  1. object StockService {
  2. def process(input : String) {
  3. input match {
  4. case Symbol() => println("Look up price for valid symbol " + input)
  5. case ReceiveStockPrice(symbol, price) =>
  6. printf("Received price %f for symbol %s\n", price, symbol)
  7. case _ => println("Invalid input " + input)
  8. }
  9. }
  10. }

这里添加了一个新的case,用到尚未编写的提取器ReceiveStockPrice。这个提取器不同于之前编写的Symbol提取器。后者只返回了一个boolean结果,而ReceiveStockPrice则需要解析输入,返回两个值,symbolprice。在case语句里,它们被指定成ReceiveStockPrice的实参;不过,它们并不是传入的实参,而是从提取器中传出的实参。所以,symbolprice并不是用来传递值,而是用来接收值的。

我们看一下ReceiveStockPrice提取器。你可能已经预料到了,它应该有个unapply(),根据“:”分割输入,返回一个元组,包含股票代码和价格。然而,这里还有个catch,因为输入可能并不遵循“SYMBOL:PRICE”的格式。为了处理这种可能性,这个方法的返回值应该是Option[(String, Double)]。在运行时,我们得到的要么是Some(String, Double),要么是None①。下面就是提取器ReceiveStockPrice的代码:

①参见5.4节,“Option类型”,了解有关于Option[T]Some[T]None的内容。

PatternMatching/Extractor.scala

  1. object ReceiveStockPrice {
  2. def unapply(input: String) : Option[(String, Double)] = {
  3. try {
  4. if (input contains ":"){
  5. val splitQuote = input split ":"
  6. Some(splitQuote(0), splitQuote(1).toDouble)
  7. }
  8. else {
  9. None
  10. }
  11. }
  12. catch {
  13. case _ : NumberFormatException => None
  14. }
  15. }
  16. }

下面是如何使用更新过的服务:

PatternMatching/Extractor.scala

  1. StockService process "GOOG"
  2. StockService process "GOOG:310.84"
  3. StockService process "GOOG:BUY"
  4. StockService process "ERR:12.21"

上面代码的输出如下:

  1. Look up price for valid symbol GOOG
  2. Received price 310.840000 for symbol GOOG
  3. Invalid input GOOG:BUY
  4. Received price 12.210000 for symbol ERR

对于前三个请求,这段代码都做了很好的处理。接收了有效的参数,拒绝了无效的参数。不过,最后一个请求处理得并不好。它应该拒绝掉无效的股票代码ERR,即便输入的格式是有效的。有两种方式处理这种情况。一是在ReceiveStockPrice里检查股票代码是否有效,不过这会导致重复的工作。另外,还可以在一个case语句里应用多个模式。我们修改process()方法来做到这一点:

  1. case ReceiveStockPrice(symbol @ Symbol(), price) =>
  2. printf("Received price %f for symbol %s\n", price, symbol)

这里会先应用ReceiveStockPrice提取器,成功的话,会返回一对结果。对第一个结果(symbol)进一步应用Symbol提取器校验这个股票代码。我们可以使用后面跟着@符号的模式变量,在symbol从两个提取器之间传递的过程中把它拦截住,如上面代码所示。

现在,如果重新运行这个修改过的服务,会得到如下输出:

  1. Look up price for valid symbol GOOG
  2. Received price 310.840000 for symbol GOOG
  3. Invalid input GOOG:BUY
  4. Invalid input ERR:12.21

至此,我们已经见识到了提取器是多么的强大。用它可以匹配任意的模式。通过unapply()方法几乎可以完全控制匹配,按照期望返回匹配的部分。虽然这种“绝对权力”非常有用,但如果可以用正则表达式替代模式,就不必非要单独创建一个单实例的提取器对象了。下面就会看到如何使用正则表达式。