Scala编程(第15章:样例类和模式匹配)

1.一个简单的例子:假定你需要编写一个操作算术表达式。

样例类

abstract class Expr
case class Var(name:String) extends Expr
case class Number(num:Double) extends Expr
case class UnOp(operator:String,arg:Expr) extends Expr
case class BinOp(operator:String,left: Expr,right: Expr) extends Expr

每个子类都有一个case修饰符,带有这种修饰符的类称作样例类。用上这个修饰符会让Scala编译器对我们的类添加一些语法上的便利。首先,它会添加一个跟类同名的工厂方法

scala> val v=Var("x")
v:Var = Var(x)

我们可以用Var("x")来创建对象而不是new Var("x")。当需要嵌套定义时,工厂方法尤为有用。没有到处落满new关键字,可以:

scala> val op=BinOp("+",Number(1),v)
op:BinOp=BinOp(+,Number(1.0),Var(x))

第二个语法上的便利是参数列表中的参数都隐式获得了一个val前缀,因此它们会被当作字段处理:

scala> v.name
res0: String=x
scala> op.left
res1: Expr=Number(1.0)

再次,编译器会帮我们自然的方式实现toString、hashCode和equals方法。分别打印、哈希、比较包含类及所有入参的整棵树。由于Scala的==总是代理给equals方法,这意味着以样例类表示的元素总是以结构化的方式做比较:

scala> println(op)
BinOp(+,Number(1.0),Var("x"))
scala> op.right == Var("x")
res3: Boolean = true

最后,编译器还会添加一个copy方法用于制作修改过的拷贝。这个方法可以用于制作除了一两个属性不同之外其余完全相同的该类的新实例。这个方法用到了带名字的参数和缺省参数。我们用带名字的参数给出想要做的修改。对于任何你没有给出的参数,都会用老对象中的原值。如:

scala> op.copy(operator ="-")
res4: BinOp = BinOp(-,Number(1.0),Var(x))

所有这些带来的是大量的便利(代价却很小)。你需要多写一个case修饰符,并且你的类和对象会变得大那么一点,因为生产了额外的方法,而且对于构造方法的每个参数都隐式地添加了字段。不过,样例类最大的好处是它们支持模式匹配

模式匹配:用模式匹配的话,这些规则可以被看成是一个Scala编写的简化函数的核心逻辑,我们可以这样来使用这个simplifyTop函数:

def simplifyTop(expr: Expr)=expr match {
   case UnOp("-",UnOp("-",e))=>e
   case BinOp("+",e,Number(0))=>e
   case BinOp("*",e,Number(1))=>e
   case _=>expr
}
scala> simplifyTop(UnOp("-",UnOp("-",Var("x"))))
res4:Expr = Var(x)

simplifyTop的右边由一个match表达式组成。match表达式对应Java的switch,不过match关键字出现在选择器的后面:

选择器 match {可选分支}

而不是:

switch (选择器) {可选分支}

类似“+”和1这样的常量模式可以匹配那些按照==的要求跟它们相等的值。而像e这样的变量模式可以匹配任何值。匹配后,在右侧的表达式中,这个变量将指向这个匹配的值。通配模式,即_也匹配任何值,不过它并不会引入一个变量名来指向这个值。

构造方法模式看上去就像UnOp("-",e)。这个模式匹配所有类型weiUnOp且首个入参匹配“-”而第二个入参匹配e的值。注意构造方法的入参本身也是模式。这允许我们用精简的表示法来编写有深度的模式:

UnOp("-",UnOp("-",e))

对比match和switch:match表达式可以被看作Java风格的switch的广义化。Java风格的switch可以很自然地用match表达式表达。不过,我们需要记住三个区别:

  1. Scala的match是一个表达式(也就是说它总是能得到一个值)
  2. Scala的可选分支不会贯穿到下一个case
  3. 如果没有一个模式匹配上,会抛出名为MatchError的异常。这意味着你需要确保所有的case被覆盖到,哪怕这意味着你需要添加一个什么都不做的缺省case。

 

