Scala match case 模式匹配

2019-05-04 17:04:06 | 编辑 | 添加

在Scala 的模式匹配中,可以使用类型、通配符、序列、正则表达式,甚至可以深入获取对象的状态。这种对象状态的获取遵循一定的协议,也就是对象内部状态的可见性由该类型的实现来控制,这使得我们能够轻易获取暴露的状态并应用于变量中。对象状态的获取往往被称为“提取”或“解构”。Scala match 中如果没有任何case匹配,则会抛出MatchError错误警告异常,所以你最好写个类似default来匹配其他值。


1.简单匹配

val bools = Seq(true, false)
for (bool <- bools) {
bool match {
case true => println("Got heads")
case false => println("Got tails")
}
}

这看起来就像是C 风格的case 语句。为了试验一下,你可以尝试将第二个case false 语句注释掉,再运行脚本。这时你会得到一个警告和一个错误消息:

<console>:12: warning: match may not be exhaustive.
It would fail on the following input: false
bool match {
Got heads
scala.MatchError: false (of class java.lang.Boolean)
at .<init>(<console>:11)
at .<clinit>(<console>)
...

由于序列类型存在两种可能的取值:true 或false,因此编译器警告match 语句未能覆盖所有可能的输入值。当尝试去匹配一个没有case 语句的值时,我们发现编译器抛出了MatchError


2 match中的变量和类型匹配

以下的例子能匹配特定的某个值,也能匹配特定类型的所有值,同时展示了default 语句的写法来匹配任意输入值。

for {
x <- Seq(1, 2, 2.7, "one", "two", 'four) // 由于序列元素包含不同类型,因此序列的类型为Seq[Any]。
} {
val str = x match { // x 的类型为Any。
case 1 => "int 1" // 如果x 等于1 则匹配。
case i: Int => "other int: "+i //  匹配除1 外的其他任意整数值。将x 的值安全地转为Int,并赋值给i。
case d: Double => "a double: "+x // 匹配所有Double 类型,x 的值被赋给Double 型变量d。
case "one" => "string one" // 匹配字符串“one”。
case s: String => "other string: "+s // 匹配除“one”外的其他任意字符串,x 的值被赋给了String 类型的变量x。
case unexpected => "unexpected value: " + unexpected // 匹配其他任意输入,x 的值被赋给unexpected 这个变量。由于未给出任何类型说明,unexpected 的类型被推断为Any,起到了default 语句的功能。
}
println(str) // 打印返回的字符串。
}



为了使代码直观一些,我将=>(“箭头”) 排成一列。以下是程序的输出:

int 1
other int: 2
a double 2.7
string one
other string: two
unexpected value: 'four

像if 推导式表达式一样,match 语句也会返回一个值。在这里,所有的子句都返回字符串,因此整个子句的返回值类型为String。编译器会推断所有case 子句返回值类型的最近公共父类型(也称为最小公共上限)作为返回值类型。


由于x 类型为Any,因此我们需要足够的子句来覆盖所有可能的输入值。(对比一下我们用来匹配Boolean 值的第一个例子。)这就是我们需要“default 子句”(使用unexpected)的原因。然而,编写偏函数时,我们不需要覆盖所有可能的类型,因为它们是被有意设计的。


匹配是按顺序进行的,因此具体的子句应该出现在宽泛的子句之前。否则,具体的语句将不可能有机会被匹配上。所以,默认子句必须是最后一个子句。幸运的是,编译器能识别这种类型的错误。


由于舍入误差的存在,两个看似相等的值可能由于最后一位有效数字的不同而被判断为不相等,我没有在示例中使用匹配浮点数字面量的子句。

以下是对之前例子的简单变形:


for {
x <- Seq(1, 2, 2.7, "one", "two", 'four)
} {
val str = x match {
case 1 => "int 1"
case _: Int => "other int: "+x
case _: Double => "a double: "+x
case "one" => "string one"
case _: String => "other string: "+x
case _ => "unexpected value: " + x
}
println(str)
}


我们用占位符_ 替换了变量i、d、s 和unexpected。事实上我们并不需要这些类型的对应变量值,只需要产生字符串。所以,可以在所有子句中使用x。


除了偏函数,所有的match 语句都必须是完全覆盖所有输入的。当输入类型为Any 时,在结尾用case _ 或case some_name 作为默认子句。


3.case匹配值中,编译器以大写字母开头的为类型名,以小写字母开头的为变量名

编写case 子句时,有一些规则和陷阱需要注意。在被匹配或提取的值中,编译器假定以大写字母开头的为类型名,以小写字母开头的为变量名。以下示例中的这条规则可能会使你感到惊讶:

def checkY(y: Int) = {
for {
x <- Seq(99, 100, 101)
} {
val str = x match {
case y => "found y!"
case i: Int => "int: "+i
}
println(str)
}
}
checkY(100)

在第一个case 子句中,我们希望它能匹配上一个可以由我们来指定的值,而不是一个硬编码的值。所以,我们可能会希望第一个case 子句在x 等于y 时成功匹配,y 的值为100。脚本执行时将产生以下输出:

int: 99
found y!
int: 101

以下是我们获得的实际输出:

<console>:12: warning: patterns after a variable pattern cannot match (SLS 8.1.1)
If you intended to match against parameter y of method checkY, you must use
backticks, like: case `y` =>
case y => "found y!"
<console>:13: warning: unreachable code due to variable pattern 'y' on line 12
case i: Int => "int: "+i
<console>:13: warning: unreachable code
case i: Int => "int: "+i
checkY: (y: Int)Unit
found y!
found y!
found y!


case y 的含义其实是:匹配所有输入(由于这里没有类型注解),并将其赋值给新的变量y。


这里的y 没有被解释为方法参数y。因此,事实上我们将一个默认的、匹配一切的语句写在了第一个,导致系统给出了这条“变量型匹配语句”会匹配一切输入的警告。我们的代码也从未执行到第二条case 语句,于是就得到了两条关于不可达代码的警告。第一条错误信息已经告诉我们应该怎么做:使用反引号表示真正想要匹配的是参数y的值。


def checkY(y: Int) = {
    for {
      x <- Seq(99, 100, 101)
    } {
      val str = x match {
        case `y` => "found y!" // 只修改了这一行: `y`
        case i: Int => "int: "+i
      }
      println(str)
    }
  }
  checkY(100)

这时,输出就符合我们的期望了。


在case 子句中,以小写字母开头的标识符被认为是用来提取待匹配值的新变量。如果需要引用之前已经定义的变量时,应使用反引号将其包围。与此相对,以大写字母开头的标识符被认为是类型名称。


4.Scala match case逻辑或运算符 匹配

有时不同的匹配子句需要使用相同的处理代码,此时,为了避免代码冗余,我们可以将相同处理代码重构为一个单独的方法。同时,case 子句也持“或”逻辑,使用| 方法即可:

for {
    x <- Seq(1, 2, 2.7, "one", "two", 'four)
  } {
    val str = x match {
      case _: Int | _: Double => "a number: "+x
      case "one" => "string one"
      case _: String => "other string: "+x
      case _ => "unexpected value: " + x
    }
    println(str)
  }

现在,Int 和Double 类型的值都能匹配上第一个case 子句了。