原文发表于:http://nerd-is.in/2013-09/scala-learning-pattern-matching-and-case-classes/
Scala强大的模式匹配机制,可以应用在switch语句、类型检查以及“析构”等场合。样例类对模式匹配进行了优化。
更好的switch
1
2
3
4
5
6
7
8
|
var
sign
=
.
.
.
val
ch
:
Char
=
.
.
.
ch
match
{
case
'+'
=
>
sign
=
1
case
'-'
=
>
sign
=
-
1
case
_
=
>
sign
=
0
}
|
上面代码中,case _模式对应于switch语句中的default,能够捕获剩余的情况。如果没有模式能匹配,会抛出MatchError。而且不像常见的switch语句,在一种模式匹配之后,需要使用break来声明分支不会进入下一个分支。
match是表达式,不是语句,所以是有返回值的,故可将代码简化:
1
2
3
4
5
|
sign
=
ch
match
{
case
'+'
=
>
1
case
'-'
=
>
-
1
case
_
=
>
0
}
|
match表达式中可以使用任何类型。模式总是从上往下进行匹配。
守卫
看代码就好,与if表达式的守卫相同作用:
1
2
3
4
5
6
|
ch
match
{
case
'+'
=
>
sign
=
1
case
'-'
=
>
sign
=
-
1
case
_
if
Character
.
isDigit
(
ch
)
=
>
digit
=
Character
.
digit
(
ch
,
10
)
case
_
=
>
sign
=
0
}
|
模式中的变量
如果在case关键字后跟着一个变量名,那么匹配的表达式会被赋值给那个变量。case _是这个特性的一个特殊情况,变量名是_。
1
2
3
4
5
6
|
"Hello, world"
foreach
{
c
=
>
println
(
c
match
{
case
' '
=
>
"space"
case
ch
=
>
"Char: "
+
ch
}
)
}
|
经过我的尝试,在如果变量名是_,那么在=>后使用_是不行的。
在模式中使用变量可能会与常量冲突。
1
2
3
4
5
|
import
scala
.
math
.
_
x
match
{
case
Pi
=
>
.
.
.
.
.
.
}
|
在上面的代码中,要如何判断Pi这个标志符是一个用来匹配的常量还是模式中的变量?规则是:变量比需要以小写字母开始。如果有常量是小写字母开头的,那么需要用反引号将常量名包起来:
1
2
3
4
5
|
import
java
.
io
.
File
.
_
str
match
{
case
`
pathSeparator
`
=
>
.
.
.
.
.
.
}
|
类型模式
相比使用isInstanceOf来判断类型,使用模式匹配更好。
1
2
3
4
5
6
|
obj
match
{
case
x
:
Int
=
>
x
case
s
:
String
=
>
Integer
.
parseInt
(
s
)
case
_
:
BigInt
=
>
Int
.
MaxValue
case
_
=
>
0
}
|
在匹配类型时,需要使用一个变量名,否则就是使用对象本身来进行匹配了。
1
2
3
4
|
obj
match
{
case
_
:
BigInt
=
>
Int
.
MaxValue
// 匹配任何类型为BigInt的对象
case
BigInt
=
>
-
1
// 匹配类型为Class的BigInt对象
}
|
因为匹配是发生在运行期的,而且JVM中泛型的类型信息会被擦掉,因此不能使用类型来匹配特定的Map类型(大部分集合类型也都不可以吧):
1
2
|
case
m
:
Map
[
String
,
Int
]
=
>
.
.
.
// 不行
case
m
:
Map
[
_
,
_
]
=
>
.
.
.
// 匹配通用的Map,OK
|
但对于数组来说,类型信息是完好的,所以可以在Array上匹配。
匹配数组、列表和元组
1
2
3
4
5
6
|
arr
match
{
case
Array
(
0
)
=
>
"0"
// 匹配包含0的数组
case
Array
(
x
,
y
)
=
>
x
+
" " +
y
// 匹配任何带有两个元素的数组,并将元素绑定到x和y
case
Array
(
0
,
_
*
)
=
>
"0 ..."
// 匹配任何以0开始的数组
case
_
=
>
"something else"
}
|
下面的模式匹配,功能与上面的代码是一样的,不过将数组换成了列表。
1
2
3
4
5
6
|
lst
match
{
case
0
::
Nil
=
>
"0"
case
x
::
y
::
Nil
=
>
x
+
" "
+
y
case
0
::
tail
=
>
"0 ..."
case
_
=
>
"something else"
}
|
与上面两个例子差不多,模式匹配也可以使用在元组上。
注意到变量将会被绑定到这三种数据结构的不同部分上,这种操作被称为“析构”。
提取器
在上一节中,使用模式匹配来对数组、列表和元组进行了匹配,在这个过程的背后的是提取器(extractor)机制。使用unapply来提取固定数量的对象,使用unapplySeq来提取一个序列。
在前面的代码 case Array(0, x) => ...中, Array(0, x)部分实际上是使用了伴生对象中的提取器,实际调用形式是: Array.unapplySeq(arr)。根据Doc,提取器方法接受一个Array参数,返回一个Option。
正则表达式是另一个适用提取器的场景。正则有分组时,可以用提取器来匹配分组:
1
2
3
4
|
val
pattern
=
"([0-9]+) ([a-z]+)"
.
r
"99 bottles"
match
{
case
pattern
(
num
,
item
)
=
>
.
.
.
}
|
变量声明中的模式
在变量声明中的模式对于返回对偶(更广一点也可以用在元组上吧?)的函数来说很有用。
1
2
3
|
val
(
x
,
y
)
=
(
1
,
2
)
val
(
q
,
r
)
=
BigInt
(
10
)
/
%
3
// 返回商和余数的对偶
val
Array
(
first
,
second
,
_
*
)
=
arr
// 将第一和第二个分别给first和second
|
for表达式中的模式
这一部分的内容多在介绍for表达式时提过了,不过当时并没有意识到使用的是模式。
1
2
3
4
5
6
7
|
import
scala
.
collection
.
JavaConversions
.
propertiesAsScalaMap
for
(
(
k
,
v
)
<
-
System
.
getProperties
(
)
)
// 这里使用了模式
println
(
k
+
" -> "
+
v
)
for
(
(
k
,
""
)
<
-
System
.
getProperties
(
)
)
// 失败的匹配会被忽略,所以只打印出值为空的键
println
(
k
)
|
样例类
样例类是种特殊的类,经过优化以用于模式匹配。
1
2
3
4
5
6
7
|
abstract
class
Amount
// 继承了普通类的两个样例类
case
class
Dollar
(
value
:
Double
)
extends
Amount
case
class
Currency
(
value
:
Double
,
unit
:
String
)
extends
Amount
// 样例对象
case
object
Nothing
extends
Amount
|
使用:
1
2
3
4
5
|
amt
match
{
case
Dollar
(
v
)
=
>
"$"
+
v
case
Currency
(
_
,
u
)
=
>
"Oh noes, I got "
+
u
case
Nothing
=
>
""
// 样例对象没有()
}
|
在声明样例类时,下面的过程自动发生了:
- 构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
- 在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
- 提供unapply方法使模式匹配可以工作;
- 生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
除了上述之外,样例类和其他类型完全一样,方法字段等。
copy方法和带名参数
样例类的copy方法创建一个与现有对象相同的新对象。可以使用带名参数来修改某些属性:
1
2
3
|
val
amt
=
Currency
(
29.95
,
"EUR"
)
val
price
=
amt
.
copy
(
values
=
19.95
)
val
price
=
amt
.
copy
(
unit
=
"CHF"
)
|
case语句中的中置表示法
如果unapply方法产出一个对偶,则可以在case语句中使用中置表示法。对于有两个参数的样例类,可以使用中置表示法。
1
|
amt
match
{
case
a
Currency
u
=
>
.
.
.
}
// 等于case Currency(a, u)
|
这个特性的本意是要匹配序列。举例,List对象要么是Nil,要么是样例类::。所以可以:
1
|
lst
match
{
case
h
::
t
=
>
.
.
.
}
// 等同于case ::(h, t),调用::.unapply(lst)
|
多个中置表达式放在一起时会比普通的形式更加易读。
匹配嵌套结构
这个解释起来有点绕。
1
2
3
4
5
6
7
8
9
10
11
|
abstarct
class
Item
case
class
Article
(
description
:
String
,
price
:
Double
)
extends
Item
case
class
Bundle
(
description
:
String
,
price
:
Double
,
items
:
Item
*
)
extends
Item
Bundle
(
"Father's day special"
,
20.0
,
Article
(
"Scala for the Impatient"
,
39.95
)
,
Bundle
(
"Anchor Distillery Sampler"
,
10.0
,
Article
(
"Old Potrero Straight Rye Whisky"
,
79.95
)
,
Article
(
"Junipero Gin"
,
32.95
)
)
)
|
模式可以匹配到特定的嵌套:
1
|
case
Bundle
(
_
,
_
,
Article
(
descr
,
_
)
,
_
*
)
=
>
.
.
.
|
上面的代码中descr这个变量被绑定到第一个Article的description。另外还可以使用@来将值绑定到变量:
1
2
|
// art被绑定为第一个Article,rest是剩余的Item序列
case
Bundle
(
_
,
_
,
art
@
Article
(
_
,
_
)
,
rest
@
_
*
)
=
>
.
.
.
|
下面是个使用了模式匹配来递归计算Item价格的函数。
1
2
3
4
|
def
price
(
it
:
Item
)
:
Double
=
it
match
{
case
Article
(
_
,
p
)
=
>
p
case
Bundle
(
_
,
disc
,
its
@
_
*
)
=
>
its
.
map
(
price
_
)
.
sum
-
disc
}
|
密封类
当使用样例类来做模式匹配时,如果要让编译器确保已经列出所有可能的选择,可以将样例类的通用超类声明为sealed。
密封类的所有子类都必须在与该密封类相同的文件中定义。
如果某个类是密封的,那么在编译期所有的子类是可知的,因而可以检查模式语句的完整性。
让所有同一组的样例类都扩展某个密封的类或特质是个好的做法。
模拟枚举
可以使用样例类来模拟枚举类型:
1
2
3
4
5
6
7
8
9
10
|
sealed
abstract
class
TrafficLightColor
case
object
Red
extends
TrafficLightColor
case
object
Yellow
extends
TrafficLightColor
case
object
Green
extends
TrafficLightColor
color
match
{
case
Red
=
>
"stop"
case
Yellow
=
>
"hurry up"
case
Green
=
>
"go"
}
|
Option类型
Option类型用来表示可能存在也可能不存在的值。样例子类Some包装了某个值,而样例对象None表示没有值。Option支持泛型。
1
2
3
4
|
scores
.
get
(
"Alice"
)
match
{
case
Some
(
score
)
=
>
println
(
score
)
case
Nome
=
>
println
(
"No score"
)
}
|
偏函数(L2)
被包在花括号内的一组case语句是一个偏函数。
偏函数是一个并非对所有输入值都有定义的函数,是PartialFunction[A, B]类的一个实例,其中A是参数类型,B是返回类型。该类有两个方法:apply方法从匹配的模式计算函数值;isDefinedAt方法在输入至少匹配其中一个模式时返回true。
1
2
3
4
|
val
f
:
PartialFunction
[
Char
,
Int
]
=
{
case
'+'
=
>
1
;
case
'-'
=
>
-
1
}
f
(
'-'
)
// 返回-1
f
.
isDefinedAt
(
'0'
)
// false
f
(
'0'
)
//抛出MatchError
|