2.模式的种类:由于模式的语法是透明的,我们只需关心能使用哪几种模式就对了。

通配模式:通配模式还可以用来忽略某个对象中你并不关心的局部:

expr match{
  case e @ BinOp(_,_,_)=>println(e+" is a binary operation")
  case _ =>println("It's something else")
}

常量模式:常量模式仅匹配自己。任何字面量都可以作为常量(模式)使用。同时,任何val或单例对象也可以被当作常量(模式)使用:

def describe(x:Any)=x match {
   case 5=>"five"
   case true=>"truth"
   case "hello"=>"hi!"
   case Nil=>"the empty list"
   case _=>"something else"
}
describe(List)
//打印:the empty list

变量模式:变量模式匹配任何对象,这一点跟通配模式相同。不过不同于通配模式的是,Scala将对应的变量绑定成匹配的对象。在绑定之后,你就可以用这个变量来对对象做进一步的处理:

val x=true
x match {
  case 0=>"zero"
  case something=>"not zero: "+something
}
//打印:not zero: true

常量模式也可以有符号形式的名称。如:Nil,Pi:

val x=true
x match {
  case Pi=>"strange math Pi = "+Pi
  case _=>"OK"
}
//返回:OK

Scala编译器是如何知道Pi是从scala.math包引入的常量而不是一个代表选择器本身的变量呢?Scala采用了一种简单的词法规则来区分:一个以小写字母打头的简单名称会被当作模式变量处理;所有其他引用都是常量。如:

val x=true
x match {
  case pi=>"strange math Pi = "+pi
  case _=>"OK"
}
//返回:strange math Pi = true

在这里编译器会警告,由于pi是变量模式,它将会匹配所有输入,因此不可能走到后面的case。如果需要,仍然可以用小写的名称来作为模式常量。首先,如果常量是某个对象的字段,可以在字段名前面加上限定词。如,虽然pi是个变量模式,但this.pi或obj.pi是常量(模式)。还可以用反引号将这个名称包起来

val x=true
x match {
  case `pi`=>"strange math Pi = "+pi
  case _=>"OK"
}
//返回:OK

你还看过反引号可以用来将关键字当作普通的标识符,比如Thread.`yield`()这段代码将yield当作标识符而不是关键字。

构造方法模式:一个构造方法模式看上去像这样:“BinOp("+",e,Number(0))”。它由一个名称(BinOp)和一组圆括号中的模式:"+"、e和Number(0)组成。假定这里的名称指定的是一个样例类,这样的一个模式将首先检查被匹配的对象是否是以这个名称命名的样例类实例,然后再检查这个对象的构造方法参数是否匹配这些额外给出的模式。这些额外的模式意味着Scala的模式支持深度匹配。如:

x match {
  case BinOp("+",e,Number(0))=>println("a deep match")
  case _=>
}

序列模式:与样例类匹配一样,也可以跟序列类型做匹配,比如List或Array。使用的语法是相同的:

val x=List(0,1,1)
x match {
  case List(0,_,1)=>println("found it")
  case _=>
}
//打印:found it

如果你想匹配一个序列,但又不想给出多长,你可以用_*作为模式的最后一个元素:

val x=List(0,1,2)
x match {
  case List(0,_*)=>println("found it")
  case _=>
}
//打印:found it

元组模式:我们还可以匹配元组:

val x=(0,1,1)
x match {
  case (a,b,c)=>println("matched "+a+b+c)
  case _=>
}
//打印:matched 011

带类型的模式:可以用带类型的模式来替代类型测试和类型转换:

val x=Map(1->'a',2->'b')
x match {
  case s:String=>s.length
  case m:Map[_,_]=>m.size
  case _=>-1
}
//返回:2
val x="abc"
//返回: 3

另一个跟用带类型的模式匹配等效但是更冗长的方式是做类型测试然后(强制)类型转换。对于类型测试和转换,Scala跟Java的语法不太一样。如要测试某个表达式expr的类型是否为String:

