7. Scala的模式匹配
7.1 样例类和对象
定义类时,如果在最前面加上关键字case
,则这个类就被称为样例类。Scala的编译器自动对样例类添加一些语法便利:
- 添加一个与类同名的工厂方法,可以通过
类名(参数)
来构造对象,而不需要使用new 类名(参数)
来构造; - 参数列表的每个参数都隐式地获得了一个
val
前缀,类内部会自动添加与参数同名地公有字段; - 会自动以自然的方式实现
toString
,hashCode
和equals
方法; - 添加一个
copy
方法,用于构造与旧对象只有某些字段不一样的新对象,只需通过传入具名参数和默认参数实现。比如object.copy(arg0=10)
会创建一个只有arg0
为10,其余成员都与objectA
完全一样的新对象。
scala> case class Students(name: String, score: Int)
// defined case class Studentsscala> val stu1 = Students("Alice", 100)
val stu1: Students = Students(Alice,100)scala> stu1.name
val res4: String = Alicescala> stu1.score
val res5: Int = 100scala> val stu2 = stu1.copy()
val stu2: Students = Students(Alice,100)scala> stu2 == stu1
val res6: Boolean = truescala> val stu3 = stu1.copy(name = "Bob")
val stu3: Students = Students(Bob,100)scala> stu3 == stu1
val res7: Boolean = false
样例类支持模式匹配,之后详细说明。样例对象类似于样例类,也是在定义单例对象时加上case
关键字。样例对象和一个无参,无构造方法的样例类是一样的。
7.2 模式匹配
模式匹配的语法:选择器 match {可选分支}
其中,选择器是待匹配的对象,花括号中式一系列以关键字case
开头的可选分支,每个可选分支都包括一个模式及一个或多个表达式。如果模式匹配成功,则执行相应的操作,最后返回结果。
可选分支的定义:case 模式 => 表达式
match
是一个表达式,它可以返回一个值;- 可选分支存在优先级,匹配顺序是代码编写顺序,只有第一个匹配成功的模式会被选中,将其表达式求值并返回;
- 要确保至少有一个模式匹配成功,不然会报错。
7.3 模式的种类
模式匹配的强大原因之一是因为它支持多种模式。
7.3.1通配模式
通配模式用下划线_
表示,它会匹配任何对象,通常放在末尾用于缺省,捕获所有可选路径,相当于default
。
scala> def test(x: Any) = x match {| case List(1,2,_) => true| case _ => false| }
def test(x: Any): Booleanscala> test(List(1,2,3))
val res8: Boolean = truescala> test(List(1,2,10))
val res9: Boolean = truescala> test(List(1,2))
val res10: Boolean = falsescala> test(List(1,2,3,4))
val res11: Boolean = falsescala> test(List(1,2,List(1,2,3)))
val res12: Boolean = truescala> test(List(1,2,stu1))
val res13: Boolean = true
第一个case
用于忽略局部特性,表面只有含有三个元素,且前两个是1和2,第三个元素是任意(这个任意包含了任何字面量,任何集合和变量等),则匹配该模式。不符合第一种模式的都会在第二种模式被捕获。由于它们是有优先级的,因此这种可选范围很大,很模糊的匹配比如只有一个下划线的模式,必须放到最后,否则将永远无法执行后面的匹配操作。
7.3.2常量模式
常量模式是使用一个常量作为模式,使其只能匹配自己。任何字面量,val
类型的变量,单例对象都可以作为常量模式。如Nil
可用于匹配空列表。
scala> def test2(x: Any) = x match {| case 5 => "five"| case true => "truth"| case "hello" => "hi!"| case Nil => "the empty list"| case _ => "something else"| }
def test2(x: Any): Stringscala> test2(List())
val res14: String = the empty listscala> test2(5)
val res15: String = fivescala> test2(true)
val res16: String = truthscala> test2("hello")
val res17: String = hi!scala> test2(2333)
val res18: String = something else
7.3.3变量模式
变量模式是一个变量名,可以匹配任何对象,和通配模式不同的是,变量模式还会把该变量名与成功匹配的输入对象绑定,在表达式中可以通过这个变量名进一步操作输入对象,也可以放在最后替代通配模式。
scala> def test3(x: Any) = x match {| case 0 => "Zero!"| case somethingElse => "Not Zero: " + somethingElse| }
def test3(x: Any): Stringscala> test3(0)
val res19: String = Zero!scala> test3(1)
val res20: String = Not Zero: 1
变量模式和通配模式一样,只能放在最后,否则编译器会警告无法到达变量模式后面的代码。
Scala使用一条简单的词法区分规则:以小写字母开头的简单名称被当作变量模式,其他引用都是常量模式。即便以小写字母开头的简单名称其实是某个常量的别名,也会被当成变量模式,如果想要绕过这条规则有两个方法:
- 如果常量是某个对象的字段,可以加上限定词表明这是一个常量
this.a
或object.a
等; - 用反引号``把名称包起来,编译器会将它解读为常量。
scala> val somethingElse = 1
val somethingElse: Int = 1scala> def test4(x: Any) = x match {| case `somethingElse` => "A constant"| case 0 => "Zero!"| case _ => "Something else!"| }
def test4(x: Any): Stringscala> test4(somethingElse)
val res21: String = A constant
7.3.4构造方法模式
构造方法模式是把一个样例类的构造方法作为模式,其形式为名称(模式)
,假设这里的名称是一个样例类的名字,那么模式首先检查待匹配的对象是不是以这个名称命名的样例类的实例,在检查待匹配对象的构造方法参数是不是匹配括号中的模式。Scala的模式支持深度匹配,括号中的模式可以是任何一种模式,包括构造方法模式。嵌套的构造方法模式会进一步展开匹配。
scala> def test5(x: Any) = x match {| case B("abc", e, A(10)) => e + 1| case _ =>| }
def test5(x: Any): Int | Unitscala> val a = B("abc", 1, A(10))
val a: B = B(abc,1,A(10))scala> val b = B("abc", 1, A(1))
val b: B = B(abc,1,A(1))scala> test5(a)
val res22: Int | Unit = 2scala> test5(b)
val res23: Int | Unit = ()
“abc”是常量模式,只能匹配字符串“abc”,e是变量模式,绑定B的第二个构造参数,在表达式中+1后返回,A(10)是构造方法模式,B的第三个参数必须是以10为参数构造的A的对象。
7.3.5序列模式
序列类型可用于模式匹配,如List
或Array
。下划线或变量模式可以指出不关心的模式,_*
放在最后可以匹配任意的元素个数。
scala> def test6(x: Any) = x match {| case Array(1, _*) => "OK"| case _ => "Oops!"| }
def test6(x: Any): Stringscala> test6(Array(1,2,3))
val res24: String = OKscala> test6(1)
val res25: String = Oops!
7.3.6元组模式
元组可以用于模式匹配,在圆括号中可以包含任意模式,形如(a,b,c)
的模式可以匹配任意三元组,里面是三个变量模式而不是三个常量模式。
scala> def test7(x: Any) = x match {| case (1,e,"OK") => "OK, e = " + e| case _ => "Oops!"| }
def test7(x: Any): Stringscala> test7(1,10,"OK")
val res26: String = OK, e = 10
7.3.7带类型的模式
模式定义时可以声明具体的数据类型,用带类型的模式可以替代类型测试和类型转换:
scala> def test8(x: Any) = x match {| case s: String => s.length| case m: Map[_,_] => m.size| case _ => -1| }
def test8(x: Any): Intscala> test8("OK")
val res27: Int = 2scala> test8(Map(1->"one"))
val res28: Int = 1
在带类型的模式中虽然可以指明对象类型是笼统的映射Map[_,_]
,但是无法指明映射的键-值分别是什么信息。这是因为Scala采用了擦除式的泛型,运行时不会保留类型参数的信息。
7.3.8变量绑定模式
除了变量模式可以使用变量以外,还可以对任何其他模式添加变量,构成变量绑定模式,形式是变量名 @ 模式
,变量绑定模式的匹配规则与绑定前相同,只不过在匹配成功后会把输入对象的相应部分与添加的变量进行绑定。
scala> def test9(x: Any) = x match {| case (1, 2, e @ 3) => e| case _ => 0| }
def test9(x: Any): Anyscala> test9(1,2,3)
val res29: Any = 3
7.4 模式守卫
模式守卫出现在模式之后,是一条用if
开头的语句,模式守卫可以是任意的布尔表达式,如果存在模式守卫,则必须模式守卫返回true
时才能匹配成功。
Scala要求模式都是线性的,即一个模式中的两个变量不能同名,如果存在两个变量同名的情况可以通过模式守卫解决。
case i: Int if i > 0 => ...
case s: String if s(0) == 'a' => ...
case (x, y) if x == y => ...
7.5 密封类
如果在class
前面加上关键字sealed
,那么这个类是密封类。密封类只能在同一个文件中定义子类,不能在文件之外被别的类继承。要使用模板匹配,最好把顶层的基类做成密封类。
7.6 可选值
从前面很多例子中可以发现两个问题:一是每条 case 分支可能返回不同类型的值,导致函数的返回值或变量的类型不好确定,该如何把它们统一起来呢?二是在通配模式下,常常不需要返回一个值。要解决这两个问题, Scala 提供了一个新的语法﹣﹣可选值。可选值就是类型为Option[T]
的一个值。其中, Option
是标准库中的一个密封抽象类。 T 可以是任意的类型,例如,标准类型或自定义的类。并且 T 是协变的,简单来说,就是如果类型 T 是类型 U 的超类,那么Option[T]
也是Option[U]
的超类。 Option 类有一个子类: Some
类。通过Some(x)
可以构造一个 Some 的对象,其中参数 x 是一个具体的值。根据 x 的类型,可选值的类型会发生改变。例如,Some(10)
的类型是Option[Int]
, Some (“10”)的类型是 Option [ String ]。由于 Some 对象需要一个具体的参数值,所以这部分可选值用于表示"有值"。 Option 类还有一个子对象: None ,它的类型是Option[Nothing]
,是所有Option[T]
类型的子类,代表"无值"。Option
类型代表要么是一个具体的值,要么无值。Some(x)
常作为 case
语句的返回值,而 None 常作为通配模式的返回值。需要注意的是,Option[T]
和 T 是两个完全无关的类型,赋值时不要混淆。如果没有可选值语法,要表示"无值"可能会选用 null ,就必须对变量进行判空操作。在 Java 中,判空是一个运行时的动作,如果忘记判空,编译时并不会报错,但是在运行时可能会抛出空指针异常,进而引发严重的错误。有了可选值之后,首先从字面上提醒读者这是一个可选值,存在无值和有值两种情况;其次,最重要的是,由于 Option [T]
类型与 T 类型不同,赋值时可能需要先做相应的类型转换。类型转换最常见的方式就是模式匹配,在这期间可以把无值 None 过滤掉。如果不进行类型转换,编译器就会抛出类型错误,这样在编译期就进行判空处理进而防止运行时出现更严重的问题。可选值提供了一个方法 isDefined
,如果调用对象是 None ,则返回 false ,而 Some 对象都会返回 true 。还有一个方法 get ,用于把 Some (x)
中的 x 返回,如果调用对象是 None ,则报错。
7.7 模式匹配的另类用法
对于提取器,可以通过var/val 对象名(模式) = 值
的方式使用模式匹配,常用于定义变量。这里的对象名指提取器,即某个单例对象。列表,数组,映射,元组等常用集合的伴生对象都是提取器。以下写法在新版Scala中会抛出警告,提示自动推断类型不完全兼容。我们最好避免这样的写法。
scala> val Array(x, y, _*) = Array(-1, 1, 233)
1 warning found
-- Warning: ------------------------------------------------------------------------------------
1 |val Array(x, y, _*) = Array(-1, 1, 233)| ^^^^^^^^^^^^^^^^^|pattern's type Int* does not match the right hand side expression's type Int||If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,|which may result in a MatchError at runtime.
val x: Int = -1
val y: Int = 1scala> val a :: 10 :: _ = List(999, 10)
1 warning found
-- Warning: ------------------------------------------------------------------------------------
1 |val a :: 10 :: _ = List(999, 10)| ^^^^^^^^^^^^^|pattern's type ::[Int] is more specialized than the right hand side expression's type List[Int]||If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,|which may result in a MatchError at runtime.
val a: Int = 999
7.8 偏函数
Scala中的函数也是一种对象,它属于某种类型。为了标记函数的类型,提供了一系列特质Function0~Function22来表示参数为0~22的函数,函数参数最多有22个,也可以自己拓展。
除此之外还有一个特殊的函数特质,即偏函数。偏函数的作用是划分一个输入参数的可行域,在可行域内对入参执行一种操作,在可行域之外对入参执行其他操作。偏函数有两个抽象方法需要实现,即apply
和isDefinedAt
。isDefinedAt
用于判断入参是否在可行域内,是的话返回true
,不是的话返回false
。apply
是偏函数的函数体,用于对入参执行操作。使用偏函数之前先用isDefinedAt
判断入参是否合法。
定义偏函数的一种简便方法是使用case
语句组,广义上一个case
语句就是一个偏函数,所以才可以用于模式匹配。一个case
是函数的一个入口,多个case
语句就有多个入口。每个case
语句可以有自己的参数列表和函数体。
用case
语句定义偏函数时,前面的各种模式类型,模式守卫都可以使用,通配模式可有可无,但没有时要保证运行不会出错。
scala> val isInt1: PartialFunction[Any, String] = {| case x: Int => x + " is a Int."| case _ => "else."| }
there was 1 deprecation warning; re-run with -deprecation for details
1 warning found
val isInt1: PartialFunction[Any, String] = <function1>scala> isInt1(1)
val res30: String = 1 is a Int.scala> isInt1("1")
val res31: String = else.