样例类是Scala
对对象进行模式匹配而不需要大量样板代码的方式,对希望做模式匹配的类加上关键字case
即可。在Scala
中如果类的定义体是空时,可以省去定义体的花括号。
样例类
-
case class Var(String: x)
带有case
关键字的类称为样例类,生成了一个伴生对象,在这个对象中存在接收一个参数的apply
工厂方法。可以使用Var(x)
而不是new Var(x)
这样繁琐的语句来构建新的对象;参数列表中的参数都是有隐式的val
从而称为类字段,编译器以自然的方式实现toString,hashCode
和equals
方法,编译器会添加一个copy
用于拷贝,使用到了带名参数和缺省参数。样例类的最主要贡献还是可用于模式匹配。
模式匹配
- 模式匹配包含了一系列以
case
关键字打头的可选分支,每一个分支都包含一个模式,如果模式匹配了,模式对应的表达式就会被求值。一个match
表达式的求值过程是按照模式给出的顺序逐一尝试的。
- 模式匹配包含了一系列以
- 常量模式匹配常量,变量模式可以匹配任何值,
_
表示通用模式,并不会引入一个变量名来指向这个值。
- 常量模式匹配常量,变量模式可以匹配任何值,
- 还有就是构造者模式,相对于
if
和else
的判断,非常非常的方便。case _ =>
返回的是Unit
类型的()
值。
- 还有就是构造者模式,相对于
模式的种类
- 通配模式:
_
用于任何对象,还可以用来忽略某个对象中并不关心的局部,在模式中使用名字,其实是将等效的对象或者变量绑定在这个名字上。
- 通配模式:
- 常量模式:仅能匹配自己。任何字面量都可以作为常量模式来使用。单例对象也可以被当做常量模式使用。
val
对象也可以被当做常量模式使用。
- 常量模式:仅能匹配自己。任何字面量都可以作为常量模式来使用。单例对象也可以被当做常量模式使用。
-
- 变量模式:变量模式可以匹配任何对象,
scala
将匹配上的对象绑定为对应的变量。绑定之后,可以利用这个变量来对对象做进一步的处理。
- 注意:常量也可以有符号,比如
Pi
表示3.1415926
。在模式中通过标识符的首个字母来表示名称是常量还是变量,所有以大写字母打头的标识符对应的模式都是常量模式。如果必须使用小写字母来表示常量,则需要使用反引号`pi`
,在这种情况下,编译器认为pi
是一个常量而不是变量,或者使用限定词,比如obj.pi
等。`
在Scala
中有两种用途,第一是将首字母为小写字母的标识符作为常量模式使用,二是将关键字当做普通的标识符,比如`Thread.`yield`
。
- 变量模式:变量模式可以匹配任何对象,
- 构造方法模式:构造方法模式是真正体现出模式匹配威力的地方。首先检测被匹配的对象是否是该样例类的实例,然后检查该对象的构造方法参数是否匹配模式中的构造参数。构造参数可能也是构造模式,匹配时也对他们进行检测,因此匹配可以达到任意的深度。
- 序列模式:类似于样例类匹配,可以跟序列类型做匹配。比如
List
或者Array
,使用的语法是相同的,但可以在模式中给出任意数量的元素,因为List
和Array
中含有接收任意参数的apply
工厂构造方法。在该模式中使用_*
表示任意多个元素,如果要使用的话,必须放在构造列表的最后面。
- 序列模式:类似于样例类匹配,可以跟序列类型做匹配。比如
- 元组模式,
(a,b,c)
可以匹配任意的三元组。
- 元组模式,
-
- 带类型的模式:来代替类型检测和类型转换
case s: String => s.length
case m: Map[_,_] => m.size
- 和带类型的模式匹配但等效的方法是使用类型检测并进行强制转换。在
Scala
中类型检测为expr.isInstanceOf[String]
,expr.asInstanceOf[String]
。 - 这其中涉及到的一个问题是类型擦除,
Scala
和Java
一样,采用了擦除式的泛型,在运行时并不会保留类型参数的信息,所以不能使用类似的语句来判断,case m: Map[Int,Int] => m.size
,编译器只能判断某个变量是不是Map
,而不能判断这个Map
中key
和value
的类型。唯一例外的是数组,可以使用Array[String]
这样的来进行验证,数组的元素类型被保存了。(where???)
- 带类型的模式:来代替类型检测和类型转换
- 变量绑定,除了各自存在的变量模式外,还可以对任何其他模式添加变量,在模式之前添加变量名以及一个
@
符号,即可得到。case UnOp("abs",e@Unop("abs",_)) => e
,出现两次求绝对值的操作,只需要计算一次即可。
模式守卫
-
Scala
要求模式都是线性的:同一个模式变量在模式中只能出现一次。但是可以出现在多个case
语句中。比如
def simplifyAdd(e: Expr) = e match { case BinOp("+", x, x) => BinOp("*", x, Number(2)) case _ => e }
是不被允许的。使用模式守卫,以if
打头。模式守卫可以是任意的布尔表达式,一般会引用到模式中的变量,如果存在模式守卫,这个匹配仅在模式守卫为true
的时候才会成功。
def simplifyAdd(e: Expr) = e match { case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2)) case _ => e }
模式重叠
- 模式会按照顺序进行逐个尝试,所以在
match
中,case
的顺序是很重要的。
密封类
- 当编写模式匹配的时候,需要确保完整地覆盖了所有可能的
case
。为了使得Scala
编译器能够帮助我们检测是否穷举了所有的情况,需要阻止子类的不可控再增加。手段是将这些样例类的超类标记为sealed
,密封的。密封类除了在同一个文件中定义子类之外,不能添加新的子类。对于模式匹配非常有用,可以获得更好的编译器支持。如果类被打算用于样例类,一般会在类继承关系的顶部类前加上sealed
关键字。
- 当编写模式匹配的时候,需要确保完整地覆盖了所有可能的
- 如果在
match
中可以明确确定只处理某些情况,对缺失不检查的情况可以使用@unchecked
注解。在match
的选择器部分进行添加(e: @unchecked) match {}
,对分支的覆盖完整性会被压制。
- 如果在
Option类型
-
Option
表示可选值。将可选值解开的最常见方式是通过模式匹配,利用构造器模式。可以轻易的避免由null
带来的空指针异常问题。
Pattern is everywhere
- 在
Scala
中很多地方都可以使用模式。利用可以使用元组模式val (a, b) = (123, 234)
,在处理样例类的时候非常有用,如果知道要处理的样例类是什么,可以使用一个模式来析构它。
- 在
- 作为偏函数的
case
序列。用花括号包起来的一系列case
可以用在任何允许出现函数字面量的地方。本质上将,case
序列就是一个函数字面量,只不过是有多个入口,每个入口都有自己的参数列表。每个case
对应该函数的一个入口,该入口的参数列表使用模式来指定。每个入口的逻辑主体是case
右边的部分。通过case
序列得到的是一个偏函数。偏函数如果应用在它不支持的值上(虽然该参数的类型是正确的),会发生运行时异常。
- 作为偏函数的
val second: List[Int] => Int = { case x :: y :: _ => y }
- 如果要检查偏函数对某个入参是否与定义,必须首先声明处理的是偏函数。
List[Int] => Int
表示的是所有从整数列表到正数的函数,定义偏函数需要使用到
val second: PartialFunction[List[Int],Int] = { case x :: y :: _ => y }
PartialFunction
中定义了一个isDefinedAt
方法,可以用来检查该函数对某个特定的数值是否有定义。偏函数的典型应用是模式匹配函数字面量(case
序列),这个模式匹配会出现在PartialFunction
定义中的两处,一处用于apply
,一处用于isDefinedAt
。在默认没有声明的情况下,函数字面量对应的就是全函数。
-
for
表达式中的模式
在for
表达式中也可以使用元组表达式,构造器表达式等等。如果在for
表达式中迭代器代表的值没有匹配到模式,则该值会被直接丢弃。
-
tips
-
a -> 1
只是Tuple2(a, 1)
的一个语法糖,->
是一个方法调用,在Predef
中有隐式类ArrowAssoc
对值a
进行隐式转换,可以调用->
方法。
网友评论