Scala学习笔记-核心特性(4)-模式匹配

模式匹配

首先回顾一下Java中的switch-case语法,我们可以使用case来指定多分支的执行逻辑,在每个case语句中需要使用break退出。根据情况会首先进入一个case代码块,一直向下执行直到遇见一个break块或者一直向下执行了所有的代码块。

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
int i = 20;
switch (i) {
case 10:
System.out.println("10");
break;
case 20:
System.out.println("20");
break;
case 30:
System.out.println("30");
break;
default:
System.out.println("other number");
break;
}
switch (i) {
case 10:
System.out.println("10-1");
case 20:
System.out.println("20-1");
case 30:
System.out.println("30-1");
default:
System.out.println("other number-1");
}

以上代码的执行结果为:

1
2
3
4
20
20-1
30-1
other number-1

Scala中没有switch关键字,但是提供了模式匹配的功能。下面我们介绍在Scala中模式匹配的基本语法。

基本语法

Scala中的模式匹配语法采用match-case结构。模式匹配采用match关键字声明,每个分支采用case关键字进行声明。匹配会进入对应的case代码块,如果所有的case都没有匹配,则进入case _分支(类似Java中的default)

1
2
3
4
5
6
value match {
case caseVal1 => returnVal1
case caseVal2 => returnVal2
...
case _ => defaultVal
}

与Java中不同的是,模式匹配的每个case分支中不用写break语句(Scala中也没有break关键字),在执行的时候,执行完了一个case分支,就直接跳出整个模式匹配的后面(不会像Java中一样还会向后继续执行case分支)

1
2
3
4
5
6
7
val i:Int = 20
i match {
case 10 => println("10")
case 20 => println("20")
case 30 => println("30")
case _ => println("other number")
}

以上代码的运行结果为

1
20
  • match case语句可以匹配任何类型,而不只是字面量。后面会提到这点
  • =>后面的代码块,直到下一个case语句之前的代码是作为一个整体执行,可以使用{}进行包裹,也可以不使用

模式守卫

如果想要表达式匹配某个范围内的数据,则可以在模式匹配中增加条件守卫,即使用条件表达式,例如下面的代码实现了求整数绝对值的功能

1
2
3
4
5
6
7
def abs(num: Int): Int = {
num match {
case i if i >= 0 => i
case i if i < 0 => -i
}
}
println(abs(-29))

模式匹配类型

在Scala中,模式匹配的功能比其他语言中的switch-case语句要更加强大,它提供了更多的模式匹配类型,包括匹配常量、类型、数组、列表、元组以及对象等。(注意以下的匹配方式都可以混用)

匹配常量

Scala中,模式匹配支持匹配所有的字面量,包括字符串、字符、数字、布尔值等,可以通过传入Any类型的变量来进行匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
def matchConst(x: Any): String = {
x match {
case 1 => "Int One"
case "hello" => "String hello"
case '+' => "Char +"
case true => "Boolean true"
case a => "other " + a
}
}

println(matchConst(1))
println(matchConst(2))
println(matchConst(true))

注意,这里没有case _,但是同样存在defalut的情况,就是最后一个case代码块。case _的情况是在不需要变量的值的时候建议这么做,当然也可以使用一个变量的值来承接,就像这里的case a,这里的a承接了不匹配的其他所有的值,应该是Any类型

匹配类型

需要进行类型判断的时候,可以使用前面说到过的isInstanceOfasInstanceOf,也可以使用模式匹配来匹配类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def matchType(x: Any): String = {
x match {
case i: Int => "Int " + i
case s: String => "String " + s
case list: List[Int] => "List " + list
case array: Array[Int] => "Array[Int] " + array.mkString(" ")
case somethingElse => "SomethingElse " + somethingElse
}
}
println(matchType(10))
println(matchType("hello"))
println(matchType(List(1, 2, 3, 4, 5)))
println(matchType(List("hi", "hello")))
println(matchType(Array(1, 2, 3, 4, 5)))
println(matchType(Array("hi", "hello")))
println(matchType('c'))

以上代码的运行结果如下

