六、Scala之模式匹配

Scala之模式匹配

文章目录

1. match

Scala中的模式匹配类似于Java中的switch语法,但是更加强大。

模式匹配语法中,采用match关键字声明,每个分支采用case关键字进行声明,当需要匹配时,会从第一个case分支开始,如果匹配成功,那么执行对应的逻辑代码,如果匹配不成功,继续执行下一个分支进行判断。如果所有case都不匹配,那么会执行case _ 分支,类似于Java中default语句。

Java Switch的简单回顾

1
2
3
4
5
6
7
8
9
10
// Java
int i = 1;
switch ( i ) {
case 0 :
break;
case 1 :
break;
default :
break;
}

Scala的模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
// 模式匹配,类似于Java的switch语法
val oper = '#'
val n1 = 20
val n2 = 10
var res = 0
oper match {
case '+' => res = n1 + n2
case '-' => res = n1 - n2
case '*' => res = n1 * n2
case '/' => res = n1 / n2
case _ => println("oper error")
}
println("res=" + res)

match的细节和注意事项

  1. 如果所有case都不匹配,那么会执行case _ 分支,类似于Java中default语句
  2. 如果所有case都不匹配,又没有写case _ 分支,那么会抛出MatchError
  3. 每个case中,不用break语句,自动中断case
  4. 可以在match中使用其它类型,而不仅仅是字符

  1. => 等价于 java swtich 的 :
  2. => 后面的代码块到下一个 case, 是作为一个整体执行,可以使用{} 扩起来,也可以不扩。

2. 守卫

如果想要表达匹配某个范围的数据,就需要在模式匹配中增加条件守卫

