9.7 使用case类进行模式匹配

case类是一种特殊的类,用于case表达式的模式匹配。假定我们要接收和处理股票交易信息。买卖消息通常会带有一些信息,诸如股票名称、数量。把这些信息存到对象里会很方便,但是如何对他们进行模式匹配呢?这就是case类的目的所在了。它们是模式匹配器(pattern matcher)可以识别和匹配的类。下面有几个case类的例子:

PatternMatching/TradeProcessor.scala

  1. abstract case class Trade()
  2. case class Sell(stockSymbol: String, quantity: Int) extends Trade
  3. case class Buy(stockSymbol: String, quantity: Int) extends Trade
  4. case class Hedge(stockSymbol: String, quantity: Int) extends Trade

这里将Trade定义为abstract,因为不需要它的实例。从它继承出SellBuyHedge。这三个都以股票代码和数量作为参数。

现在,在case语句里使用这些类,如下:

PatternMatching/TradeProcessor.scala

  1. class TradeProcessor {
  2. def processTransaction(request : Trade) {
  3. request match {
  4. case Sell(stock, 1000) => println("Selling 1000-units of " + stock)
  5. case Sell(stock, quantity) =>
  6. printf("Selling %d units of %s\n", quantity, stock)
  7. case Buy(stock, quantity) if (quantity > 2000) =>
  8. printf("Buying %d (large) units of %s\n", quantity, stock)
  9. case Buy(stock, quantity) =>
  10. printf("Buying %d units of %s\n", quantity, stock)
  11. }
  12. }
  13. }

这里,用request匹配SellBuy。如果接收到的股票代码和数量得到匹配,就会分别存储到模式变量stockquantity里。这里还用到特定常量值(比如quantity1000)或是卫述句(比如检查if quantity > 2000)进行匹配。下面是使用TradeProcessor类的例子:

PatternMatching/TradeStock.scala

  1. val tradeProcessor = new TradeProcessor
  2. tradeProcessor.processTransaction(Sell("GOOG", 500))
  3. tradeProcessor.processTransaction(Buy("GOOG", 700))
  4. tradeProcessor.processTransaction(Sell("GOOG", 1000))
  5. tradeProcessor.processTransaction(Buy("GOOG", 3000))

上面代码的输出如下:

  1. Selling 500 units of GOOG
  2. Buying 700 units of GOOG
  3. Selling 1000-units of GOOG
  4. Buying 3000 (large) units of GOOG

processTransaction()并没有匹配Trades所有可能的类型,这里跳过了Hedge。在运行时,如果接收到Hedge,就会带来问题。不过,Scala并不知道有多少case类继承Trade。毕竟,在其他文件里也是可以继承case类的。然而,如果我们告诉了Scala:“除了这个文件中已有的类之外,不会再有更多的类出现”,Scala就可以帮助我们解决这个问题。要做到这一点,可以使用一个不常见的组合sealed abstract,如下所示:

  1. sealed abstract case class Trade()
  2. case class Sell(stockSymbol: String, quantity: Int) extends Trade
  3. case class Buy(stockSymbol: String, quantity: Int) extends Trade
  4. case class Hedge(stockSymbol: String, quantity: Int) extends Trade

现在,如果编译TradeProcessor类,Scala编译器会发出严重警告“warning: match is not exhaustive!”添加一个Hedgecase就可以修复这个警告。在上面的代码里,所有具体的case类都接收了参数。如果有个没有参数的case类,记得用的时候要放个括号(参见附录A)。下面的例子就有个没有任何参数的case类:

PatternMatching/ThingsAcceptor.scala

  1. import scala.actors._
  2. import Actor._
  3. case class Apple()
  4. case class Orange()
  5. case class Book ()
  6. class ThingsAcceptor {
  7. def acceptStuff(thing: Any) {
  8. thing match {
  9. case Apple() => println("Thanks for the Apple")
  10. case Orange() => println("Thanks for the Orange")
  11. case Book() => println("Thanks for the Book")
  12. case _ => println("Excuse me, why did you send me a " + thing)
  13. }
  14. }
  15. }

下面的代码里,有一个调用,忘了在Apple后面加括号:

PatternMatching/UseThingsAcceptor.scala

  1. val acceptor = new ThingsAcceptor
  2. acceptor.acceptStuff(Apple())
  3. acceptor.acceptStuff(Book())
  4. acceptor.acceptStuff(Apple)

上面调用的结果如下:

  1. Thanks for the Apple
  2. Thanks for the Book
  3. Excuse me, why did you send me a <function>

一旦忘记了括号,传的就不是case类的实例,而是它的伴生对象。这个伴生对象混入了scala.Function0 trait,这意味着它可以当作函数用。所以,最终传入的是一个函数,而不是case类的实例。如果acceptStuff()方法接收的是名为Thingcase类的实例,这不会有问题。不过,在actor间传递消息时,我们无法以类型安全的方式在编译时控制发送给actor的东西。因此,传递case类时,请小心一点。

虽然随着Scala编译器的逐步更新,可能会修复上面的问题,但这种边缘情况依然会出现。这也充分说明对代码进行测试的重要性,即便是对静态类型语言也是一样。(参见第12章“Scala单元测试”。)