1
2
3
4
5
6
7
Int 10
String hello
List List(1, 2, 3, 4, 5)
List List(hi, hello)
Array[Int] 1 2 3 4 5
SomethingElse [Ljava.lang.String;@6bdf28bb
SomethingElse c

需要注意的是输出结果的第4行和第6行,第4行中List的泛型和我们定义的不匹配,但是仍旧输出了对应的case语句,而第6行中Array的泛型不匹配,就走了最后一行的default。这里涉及到Scala中泛型擦除的现象,即在类型匹配的时候由于泛型擦除,可能并不能严格匹配泛型的类型参数。但是由于Array是基本数据类型,对应于Java中的原生数组类型,能够匹配泛型类型参数。

匹配数组

Scala中的模式匹配可以对集合进行精确的匹配。可以对数组定义多种匹配方式,例如模糊的元素类型匹配、元素数量匹配或者精确到某个数组元素值匹配,功能非常强大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def matchArray(array: Any): String = {
array match {
case Array(0) => "0" // 匹配特定数组Array(0)
case Array(1, 0) => "Array(1, 0)" // 匹配特定数组Array(1, 0)
case Array(x: Int, y: Int) => "Array: " + x + " " + y // 匹配两整型元素数组
case Array(0, _*) => "以0开头的数组"
case Array(x, 1, z) => "中间为1的三元素数组"
case Array(x: String, _*) => "第一个元素为字符串的数组"
case _ => "Something Else"
}
}

println(matchArray(Array(0)))
println(matchArray(Array(1, 0)))
println(matchArray(Array(1, 1, 0)))
println(matchArray(Array(0, 1, 0)))
println(matchArray(Array(2, 3, 4, 5, 6)))
println(matchArray(Array("String", 1, 2, 3)))

匹配列表

列表匹配和数组匹配类似,其中可以混入集合类中灵活的运算符

1
2
3
4
5
6
7
8
9
10
11
12
def matchList(list: Any): String = {
list match {
case List(0) => "0"
case List(x, y) => "List(x, y) " + x + "," + y
case List(0, _*) => "List(0, ...)"
case _ => "Something Else"
}
}
println(matchList(List(0)))
println(matchList(List(1, 0)))
println(matchList(List(0, 0, 0)))
println(matchList(List("hello")))

注意混入集合运算符的用法。下面的代码匹配数组的第一个元素、第二个元素和其余元素

1
2
3
4
5
6
7
8
9
10
def matchList1(list: Any): String = {
list match {
case first :: second::rest =>s"first:${first} second:${second} rest:${rest}"
case _ => "Something Else"
}
}

println(matchList1(List(1, 2, 3, 4, 5, 6)))
println(matchList1(List(1, 2)))
println(matchList1(List(1)))

以上代码的输出结果如下,如果rest为空也是能够匹配的

1
2
3
first:1 second:2 rest:List(3, 4, 5, 6)
first:1 second:2 rest:List()
Something Else

匹配元组

匹配元素类似于其他的集合匹配,可以匹配n元组、元素类型、元素值等。由于元组的大小固定,因此不能使用_*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def matchTuple(tuple: Any): String = {
tuple match {
case (a, b) => "(" + a + "," + b + ")"
// case (0, _*) => "(0,...)" // 此行报错
case (0, _) => "(0,_)"
case (x, 1, z) => "(x, y, z):" + x + " " + 1 + " " + z
case _ => "Something Else"
}
}

println(matchTuple((0, 1)))
println(matchTuple((0, 1, 1)))
println(matchTuple((0, 1)))
println(matchTuple((1, 1)))
println(matchTuple(("ture", '1', 1)))

元组的模式匹配可以用在变量声明以及for推导式中,在后面的应用场景提及到了

匹配对象以及样例类

Scala的模式匹配还能够匹配对象,我们希望完成的是对象的内容匹配。直接在match-case中匹配对应的引用变量,语法是存在问题的。编译报错信息提示我们需要使用样例类或者实现unapply方法

要完成对象匹配,我们应该实现伴生对象中的apply方法和unapply方法,其中unapply方法也是一个特殊的方法,用来对对象的属性进行拆解

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
class Student(val name: String, val age: Int)

object Student {
def apply(name: String, age: Int): Student = new Student(name, age)

// 实现unapply方法,用来对对象属性进行拆解
def unapply(student: Student): Option[(String, Int)] = {
if (student == null) None
else Some((student.name, student.age))
}
}

object Test {
def main(args: Array[String]): Unit = {
val student = new Student("xxx", 21)

// 针对对象实例的内容进行匹配
val result = student match {
case Student("xxx", 21) => "xxx, 21"
case _ => "Else"
}
println(result)
}
}

  • 在case后面的Student("xxx", 21),实际上是调用了伴生对象的apply方法
  • 当写在case后面,会默认调用unapply方法(对象提取器),Student对象作为unapply方法的参数,它将对象的nameage属性进行提取,然后与前面的case对象进行匹配
  • unapply方法返回Some对象,只有所有属性都一致才算返回成功。属性不一致或者返回None,则匹配失败
  • 以下是一些模板
    • 只提取对象的一个属性:unapply(obj:Obj):Option[T]
    • 提取对象的多个属性:unapply(obj:Obj):Option[T1, T2, T3,...]
    • 提取对象的可变个属性:unapplySeq(obj:Obj):Option[Seq[T]]

也可以使用样例类的方式来完成,利用case声明一个样例类。样例类是为模式匹配而优化的类,其中自动生成了伴生对象,并且自动提供一些常用的方法,默认提供了如applyunapplytoStringequalshashCodecopy等方法,因此无需自己实现unapply方法。样例类中的每个参数都是val,除非它被显式地声明为var

这样上面的代码可以修改成下面的代码,同样能够完成对应的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case class Student(val name: String, val age: Int)


object Test {
def main(args: Array[String]): Unit = {
val student = new Student("xxx", 21)

// 针对对象实例的内容进行匹配
val result = student match {
case Student("xxx", 21) => "xxx, 21"
case _ => "Else"
}
println(result)
}
}

一些应用场景

变量声明中的模式匹配

1
2
3
4
5
6
7
8
val (x, y) = (10, "hello")
println(s"x: $x, y: $y")

val List(first, second, _*) = List(23, 15, 9, 78)
println(s"first: $first, second: $second")

val fir :: sec :: rest = List(23, 15 , 9, 78)
println(s"first: $fir, second: $sec, rest: $rest")

for表达式中的模式匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val list: List[(String, Int)] = List(("a", 12), ("b", 35), ("c", 27), ("a", 13))

// 原本的遍历方式
for (elem <- list){
println(elem._1 + " " + elem._2)
}

// 将List的元素直接定义为元组,对变量赋值
for ((word, count) <- list ){
println(word + ": " + count)
}

println("-----------------------")
// 可以不考虑某个位置的变量,只遍历key或者value
for ((word, _) <- list)
println(word)

println("-----------------------")

// 可以指定某个位置的值必须是多少
for (("a", count) <- list){
println(count)
}

偏函数中的模式匹配

偏函数是函数的一种,通过偏函数我们可以方便地对输入参数做更精确的检查,内部就是通过模式匹配来实现的,举例如下

1
2
3
val partialFuncName: PartialFunction[List[Int], Option[Int]] = {
case x :: y :: _ => Some(y)
}
  • 通过一个变量定义方式定义,PartialFunction的泛型类型中,前者是参数类型,后者是返回值类型。函数体中用一个case语句来进行模式匹配。上面例子返回输入的List集合中的第二个元素。
  • 一般一个偏函数只能处理输入的一部分场景,实际中往往需要定义多个偏函数用以组合使用。
  • 偏函数能够实现case条件语句的复用?
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
26
27
28
29
30
31
32
33
34
35
36
37
val list: List[(String, Int)] = List(("a", 12), ("b", 10), ("c", 100), ("a", 5))

// map转换,实现key不变,value变成2倍
val newList = list.map(tuple => (tuple._1, tuple._2 * 2))
println(newList)

// 用模式匹配对元组元素赋值 实现功能
val newList1 = list.map(
tuple => {
tuple match {
case (x, y) => (x, y * 2)
}
}
)
println(newList1)

// 简化表达
val newList2 = list.map {
case (x, y) => (x, y * 2) // this is a partial function
}
println(newList2)

// 偏函数的应用,求绝对值
val positiveAbs: PartialFunction[Int, Int] = {
case x if x > 0 => x
}
val negativeAbs: PartialFunction[Int, Int] = {
case x if x < 0 => -x
}
val zeroAbs: PartialFunction[Int, Int] = {
case 0 => 0
}

def abs(x: Int): Int = (positiveAbs orElse negativeAbs orElse zeroAbs) (x)
println(abs(-13))
println(abs(30))
println(abs(0))

Scala学习笔记-核心特性(4)-模式匹配
http://example.com/2022/05/03/Scala学习笔记-核心特性-4-模式匹配/
作者
EverNorif
发布于
2022年5月3日
许可协议