1
2
3
4
5
6
7
8
9
10
11
for (ch <- "+-3!") {
var sign = 0
var digit = 0
ch match {
case '+' => sign = 1
case '-' => sign = -1
// 说明..
case _ if ch.toString.equals("3") => digit = 3
case _ => sign = 2
}
println("ch=" + ch + " " + "sign=" + sign + " " + "digit=" + digit)

输出

1
2
3
4
ch=+ sign=1 digit=0
ch=- sign=-1 digit=0
ch=3 sign=0 digit=3
ch=! sign=2 digit=0

再来看一个例子

1
2
3
4
5
6
7
8
9
10
for (ch <- "+-3!") {
var sign = 0
var digit = 0
ch match {
case _ => digit = 3 //匹配到这个,后面的就不执行了
case '+' => sign = 1
case '-' => sign = -1
}
println("ch=" + ch + " " + "sign=" + sign + " " + "digit=" + digit)
}

输出:

1
2
3
4
ch=+ sign=0 digit=3
ch=- sign=0 digit=3
ch=3 sign=0 digit=3
ch=! sign=0 digit=3

3. 模式中的变量

如果在case关键字后跟变量名,那么match前表达式的值会赋给那个变量

1
2
3
4
5
6
val ch = 'V'
ch match {
case '+' => println("ok~")
case mychar => println("ok~" + mychar) //这一个执行,输出:ok~V
case _ => println("ok~~")
}

match是一个表达式,因此有返回值

1
2
3
4
5
6
val ch1 = '+'
val res = ch1 match {
case '+' => ch1 + "hello"
case '-' => println("12311")
}
println(res)//输出 +hello

4. 类型匹配

可以匹配对象的任意类型,这样做避免了使用isInstanceOf和asInstanceOf方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 类型匹配, obj 可能有如下的类型
val a = 7
val obj = if (a == 1) 1
else if (a == 2) "2"
else if (a == 3) BigInt(3)
else if (a == 4) Map("aa" -> 1)
else if (a == 5) Map(1 -> "aa")
else if (a == 6) Array(1, 2, 3)
else if (a == 7) Array("aa", 1)
else if (a == 8) Array("aa")

//1. 根据Object的类型来匹配
// a:Int是一个变量,如果匹配到了,会把obj赋值给a
val result = obj match {
case a: Int => a
case b: Map[String, Int] => "对象是一个字符串-数字的Map集合"
case c: Map[Int, String] => "对象是一个数字-字符串的Map集合"
case d: Array[String] => "对象是一个字符串数组"
case e: Array[Int] => "对象是一个数字数组"
case f: BigInt => Int.MaxValue
case _ => "啥也不是"
}
println(result) //输出:啥也不是

类型匹配注意事项

  1. Map[String, Int] 和Map[Int, String]是两种不同的类型,其它类推。
  2. 在进行类型匹配时,编译器会预先检测是否有可能的匹配,如果没有则报错.
1
2
3
4
5
6
7
8
val obj = 10
// val obj=Map("hell0"->12)
val result = obj match {
case a: Int => a
case b: Map[String, Int] => "Map集合"
case _ => "啥也不是"
}
//说明:编译器报错
  1. 一个说明:
1
2
3
val result = obj match {
case i : Int => i
}

上述代码中的 case i : Int => i表示 将 i = obj (其它类推),然后再判断类型

  1. 如果 case _ 出现在match 中间,则表示隐藏变量名,即不使用,而不是表示默认匹配,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 类型匹配, obj 可能有如下的类型
val a = 7
val obj = if(a == 1) 1
else if(a == 2) "2"
else if(a == 3) BigInt(3)
else if(a == 4) Map("aa" -> 1)
else if(a == 5) Map(1 -> "aa")
else if(a == 6) Array(1, 2, 3)
else if(a == 7) Array("aa", 1)
else if(a == 8) Array("aa")
val result = obj match {
case a : Int => a
case _ : BigInt => Int.MaxValue //看这里!
case b : Map[String, Int] => "对象是一个字符串-数字的Map集合"
case c : Map[Int, String] => "对象是一个数字-字符串的Map集合"
case d : Array[String] => "对象是一个字符串数组"
case e : Array[Int] => "对象是一个数字数组"
case _ => "啥也不是"
}
println(result)

5. 匹配数组

  1. Array(0) 匹配只有一个元素且为0的数组。
  2. Array(x,y) 匹配数组有两个元素,并将两个元素赋值为x和y。当然可以依次类推Array(x,y,z) 匹配数组有3个元素的等等…
  3. Array(0,_*)匹配数组以0开始

应用案例

1
2
3
4
5
6
7
8
9
10
for (arr <- Array(Array(0), Array(1, 0), Array(0, 1, 0),
Array(1, 1, 0), Array(1, 1, 0, 1))) {
val result = arr match {
case Array(0) => "0"
case Array(x, y) => x + "=" + y
case Array(0, _*) => "以0开头和数组"
case _ => "什么集合都不是"
}
println("result = " + result)
}

输出

1
2
3
4
5
result = 0
result = 1=0
result = 以0开头和数组
result = 什么集合都不是
result = 什么集合都不是

6. 匹配列表

案例

1
2
3
4
5
6
7
8
9
for (list <- Array(List(0), List(1, 0), List(0, 0, 0), List(1, 0, 0))) {
val result = list match {
case 0 :: Nil => "0" //返回只有一个0的列表
case x :: y :: Nil => x + " " + y //匹配两个元素列表
case 0 :: tail => "0 ..." //以0开头,后面有任意个元素,tail表示后面有任意
case _ => "something else"
}
println(result)
}

输出

1
2
3
4
0
1 0
0 ...
something else

请思考,如果要匹配 List(88) 这样的只含有一个元素的列表,并原值返回.应该怎么写?

1
2
3
4
5
6
7
8
9
for (list <- Array(List(99), List(88), List(0, 0, 0), List(1, 0, 0))) {
val result = list match {
case 88 :: Nil => "88" //返回只有一个88的列表
case x :: y :: Nil => x + " " + y //匹配两个元素列表
case 0 :: tail => "0 ..." //以0开头,后面有任意个元素,tail表示后面有任意
case _ => "something else"
}
println(result)
}

输出

1
2
3
4
something else
88
0 ...
something else

7. 匹配元组

案例

1
2
3
4
5
6
7
8
9
10
// 元组匹配
// 元组匹配
for (pair <- Array((0, 1),(0,1,1), (1, 0), (1, 1), (1, 0, 2))) {
val result = pair match { //
case (0, _) => "0 ..." //匹配以0开头的二元组,但是第二个元素我舍弃,以后我不用它
case (y, 0) => y //匹配一个二元组,但是第二个元素必须是0
case _ => "other" //.
}
println(result)
}

输出

1
2
3
4
5
0 ...
other
1
other
other

8. 对象匹配

对象匹配,什么才算是匹配呢?,规则如下:

  1. case中的对象的unapply方法( 对象提取器 )返回Some集合则为匹配成功
  2. 返回none集合则为匹配失败

应用案例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package dataStructure.mapPack

object T9 {

def main(args: Array[String]): Unit = {

object Square {
//Option也是一个集合
//unapply对象提取器,返回Option[Double]类型
def unapply(z: Double): Option[Double] = Some(math.sqrt(z))

def apply(z: Double): Double = z * z
}
// 模式匹配使用:
val number: Double = 36.0
number match {
//当匹配到case Square(n) 时,会调用Square的unapply方法,这时n会传给unapply的参数z
//如果unapply方法返回的是Some(6),则表示匹配成功,同时将这个8 赋给 Square(n)里面的n
case Square(n) => println(n) //输出6.0
case _ => println("nothing matched")
}
}
}

应用案例1的小结

  1. 构建对象时apply会被调用 ,比如 val n1 = Square(5)
  2. 当将 Square(n) 写在 case 后时,如case Square(n) => xxx,会默认调用unapply 方法(对象提取器)
  3. number 会被 传递给def unapply(z: Double) 的 z 形参
  4. 如果返回的是Some集合,则unapply提取器返回的结果会返回给 n 这个形参
  5. case中对象的unapply方法(提取器)返回some集合则为匹配成功
  6. 返回none集合则为匹配失败

应用案例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package dataStructure.mapPack

object T11 {

def main(args: Array[String]): Unit = {
object Names {
def unapplySeq(str: String): Option[Seq[String]] = {
if (str.contains(",")) Some(str.split(","))
else None
}
}
val namesString = "Alice,Bob,Thomas"
//说明
namesString match {
case Names(first, second, third) => {
println("the string contains three people's names")
// 打印字符串
println(s"$first $second $third")
}
case _ => println("nothing matched")
}
}
}

输出

1
2
the string contains three people's names
Alice Bob Thomas

应用案例2的小结

  1. 当case 后面的对象提取器方法的参数为多个,则会默认调用def unapplySeq() 方法
  2. 如果unapplySeq返回是Some,获取其中的值,判断得到的sequence中的元素的个数是否是三个,如果是三个,则把三个元素分别取出,赋值给first,second和third
  3. 其它的规则不变.

9. 变量声明中的模式

match中每一个case都可以单独提取出来,意思是一样的.

1
2
3
4
5
val (x, y) = (1, 2) //相等于定义了两个变量
val (q, r) = BigInt(10) /% 3 //说明 q = BigInt(10) / 3 ,r = BigInt(10) % 3
val arr = Array(1, 7, 2, 9)
val Array(first, second, _*) = arr // 提出arr的前两个元素
println(first, second) //(1,7)

10. for表达式中的模式

for循环也可以进行模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val map = Map("A" -> 1, "B" -> 0, "C" -> 3)
for ((k, v) <- map) {
println(k + " -> " + v)
}
println("=================")
//说明 匹配val为0的键值对
for ((k, 0) <- map) {
println(k + " --> " + 0)
}
println("=================")
//说明 匹配val为0的键值对
for ((k, v) <- map if v == 0) {
println(k + " ---> " + v)
}

输出

1
2
3
4
5
6
7
A -> 1
B -> 0
C -> 3
=================
B --> 0
=================
B ---> 0

11. 样例(模板)类

引入

如果一个类中没有任何内容,那么后面的{}可以省略,样例类也是如此

比如

1
2
3
4
class Dog{
}
//等同于
class Dog

样例类快速入门

1
2
3
4
abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
case object NoAmount extends Amount

说明: 这里的 Dollar,Currencry, NoAmount 是样例类。

基本介绍

  1. 样例类仍然是类
  2. 样例类用case关键字进行声明。
  3. 样例类是为模式匹配而优化的类
  4. 构造器中的每一个参数都成为val——除非它被显式地声明为var(不建议这样做)
  5. 在样例类对应的伴生对象中提供apply方法让,你不用new关键字就能构造出相应的对象
  6. 提供unapply方法让模式匹配可以工作
  7. 将自动生成toString、equals、hashCode和copy方法(有点类似模板类,直接给生成,供程序员使用)
  8. 除上述外,样例类和其他类完全一样。你可以添加方法和字段,扩展它们

样例类最佳实践1:

当我们有一个类型为Amount的对象时,可以用模式匹配来匹配他的类型,并将属性值绑定到变量(即:把样例类对象的属性值提取到某个变量,该功能有用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package dataStructure.mapPack

object T12 {

def main(args: Array[String]): Unit = {
for (amt <- Array(Dollar(1000.0), Currency(1000.0, "RMB"), NoAmount)) {
val result = amt match {
//说明
case Dollar(v) => "$" + v
//说明
case Currency(v, u) => v + " " + u
case NoAmount => ""
}
println(amt + ": " + result)
}
}
}
abstract class Amount
case class Dollar(value: Double) extends Amount
case class Currency(value: Double, unit: String) extends Amount
case object NoAmount extends Amount

输出

1
2
3
Dollar(1000.0): $1000.0
Currency(1000.0,RMB): 1000.0 RMB
NoAmount:

样例类最佳实践2:

样例类的copy方法和带名参数

copy创建一个与现有对象值相同的新对象,并可以通过带名参数来修改某些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
object T13 {

def main(args: Array[String]): Unit = {
val amt = Currency(29.95, "RMB")
val amt1 = amt.copy() //创建了一个新的对象,但是属性值一样
val amt2 = amt.copy(value = 19.95) //创建了一个新对象,但是修改了货币单位
val amt3 = amt.copy(unit = "英镑")//..
println(amt)
println(amt2)
println(amt3)
}
}
case class Currency(value: Double, unit: String) extends Amount

输出

1
2
3
Currency(29.95,RMB)
Currency(19.95,RMB)
Currency(29.95,英镑)

12. case语句的中置(缀)表达式

什么是中置表达式?

1 + 2,这就是一个中置表达式。可以在case语句中使用中置表示法,比如可以匹配一个List序列

1
2
3
4
5
6
7
8
9
List(1, 3, 5, 9) match { //修改并测试
//1.两个元素间::叫中置表达式,至少first,second两个匹配才行.注意:这里的名字随意
//2.first 匹配第一个 second 匹配第二个, rest 匹配剩余部分List(5, 9)
case first :: second :: rest => {
println(first +","+ second +","+ rest.length) //1,3,2
println(rest) //List(5, 9)
}
case _ => println("匹配不到...")
}

13. 匹配嵌套结构【案例说明】

操作原理类似于正则表达式

最佳实践案例-商品捆绑打折出售

现在有一些商品,请使用Scala设计相关的样例类,完成商品捆绑打折出售。要求

  1. 商品捆绑可以是单个商品,也可以是多个商品。
  2. 打折时按照折扣x元进行设计.
  3. 能够统计出所有捆绑商品打折后的最终价格

(1)创建样例类

1
2
3
4
abstract class Item // 
case class Book(description: String, price: Double) extends Item
//Bundle discount: Double 折扣 , item: Item* ,
case class Bundle(description: String, discount: Double, item: Item*) extends Item

(2)匹配嵌套结构(就是Bundle的对象)

1
2
//给出案例表示有一捆数,单本漫画(40-10) +文学作品(两本书)(80+30-20)= 30 + 90 = 120.0
val sale = Bundle("书籍", 10, Book("漫画", 40), Bundle("文学作品", 20, Book("《阳关》", 80), Book("《围城》", 30)))

(3)知识点1-description绑定到第一个Book的描述

请思考:如何取出“漫画”

1
2
3
4
5
6
//使用case语句,得到"漫画"
val res = sale match {
//如果我们进行对象匹配时,不想接受某些值,则使用_ 忽略即可,_* 表示所有
case Bundle(_, _, Book(desc, _), _*) => desc
}
println("res="+res) //res=漫画

(4)知识点2-通过@表示法将嵌套的值绑定到变量。_*绑定剩余Item到rest

1
2
3
4
5
6
7
8
9
10
//得到一个元祖
val result2 = sale match {
//@ Book(_, _)直接返回整个对象
case Bundle(_, _, art @ Book(_, _), rest @ _*) => (art, rest)
}

println(result2)
println("art =" + result2._1)//art =Book(漫画,40.0)
//外面一层有WrappedArray
println("rest=" + result2._2)//rest=WrappedArray(Bundle(文学作品,20.0,WrappedArray(Book(《阳关》,80.0), Book(《围城》,30.0))))

(5)知识点3-不使用_*绑定剩余Item到rest

1
2
3
4
5
6
7
8
val result3 = sale match {
//说明因为没有使用 _* 即明确说明没有多个Bundle,所以返回的rest,就不是WrappedArray了。
case Bundle(_, _, art @ Book(_, _), rest) => (art, rest)
}
println(result3)
println("art =" + result3._1) //art =Book(漫画,40.0)
//外面一层没有WrappedArray了
println("rest=" + result3._2) //rest=Bundle(文学作品,20.0,WrappedArray(Book(《阳关》,80.0), Book(《围城》,30.0)))

最佳实践案例-商品捆绑打折出售

现在有一些商品,请使用Scala设计相关的样例类,完成商品可以捆绑打折出售。
要求

  1. 商品捆绑可以是单个商品,也可以是多个商品。
  2. 打折时按照折扣xx元进行设计.
  3. 能够统计出所有捆绑商品打折后的最终价格
1
2
3
4
5
6
7
8
9
10
def price(it: Item): Double = {
it match {
//如果是一本书,则只关心价格
case Book(_, p) => p
//如果是一个Bundle,则获取折扣disc和后面的东西
//its.map(price) 指 再次调用price方法,递归的去计算,从而获取所有的价格和折扣
//计算完了之后再求和,并减去折扣
case Bundle(_, disc, its @ _*) => its.map(price).sum - disc
}
}

最终代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package dataStructure.lizi

object T1 {

def main(args: Array[String]): Unit = {

def price(it: Item): Double = {
it match {
case Book(_, p) => p
//生成一个新的集合,_是将its中每个循环的元素传递到price中it中。递归操作,分析一个简单的流程
case Bundle(_, disc, its@_*) => its.map(price _).sum - disc
}
}

val sale = Bundle("书籍", 10, Book("漫画", 40), Bundle("文学作品", 20, Book("《阳关》", 80), Book("《围城》", 30)))

println("price: "+price(sale))//120.0,答案正确
}
}

abstract class Item //
case class Book(description: String, price: Double) extends Item

//Bundle discount: Double 折扣 , item: Item* ,
case class Bundle(description: String, discount: Double, item: Item*) extends Item