expr.isInstanceOf[String]

要将这个表达式转换成String类型,我们需要用:

expr.asInstanceOf[String]

可以这样来重写上面的match表达式:

if (x.isInstanceOf[String]){
  val s=x.asInstanceOf[String]
  s.length
} else ...

这样显得很啰嗦,我们是有意为之,因为这并不是一个值得鼓励的做法。通常,使用带类型的模式可以把类型测试和类型转换在单个模式匹配中完成。类型擦除:除了笼统的映射,我们还能测试特定元素类型的映射吗?这对于测试某个值是否是Int到Int的映射这类场景会很方便。如:

val x=Map(1->3,2->4)
x match {
  case m:Map[Int,Int]=>true
  case _=>false
}
//返回:true

编译器会给出警告,有时还会报错。Scala采用了擦除式的泛型,就跟Java一样。这意味着在运行时并不会保留类型参数的信息。这样一来,我们就无法判断给定的某个Map或List对象是什么类型参数创建的。如:

val x=Map("1"->"one","2"->"two")
//返回:true

对于这个擦除规则唯一的例外是数组,因为Java和Scala都对它们做了特殊处理。数组的元素类型是跟数组一起保存的,因此我们可以对它进行模式匹配:

val x=Array(1,2,3)
x match {
  case a:Array[String]=>"???"
  case a:Array[Int]=>"yes"
  case _=>"no"
}
//返回:yes

变量绑定:除了独自存在的变量模式外,我们还可以对任何其他模式添加变量。只需写下变量名、一个@符合模式本身,就得到一个变量绑定模式。这个模式将跟平常一样执行模式匹配,如果匹配成功,就将匹配的对象赋值给这个变量,就像见到的变量模式一样:

val x=BinOp("+",Number(1),Number(0))
x match {
  case BinOp("+",e @ Number(_),Number(0))=>e
  case _=>x
}
//返回: Number(1)

匹配成功后,BinOp("+",Number(_),Number(0))的部分就被赋值给变量e。

 

3.模式守卫:有时候语法级的模式匹配不够精准。如,我们要公式化一个简化规则,即用乘以(e*2)来替换对两个相同操作元的加法(e+e)。在表示Expr树的语言中,下面这样的表达式:

BinOp("+",Var("x"),Var("x"))

应用该简化规则后将得到:

BinOp("*",Var("x"),Number(2))

你可能会这样定义这个规则:

val x=BinOp("+",Var("x"),Var("x"))
x match {
  case BinOp("+",e,e)=>BinOp("*",e,Number(2))
  case _=>x
}
//编译错误

这样会报错,因为Scala要求模式都是线性的:同一个模式变量在模式中只能出现一次。不过我们可以用一个模式守卫来重新定义:

val x=BinOp("+",Var("x"),Var("x"))
x match {
  case BinOp("+",e,y) if e==y =>BinOp("*",e,Number(2))
  case _=>x
}
//返回:BinOp("+",Var("x"),Number(2))

模式守卫出现在模式之后,并以if打头。模式守卫可以是任意的布尔表达式,通常会引用到模式中的变量。仅在模式守卫得到true时才会成功。

 

4.模式重叠:模式会按照代码中的顺序逐个被尝试。捕获所有的变量模式通配模式出现在更具体的简化规则之后,这是很重要的。如果将顺序颠倒,那么捕获所有的case就会优先于更具体的规则执行。在许多场景下,编译器甚至会拒绝编译:

val x=UnOp("+",Var("x"))
x match {
  case e @ UnOp(_,_)=>e
  case UnOp("-",e @ UnOp("-",_))=>e
  case _=>x
}
//返回:UnOp("+",Var("x"))

编译器会警告,因为第二条case语句永远也不会被匹配。

 

5.密封类:我们可以将样例类的超类标记为密封(sealed)的。密封类除了在同一个文件中定义的子类之外,不能添加新的子类。这对于模式匹配非常有用,这样我们就只需关心那些已知的样例类:

