Scala模式匹配
模式匹配简介
简介
Scala模式匹配机制十分强大,可以应用在很多场合:switch语句、类型查询,以及“析构”(获取复杂表达式中不同的部分)。除此之外,Scala还提供了样例类,对模式匹配进行了优化。
要点
- match表达式是一个更好的switch,不会有意外掉入下一个分支的问题。
- 如果没有模式能够匹配,会抛出MatchError。可以用case_模式来避免。
- 模式可以包含一个随意定义的条件,称作守卫。
- 你可以对表达式的类型进行匹配;优先选择模式匹配而不是isInstanceOf
- 你可以匹配数组、元组和样例类的模式,然后将匹配到的不同部分绑定到变量。
- 在for表达式中,不能匹配的情况会被安静的跳过。
- 样例类是编译器会为之自动产出模式匹配所需方法的类。
- 样例类继承层级中公共超类应该是sealed的。
- 用Option来存放对于可能存在也可能不存在的值——这比null更安全。
语法格式
Scala一个模式匹配包含了一系列备选项,每个都开始于关键字case。每个备选项都包含了一个模式以及一到多个表达式。箭头符号=>隔开了模式和表达式。
代码示例:
object Test {
def main(args: Array[String]) {
println(matchTest(3))
}
def matchTest(x: Int): String = x match {
case 1 => "one"
case 2 => "two"
case _ => "many"
}
}
守卫
代码示例:
ch match {
case '+' => sign = 1
case '-' => sign = -1
case _ if Character.isDigit(ch) => digit = Character.digit(ch, 10)
case _ => sign = 0
}
注意:模式总是从上往下进行匹配。如果带守卫的这个模式不能匹配,则捕获所有的模式(case _) 会被用来尝试进行匹配。
类型匹配
你可以对表达式的类型进行匹配,例如:
//可以接受任意类型的对象
def generalType(obj: Any) = obj match {
//如果对象是Int类型,则把对象的值赋给x然后执行后的打印语句
case x: Int => println("Int : " + x)
//如果对象是String类型,则把对象的值赋给s然后执行后面的语句
case s: String => println("String : " + s)
case _: BigInt => println("BigInt")
//如果是除了上面类型外的其他类型,则直接输出"other"
case _ => println("other")
}
在Scala中,我们更倾向于使用这样的模式匹配,而不是使用isInstanceOf操作符。
注意:
- 模式中的变量名,匹配到的值将会被绑定到相应的变量。
- 匹配发生在运行期,Java虚拟机中泛型的类型信息是被擦掉的。因此,你不能用类型匹配来匹配特定的Map类型。
例如:
case m: Map[String, Int] => ... //这样做是不可取的
你可以匹配一个通用的映射:
case m: Map[_, _] => ... //OK
但是,对于数组而言元素的类型时完好的。你可以匹配到Array[Int]
不同类型数据的模式匹配:
object Test {
def main(args: Array[String]) {
println(matchTest("two"))
println(matchTest("test"))
println(matchTest(1))
println(matchTest(6))
}
def matchTest(x: Any): Any = x match {
case 1 => "one"
case "two" => 2
case y: Int => "scala.Int"
case _ => "many"
}
}
匹配数组、列表和元组
数组匹配
val array: Array[Int] = Array[Int](1, 2, 3, 4, 5, 6, 7, 8)
array match {
//匹配包含0的数组
case Array(0) => "0"
//匹配任何带有两个元素的数组,并绑定值到变量x,y
case Array(x, y) => x + " " + y
//匹配任何以零开始的数组
case Array(0, _*) => "0 ......"
case _ => "Something else"
}
列表匹配
你可以使用数组匹配的方法匹配列表,使用List表达式。或者,你也可以使用::操作符:
val list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8)
list match {
//匹配包含0的列表
case 0 :: Nil => "0"
//匹配任何带有两个元素的列表,并绑定值到x,y
case x :: y :: Nil => x + " " + y
//匹配任何以0开头的列表
case 0 :: tail => "0 ..."
case _ => "Something else"
}
元组匹配
val tuple = Tuple8(1, 2, 3, 4, 5, 6, 7, 8)
tuple match {
//匹配包含0的列表
case (0, _) => "0"
//匹配任何带有两个或以上元素的列表,并绑定值到x,y
case (x, y, _) => x + " : " + y
case _ => "Something else"
变量声明中的模式
经过上面的学习,你看到了模式是可以带变量的。你也可以在变量声明中使用这样的模式。
例如:
val (x, y) = (1, 2) //把1,2绑定到变量x,y
这样同时把x定义为1,把y定义为2。这对于那些返回对偶的函数而言很有用。
例如:
val (q, r) = BigInt(10) /% 3 //返回商和余数,并绑定值到q,r
/%方法返回包含商和余数的对偶,而这两个值分别被变量q和r捕获到。
数组的变量绑定:
val arr: Array[Int] = Array(1,2,3,4,5,6,7,8)
val Array(first, second, _*) = arr //绑定数组中第一,二个元素给first,second
for表达式中的模式
你可以在for推导式中使用变量的模式。对每一个遍历到的值,这些变量都会被绑定。这使得我们可以方便地遍历映射:
import scala.collection.JavaConversions.propertiesAsScalaMap
//将Java的Properties转换为Scala映射
for ((k, v) <- System.getProperties()) {
println(k + " -> " + v)
}
对映射中的每一个(键,值)对偶,k被绑定到键,而v被绑定到值。
在for推倒式中,失败的匹配将被安静地忽略。举例来说,如下循环将打印出所有值为空的键,跳过其他键:
for ((k, "") <- System.getProperties()) {
println(k)
}
另一种写法(使用守卫)
for ((k, v) <- System.getProperties() if v == "") {
println(k)
}
Scala提取器
示例
提取器是从传递给它的对象中提取出构造该对象的参数。
Scala 提取器是一个带有unapply方法的对象。unapply方法算是apply方法的反向操作:unapply接受一个对象,然后从对象中提取值,提取的值通常是用来构造该对象的值。
以下实例演示了邮件地址的提取器对象:
object Test {
def main(args: Array[String]) {
println("Apply 方法 : " + apply("Zara", "gmail.com"));
println("Unapply 方法 : " + unapply("Zara@gmail.com"));
println("Unapply 方法 : " + unapply("Zara Ali"));
}
// 注入方法 (可选)
def apply(user: String, domain: String) = {
user + "@" + domain
}
// 提取方法(必选)
def unapply(str: String): Option[(String, String)] = {
val parts = str split "@"
if (parts.length == 2) {
Some(parts(0), parts(1))
} else {
None
}
}
}
打印结果:
Apply 方法 : Zara@gmail.com
Unapply 方法 : Some((Zara,gmail.com))
Unapply 方法 : None
以上对象定义了两个方法: apply 和 unapply 方法。通过 apply 方法我们无需使用 new 操作就可以创建对象。所以你可以通过语句 Test("Zara", "gmail.com") 来构造一个字符串 "Zara@gmail.com"。
unapply方法算是apply方法的反向操作:unapply接受一个对象,然后从对象中提取值,提取的值通常是用来构造该对象的值。实例中我们使用 Unapply 方法从对象中提取用户名和邮件地址的后缀。
提取器中模式匹配
在我们实例化一个类的时,可以带上0个或者多个的参数,编译器在实例化的时会调用 apply 方法。我们可以在类和对象中都定义 apply 方法。
就像我们之前提到过的,unapply 用于提取我们指定查找的值,它与 apply 的操作相反。 当我们在提取器对象中使用 match 语句是,unapply 将自动执行,如下所示:
object Test {
def main(args: Array[String]) {
val x = Test(5)
println(x)
x match {
case Test(num) => println(x + " 是 " + num + " 的两倍!")
//unapply 被调用
case _ => println("无法计算")
}
}
def apply(x: Int) = x * 2
def unapply(z: Int): Option[Int] = if (z % 2 == 0) Some(z / 2) else None
}
打印结果:
10
10 是 5 的两倍!
样例类
使用了case关键字的类定义就是就是样例类(case classes),样例类是种特殊的类,经过优化以用于模式匹配。
以下是样例类的简单实例:
object Test {
def main(args: Array[String]) {
val alice = new Person("Alice", 25)
val bob = new Person("Bob", 32)
val charlie = new Person("Charlie", 32)
for (person <- List(alice, bob, charlie)) {
person match {
//匹配姓名为Alice,年龄为25的Person对象
case Person("Alice", 25) => println("Hi Alice!")
//匹配姓名为Bob,年龄为32的Person对象
case Person("Bob", 32) => println("Hi Bob!")
//匹配剩下的任何有且仅有name和age属性的Person对象
case Person(name, age) =>
println("Age: " + age + " year, name: " + name + "?")
}
}
}
// 样例类
case class Person(name: String, age: Int)
}
在声明样例类时,下面的过程自动发生了:
- 构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
- 在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
- 提供unapply方法使模式匹配可以工作;
- 生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
模拟枚举
样例类让你可以在Scala中模拟出枚举类型
代码示例:
object exam1 {
def main(args: Array[String]): Unit = {
//定义父抽象类
sealed abstract class TrafficLightColor
//创建样例类对象
case object Red extends TrafficLightColor
case object Yellow extends TrafficLightColor
case object Green extends TrafficLightColor
//循环打印匹配结果
val colors = List(Red, Yellow, Green)
for (color <- colors) {
println(matchColor(color))
}
//定义函数演示——模拟枚举
def matchColor(color: Any): String = color match {
case Red => "stop"
case Yellow => "hurry up"
case Green => "go"
case _ => "something Wrong"
}
}
}
打印结果:
stop
hurry up
Go
Option类型
标准库中的Option类型用样例类来表示那种可能存在、也可能不存在的值。样例子类Some包装了某个值,例如:Some(“Fred”)。而样例对象None则表示没有值。
Option支持泛型。举例来说,Some(“Fred”)的类型为Option[String]。
Map类的get方法返回一个Option。如果对于给定的键没有值,则get返回None,如果有值,则将结果包在Some中返回。
val scores = Map(("Alice", 88), ("Cindy", 71), ("Bob", 94))
scores.get("Alice") match {
case Some(score) => println(score)
case None => println("No Score")
}
打印结果:
88
偏函数
被包含在花括号内的一组case语句是一个偏函数——一个并非对所有输入值都有定义的函数。它是PartialFunction[A,B]类的一个实例。(A为参数类型,B为返回值类型)。该类有两个方法:apply方法从匹配到的模式计算函数值,而isDefinedAt方法在输入至少匹配其中一个模式时返回true。
代码示例:
val f: PartialFunction[Char, Int] = {
case '+' => 1;
case '-' => -1
}
//调用f.apply('-'),返回 -1
println(f('-'))
//返回false
println(f.isDefinedAt('0'))
//抛出MatchError异常
println(f('0'))