本章主要内容:
1、样本类
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编译器自动为我们的类添加一些句法上的便捷设定:
- 它会添加与类名一致的工厂方法,比如说,
Var("x")
用来代替new Var("x")
,尤其我们把工厂方法嵌套在一起的时候,这种方法极为简便。比如:val op=BinOp("+",Number(1),v)
。 - 样本类参数列表中的所有参数隐式获得val前缀,因为它被当成字段维护。
- 编译器自动为我们的样本类添加了方法
toString
、hashCode
和equal
的自然实现。它们能够打印,哈希和比较由类及其所有参数组成的整颗树。因为Scala的"=="
直接转到equals。
样板类所有的这些转换以极低的代价带来了大量的便利。代价就是必须写case
修饰符并且你的类和对象会变得稍微大一点。变大的原因是产生了附加的方法和对每个构造器参数添加了隐含的字段。不过,样本类最大的好处在于它们能够支持模式匹配。
2、模式匹配
2.1 基本形式
def simplifyTop(expr:Expr):Expr = expr match{
case UnOp("-",UnOp("-",e)) => e //双重负号
case BinOp("+",e,Number(0)) => e //加0
case BinOp("*",Number(1)) => e. //乘1
case _=>expr
}
simplifyTop
右侧的部分组成了match
表达式。match
代替了Java中的switch
,不过它写在expr
选择器后面。case
后面的是一系列备选项。
match和switch的比较:
- match是Scala的表达式,也就是它始终以值作为结果返回。
- Scala的备选项永远不会进入下一个
case
,即类似Java中匹配到后隐式break
。 - 如果没有模式匹配,会抛出
MatchError
。所以我们必须把备选项情况考虑周全或者给予默认值。case _=>
中返回unit
值"()"
也是被允许的。
2.2 模式的种类
通配模式:”_”匹配任意对象。
expr match{
case BinOp(_,_,_) => println(expo + "is a binary operation")
case _=>println("It's something else")
}
常量模式:
def describe(x:Any) = x match{
case 5 => "five"
case true => "truth"
case "hello" => "hi!"
case Nil => "the empty list"
case _=>"something else"
}
变量模式:
expr match{
case 0 => "zero"
//
case pi => "pi"
case somethingElse =>"not zero:"+somethingElse
}
Scala编译器使用了一个简单的文字规则:变量模式的选择器要小写字母开始,其他的引用被认为是常量。用单引号修饰的变量名会被解释成为常量。
Scala中的单引号有两处用法:
1. 修饰关键字,使其被当作标志符。
2. 修饰小写字母标志符当作常量匹配。
构造器模式:
它是由名称(BinOp)及若干括号内的模式:”+”、e、Number(0)。假如这个名称指定了一个样本类,那么这个模式就是表示首先检查对象是该名称的样本类的成员,然后检查对象的构造器参数是符合额外提供的模式的。这种额外的模式意味着Scala模式支持深度匹配。
expo match{
case BinOp("+",e,Number(0)) =>println("a deep match")
case _=>
}
序列模式:可以像匹配样本类那样匹配如List或Array这样的序列类型。
expo match{
case List(0,_,_) => println("found it")
case _=>
}
元组模式:可以匹配元组。
def tupleDemo(expr:Any){
expo match{
case (a,b,c) => println("matched"+a+b+c)
case _=>
}
}
类型模式:把类型模式当作类型测试和类型转换的简易替代。
def generalSize(x:Any) = x match{
case s:String => s.length
case m:Map[_,_] => m.size
case _=> 1
}
在Scala中的类型匹配语法如下,但Scala 里不鼓励这么做,使用带有类型模式的模式匹配通常就能满足我们的要求,尤其是在做类型测试和转换的场景。
但是Scala中存在类型擦除,它不能匹配特定元素类型的映射。比如Map[_,_]
可以匹配,Map[Int,Int]
却不行,因为类型擦出模式让类型没有保持到运行期,在运行期没有办法判断给定Map对象创建时带了两个Int
类型还是其他类型,做的只能是判断这个值是某种任意类型参数的Map
。
//类型匹配
x.isInstanceOf[String]
//类型转换
x.asInstanceOf[String]
变量绑定:
除了独立的变量模式之外,我们还可以对任何其他模式添加变量。只要简单地写上变量名和一个@
符号,以及这个模式。这种写法创造了变量绑定模式。这种模式的意义在于它能像通常那样做模式匹配,如果匹配成功了,则把变量设置成匹配的对象,就像使用简单的变量模式一样。比如下面的e作为变量及UnOp("abs",_)
作为模式的变量绑定模式,若匹配成功,那么符合UnOp("abs",_)
的部分就可以使用e
指代。
expr match{
case UpOp("abs",e @ UnOp("abs",_)) => e
case _=>
}
2.3 模式守卫
更为精准的匹配。
def simplifyAdd(e:Expr) = e match{
// 模式守卫
case BinOp("+",x,y) if x==y =>
BinOp("*",x,Number(2))
case _=> e
}
2.4 模式重叠
全匹配的样本要跟在更具体的简化方法后面。
2.5 封闭类
我们写好了模式匹配后,我们需要确定已经考虑了所有的情况。如果处理不当,会出现编译报错。我们可以通过3种方式处理:
- 让样本类的超类被封闭
(sealed)
。封闭类除了类定义所在文件之外不能添加任何新的子类。这对于模式匹配是非常有用的。因为这意味着我们只需要关心我们已经知道的字类即可。
sealed 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
- 默认不会发生
def describe(e:Expr):String = e match{
case Number(_) => "a number"
case Var(_) => "a variable"
case _ => throw new RuntimeException //不会发生
}
- 添加@unchecked注解
def describe(e:Expr):String = (e:@unchecked) match{
case Number(_) => "a number"
case Var(_) => "a variable"
}