sealed abstract class Expr

只需要在类继承关系的顶部那个类的类名前面加上sealed关键字。这也是为什么sealed关键字通常被看作模式匹配的执照的原因。不过,有时候你也会遇到编译器过于挑剔的情况。如,你可以从上下文知道只会有Number或Var,因此你很清楚不会有MatchError发生,这时你可以添加一个捕获所有的case。你可能并不会很乐意,因为你被迫添加了永远不会被执行的代码,而只是想让编译器闭嘴。我们可以这样添加一个@unchecked注解:

def describe(e:Expr)=(e: @unchecked) match {
  case Number(_)=>"a nnumber"
  case Var(_)=>"a variable"
}

可以像添加类型声明那样对表达式添加注解:在表达式后加一个冒号和注解的名称(以@打头)。如果match表达式的选择器带上了这个注解,那么编译器对后续模式分支的覆盖完整性检查就会被压制。

 

6.Option类型:Scala由一个名为Option的标准类型来表示可选值。这样的值可以有两种形式:Some(x),其中x是那个实际的值;或者None对象,代表没有值。Scala集合类的某些标准操作会返回可选值。比如,Scala的Map有一个get方法,当传入的键有对应的值时,返回Some(value);而当传入的键在Map中没有定义的时,返回None:

val capitals=Map("France"->"Paris","Japan"->"Tokyo")
println(capitals get "France")
println(capitals get "North Pole")
//打印:
//Some(Paris)
//None

将可选值解开最常见的方式是通过模式匹配:

def show(x:Option[String])=x match {
  case Some(s)=>s
  case None=>"?"
}
show(capitals get "France")
//返回:Paris

 

7.到处都是模式:Scala很多地方都允许使用模式,并不仅仅是match表达式。

变量定义中的模式:每当我们定义一个val或var,都可以用模式而不是简单的标识符。如,可以将一个元组解开并将其中的每个元素分别赋值给不同的变量:

val myTuple=(123,"abc")
val (number,string)=myTuple
println(number+"\t"+string)
//打印:123  abc

这个语法结构在处理样例类时非常有用。如果你知道要处理的样例类是什么,就可以用一个模式来解析它:

val upOp: UnOp = UnOp("-",UnOp("-",Var("x")))
val UnOp(a,UnOp(b,Var(c)))=upOp
println(a+"\t"+b+"\t"+c)
//打印:-  -  x

作为偏函数的case序列:用花括号包起来的一系列case(即可选分支)可以用在任何允许出现函数字面量的地方。本质上讲,case序列就是一个函数字面量,只是更加通用:

val withDefault:Option[Int]=>Int={
  case Some(x)=>x
  case None=>0
}
println(withDefault(Some(10)))
//打印:10

通过case序列得到的是一个偏函数。如果我们将这样一个函数应用到它不支持的值上,它会产生一个运行时异常:

val second:List[Int]=>Int={
  case x::y::_=>y
}
println(second(List(1,2,3)))
//打印:2

在编译时,编译器会发出警告,我们的匹配并不全面。如果传入的列表元素个数大于1个,因为取的是前两个值,不会报错;当传入的列表元素个数为一个或者为空列表,就会报MatchError错误。解决的办法除了给出通配模式,还可以用偏函数的类型声明:

val second:PartialFunction[List[Int],Int]={
  case x :: y :: _=>y
}
println(second(Nil))

注意:此处Scala编程书上代码测试未通过

for表达式中的模式:我们还可以for表达式中使用模式:

for ((country,city)<-capitals){
  println(country+"\t"+city)
}
//打印:
//France   Paris
//Japan	  Tokyo

不过某个模式不能匹配某个生成的值的情况也同样存在:

val results=List(Some("apple"),None,Some("orange"),"???")
for (Some(fruit)<-results){
  println(fruit)
}
//打印:
//apple
//orange

我们从这个例子当中可以看到,生成的值当中那些不能匹配给定模式的值会被直接抛弃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值