3.8 使用一个case语句匹配复杂条件
- 几个匹配条件要求执行相同的业务逻辑,而不是使用多个case重复业务逻辑,想要使用的是匹配条件的业务逻辑的复制。
3.8.1 解决方案
使用 | 分隔符将相同的业务逻辑的匹配条件放置到同一行上
val i = 5 i match { case 1 | 3 | 5 | 7 | 9 => println("odd") case 2 | 4 | 6 | 8 | 10 => println("even") }
其他类型也适用:
val cmd = "stop" cmd match { case "start" | "go" => println("starting") case "stop" | "quit" | "exit" => println("stopping") case _ => println("doing nothing") }
匹配多个case对象:
trait Command case object Start extends Command case object Go extends Command case object Stop extends Command case object Whoa extends Command def executeCommand(cmd: Command) = cmd match { case Start | Go => start() case Stop | Whoa => stop() }
3.9 分配匹配表达式结果给变量
- 问题:从匹配表达式返回值然后分配给一个变量,或者使用匹配表达式作为一个方法的内容。
3.9.1 解决方案
在匹配表达式前插入变量:
val evenOrOdd = someNumber match { case 1 | 3 | 5 | 7 | 9 => println("odd") case 2 | 4 | 6 | 8 | 10 => println("even") }
这种方式一般用来创建短方法和函数
def isTrue(a: Any) = a match { case 0 | "" => false case _ => true }
- 查看更多
3.10 获取匹配表达式中默认case的值
- 问题:想要获取默认值,但是使用_下划线进行匹配时不能获取值。
3.10.1 解决方案
给默认case分配一个变量名代替使用_下划线,可以在语句的右边获取变量
i match { case 0 => println("1") case 1 => println("2") case default => println("You gave me: " + default) }
3.10.2 讨论
- 本章关键是使用一个变量名代表默认匹配,代替使用下划线_字符
分配的名字可以是任何合法的变量名:
i match { case 0 => println("1") case 1 => println("2") case whoa => println("You gave me: " + whoa) }
必须提供一个默认匹配,不提供会产生MatchError错误:
scala> 3 match { | case 1 => println("one") | case 2 => println("two") | // no default match | } scala.MatchError: 3 (of class java.lang.Integer) many more lines of output ...
3.11 在匹配表达式里使用模式匹配
- 问题:需要在一个匹配表达式里匹配一个或者多个模式,并且模式可能是常量模式,变量模式,构造函数模式,序列模式,元组模式,或者类型模式
3.11.1 解决方案
给想要匹配的每个模式定义一个case语句:
def echoWhatYouGaveMe(x: Any): String = x match { // constant patterns case 0 => "zero" case true => "true" case "hello" => "you said 'hello'" case Nil => "an empty List" // sequence patterns case List(0, _, _) => "a three-element list with 0 as the first element" case List(1, _*) => "a list beginning with 1, having any number of elements" case Vector(1, _*) => "a vector starting with 1, having any number of elements" // tuples case (a, b) => s"got $a and $b" case (a, b, c) => s"got $a, $b, and $c" // constructor patterns case Person(first, "Alexander") => s"found an Alexander, first name = $first" case Dog("Suka") => "found a dog named Suka" // typed patterns case s: String => s"you gave me this string: $s" case i: Int => s"thanks for the int: $i" case f: Float => s"thanks for the float: $f" case a: Array[Int] => s"an array of int: ${a.mkString(",")}" case as: Array[String] => s"an array of strings: ${as.mkString(",")}" case d: Dog => s"dog: ${d.name}" case list: List[_] => s"thanks for the List: $list" case m: Map[_, _] => m.toString // the default wildcard pattern case _ => "Unknown" }
测试上面的匹配表达式:
object LargeMatchTest extends App { case class Person(firstName: String, lastName: String) case class Dog(name: String) // trigger the constant patterns println(echoWhatYouGaveMe(0)) println(echoWhatYouGaveMe(true)) println(echoWhatYouGaveMe("hello")) println(echoWhatYouGaveMe(Nil)) // trigger the sequence patterns println(echoWhatYouGaveMe(List(0,1,2))) println(echoWhatYouGaveMe(List(1,2))) println(echoWhatYouGaveMe(List(1,2,3))) println(echoWhatYouGaveMe(Vector(1,2,3))) // trigger the tuple patterns println(echoWhatYouGaveMe((1,2))) // two element tuple println(echoWhatYouGaveMe((1,2,3))) // three element tuple // trigger the constructor patterns println(echoWhatYouGaveMe(Person("Melissa", "Alexander"))) println(echoWhatYouGaveMe(Dog("Suka"))) // trigger the typed patterns println(echoWhatYouGaveMe("Hello, world")) println(echoWhatYouGaveMe(42)) println(echoWhatYouGaveMe(42F)) println(echoWhatYouGaveMe(Array(1,2,3))) println(echoWhatYouGaveMe(Array("coffee", "apple pie"))) println(echoWhatYouGaveMe(Dog("Fido"))) println(echoWhatYouGaveMe(List("apple", "banana"))) println(echoWhatYouGaveMe(Map(1->"Al", 2->"Alexander"))) // trigger the wildcard pattern println(echoWhatYouGaveMe("33d")) }
输出如下
zero true you said 'hello' an empty List a three-element list with 0 as the first element a list beginning with 1 and having any number of elements a list beginning with 1 and having any number of elements a vector beginning with 1 and having any number of elements a list beginning with 1 and having any number of elements got 1 and 2 got 1, 2, and 3 found an Alexander, first name = Melissa found a dog named Suka you gave me this string: Hello, world thanks for the int: 42 thanks for the float: 42.0 an array of int: 1,2,3 an array of strings: coffee,apple pie dog: Fido thanks for the List: List(apple, banana) Map(1 -> Al, 2 -> Alexander) you gave me this string: 33d
在匹配表达式中,List和Map语句写成如下:
case list: List[_] => s"thanks for the List: $list" case m: Map[_, _] => m.toString
也可以使用下面方式替代:
case list: List[x] => s"thanks for the List: $list" case m: Map[a, b] => m.toString
- 更偏爱使用下划线方法,因为代码清晰不需要关心存储在List和Map中的值是什么。当然有时需要知道存储在List和Map中的值是什么,但是由于JVM中的类型擦除,那将是一个困难的问题。
如果List表达式写成如下:
case l: List[Int] => "List"
如果熟悉java平台的类型擦除,就会知道这个例子不会工作,Scala编译器会有如下警告:
Test1.scala:7: warning: non-variable type argument Int in type pattern List[Int] is unchecked since it is eliminated by erasure case l: List[Int] => "List[Int]" ^
- 如果对类型擦除不熟悉,请移步查看更多
3.11.2 讨论
- 通常使用这种技术时,你的方法将期待一个继承基类或者特性的实例,然后case语句将调用那个基类的子类。如echoWhatYouGaveMe方法中,每个Scala的类型都是Any的子类。
在 Blue Parrot application,以随机间隔播放音乐文件或者朗读文档,有以下方法:
import java.io.File sealed trait RandomThing case class RandomFile(f: File) extends RandomThing case class RandomString(s: String) extends RandomThing class RandomNoiseMaker { def makeRandomNoise(t: RandomThing) = t match { case RandomFile(f) => playSoundFile(f) case RandomString(s) => speak(s) } }
- makeRandomNoise方法声明采用RandomThing类型做参数,然后匹配表达式处理两个子类,RandomFile和RandomString。
3.11.3 模式
常量模式:常量模式只能匹配自己,任何文本都可以作为常量,如果指定0作为文本,那么只有整形 0 将会匹配
case 0 => "zero" case true => "true"
变量模式:上面例子中没有展示变量模式,在3.10中进行了讨论,变量模式如下划线字符一样匹配任何对象。Scala绑定变量为任何对象,在case语句的右边可以使用这个变量。
case _ => s"Hmm, you gave me something ..." //使用变量模式代替上面 case foo => s"Hmm, you gave me a $foo"
构造函数模式:可以在case语句中匹配构造函数,可以根据构造函数需要指定常量和变量模式:
case Person(first, "Alexander") => s"found an Alexander, first name = $first" case Dog("Suka") => "found a dog named Suka"
序列模式:可以匹配List,Array,Vector等序列。使用下划线 字符代表序列的一个元素,使用 * 代表 “0或多个字符”:
case List(0, _, _) => "a three-element list with 0 as the first element" case List(1, _*) => "a list beginning with 1, having any number of elements" case Vector(1, _*) => "a vector beginning with 1 and having any number …"
元组模式:匹配元祖模式并且获取元祖中每个元素的值,如果不关心一个元素的值可以使用下划线 _ 代替:
case (a, b, c) => s"3-elem tuple, with values $a, $b, and $c" case (a, b, c, _) => s"4-elem tuple: got $a, $b, and $c"
类型模式:下面的例子中str: String是一个类型模式,str是一个模式变量,可以在声明之后在表达式右边获取模式变量:
case str: String => s"you gave me this string: $str"
3.11.4 给模式添加变量
通过以下语法给模式添加变量
variableName @ pattern
- 如 Programming in Scala 这本书描述,“这个提供了一个变量绑定模式。这样一个模式的意义是正常执行模式匹配,如果模式成功,就像一个简单变量模式设置变量给匹配对象”。
下面通过演示问题解决来说明用处,假设有一个List模式:
case List(1, _*) => "a list beginning with 1, having any number of elements"
上面例子无法再表达式右侧获取list。想要获取list时,可以用下面方法:
case list: List[_] => s"thanks for the List: $list"
所以看上去应该尝试用一个序列模式:
case list: List(1, _*) => s"thanks for the List: $list" //编译错误 Test2.scala:22: error: '=>' expected but '(' found. case list: List(1, _*) => s"thanks for the List: $list" ^ one error found
解决方案是在序列模式添加一个绑定变量模式
case list @ List(1, _*) => s"$list"
下面更多演示:
case class Person(firstName: String, lastName: String) object Test2 extends App { def matchType(x: Any): String = x match { //case x: List(1, _*) => s"$x" // doesn't compile case x @ List(1, _*) => s"$x" // works; prints the list //case Some(_) => "got a Some" // works, but can't access the Some //case Some(x) => s"$x" // works, returns "foo" case x @ Some(_) => s"$x" // works, returns "Some(foo)" case p @ Person(first, "Doe") => s"$p" // works, returns "Person(John,Doe)" } println(matchType(List(1,2,3))) // prints "List(1, 2, 3)" println(matchType(Some("foo"))) // prints "Some(foo)" println(matchType(Person("John", "Doe"))) // prints "Person(John,Doe)" }
3.11.5 匹配表达式中使用Some和None
可能会经常在匹配表达式中使用Some和None。假定有一个toInt方法定义:
def toInt(s: String): Option[Int] = { try { Some(Integer.parseInt(s.trim)) } catch { case e: Exception => None } }
使用方法:
toInt("42") match { case Some(i) => println(i) case None => println("That wasn't an Int.") }
查看更多
3.12 在匹配表达式中使用Case类
- 问题:想要在匹配表达式中匹配不同的case类或case对象,比如在actor里接收信息时。
3.12.1 解决方案
使用不同的模式匹配case类和对象
trait Animal case class Dog(name: String) extends Animal case class Cat(name: String) extends Animal case object Woodpecker extends Animal object CaseClassTest extends App { def determineType(x: Animal): String = x match { case Dog(moniker) => "Got a Dog, name = " + moniker case _:Cat => "Got a Cat (ignoring the name)" case Woodpecker => "That was a Woodpecker" case _ => "That was something else" } println(determineType(new Dog("Rocky"))) println(determineType(new Cat("Rusty the Cat"))) println(determineType(Woodpecker)) } //输出 Got a Dog, name = Rocky Got a Cat (ignoring the name) That was a Woodpecker
3.13 Case语句中添加if表达式
- 问题:match表达式中给case语句添加限定逻辑,比如数字范围或者匹配模式,只要模式匹配一些额外的标准
3.13.1 解决方案
case语句中添加if判断
//匹配数字范围 i match { case a if 0 to 9 contains a => println("0-9 range: " + a) case b if 10 to 19 contains b => println("10-19 range: " + b) case c if 20 to 29 contains c => println("20-29 range: " + c) case _ => println("Hmmm...") } //匹配一个对象的不同值 num match { case x if x == 1 => println("one, a lonely number") case x if (x == 2 || x == 3) => println(x) case _ => println("some other value") }
可以在if判断里引用类字段,假设如下x是Stock类的实例,并且有symbol和price字段
stock match { case x if (x.symbol == "XYZ" && x.price < 20) => buy(x) case x if (x.symbol == "XYZ" && x.price > 50) => sell(x) case _ => // do nothing }
可以从case类里提取字段并应用在if判断里
def speak(p: Person) = p match { case Person(name) if name == "Fred" => println("Yubba dubba doo") case Person(name) if name == "Bam Bam" => println("Bam bam!") case _ => println("Watch the Flintstones!") }
3.13.2 讨论
注意所有的例子都可以如下把if判断放在表达式的右侧:
case Person(name) => if (name == "Fred") println("Yubba dubba doo") else if (name == "Bam Bam") println("Bam bam!")
- 但是在很多情况下,为了代码更简洁易读会直接在case语句中加入if判断,而不是右边
3.14 使用一个match表达式代替isInstanceOf
- 问题:需要匹配一个类型或者多个不同的类型
3.14.1 解决方案
可以使用isInstanceOf方法测试一个对象的类型
if (x.isInstanceOf[Foo]) { do something ...
然后一些编程开发人员并不鼓励使用这种方法,并且在其他情况下会更加复杂。下面的例子中可以在一个match表达式中处理不同的类型
//确定对象是否是Person的实例 def isPerson(x: Any): Boolean = x match { case p: Person => true case _ => false } //处理不同的子类 trait SentientBeing trait Animal extends SentientBeing case class Dog(name: String) extends Animal case class Person(name: String, age: Int) extends SentientBeing // later in the code ... def printInfo(x: SentientBeing) = x match { case Person(name, age) => // handle the Person case Dog(name) => // handle the Dog }
3.14.2 讨论
- 匹配多个类型时使用match表达式替代isInstanceOf是自然而然的事
简单例子中,isInstanceOf方法匹配一个对象更简单点:
if (o.isInstanceOf[Person]) { // handle this ...
- 但是伴随更多需求,match表达式比if/else语句可读性更高
3.15 在match表达式中使用List
- 问题:List数据结构和其他集合数据结构有所不同,它从Cons单元(Cons cells)创建,结束于一个Nil元素。match表达式中使用这个会更好,比如写一个递归函数时。
3.15.1 解决方案
创建一个List
val x = List(1, 2, 3)
使用cons单元和一个Nil元素创建List
val y = 1 :: 2 :: 3 :: Nil
写一个递归算法时,可以利用List的最后一个元素是一个Nil对象:
def listToString(list: List[String]): String = list match { case s :: rest => s + " " + listToString(rest) case Nil => "" }
REPL中运行:
scala> val fruits = "Apples" :: "Bananas" :: "Oranges" :: Nil fruits: List[java.lang.String] = List(Apples, Bananas, Oranges) scala> listToString(fruits) res0: String = "Apples Bananas Oranges "
这种一个case处理Nil,一个case处理List其他部分的方法也可以用在List的其他类型上:
def sum(list: List[Int]): Int = list match { case Nil => 1 case n :: rest => n + sum(rest) } def multiply(list: List[Int]): Int = list match { case Nil => 1 case n :: rest => n * multiply(rest) }
REPL中运行
scala> val nums = List(1,2,3,4,5) nums: List[Int] = List(1, 2, 3, 4, 5) scala> sum(nums) res0: Int = 16 scala> multiply(nums) res1: Int = 120
3.15.2 讨论
记住必须处理Nil这个case,不然会报错:
REPL中:
warning: match is not exhaustive! (完整)
正式项目中:
Exception in thread "main" scala.MatchError: List() (of class scala.collection.immutable.Nil$)
3.16 在try/catch中匹配一个或更多异常
- 问题:在一个try/catch语句块中捕获一个或者更多异常
3.16.1 解决方案
Scala的try/catch/finally语法和Java很像,但是他在catch语句块中使用match表达式:
val s = "Foo" try { val i = s.toInt } catch { case e: Exception => e.printStackTrace }
捕获更多异常:
try { openAndReadAFile(filename) } catch { case e: FileNotFoundException => println("Couldn't find that file.") case e: IOException => println("Had an IOException trying to read that file") }
3.16.2 讨论
如果不关心具体异常,想要捕获所有异常并进行处理:
try { openAndReadAFile("foo") } catch { case t: Throwable => t.printStackTrace() }
捕获所有并且忽略不进行信息处理:
try { val i = s.toInt } catch { case _: Throwable => println("exception ignored") }
在Java中,可以从一个catch语句抛出一个异常。但是因为Scala没有已检查的异常,不需要指定一个方法抛出异常。下面演示方法不使用注解:
// nothing required here def toInt(s: String): Option[Int] = try { Some(s.toInt) } catch { case e: Exception => throw e }
如果喜欢声明方法抛出的异常或者需要和Java进行交互,添加@throws注解
@throws(classOf[NumberFormatException]) def toInt(s: String): Option[Int] = try { Some(s.toInt) } catch { case e: NumberFormatException => throw e }
3.17 在try/catch/finally语句块里使用变量之前先声明它
- 问题:想要在try语句里使用一个对象,然后在finally部分获取这个对象,例如需要在一个对象上调用一个关闭的方法。
3.17.1 解决方案
在try/catch语句块之前声明字段为Option类型,然后在try语句内创建一个Some:
import java.io._ object CopyBytes extends App { var in = None: Option[FileInputStream] var out = None: Option[FileOutputStream] try { in = Some(new FileInputStream("/tmp/Test.class")) out = Some(new FileOutputStream("/tmp/Test.class.copy")) var c = 0 while ({c = in.get.read; c != −1}) { out.get.write(c) } } catch { case e: IOException => e.printStackTrace } finally { println("entered finally ...") if (in.isDefined) in.get.close if (out.isDefined) out.get.close } }
- 通常会告诉别人不要使用Option的get和isDefined方法,但是这是为数不多的一次认为这种使用是可以接受的,并且代码更可读
另一种方法是使用foreach方法:
try { in = Some(new FileInputStream("/tmp/Test.class")) out = Some(new FileOutputStream("/tmp/Test.class.copy")) in.foreach { inputStream => out.foreach { outputStream => var c = 0 while ({c = inputStream.read; c != −1}) { outputStream.write(c) } } } } // ...
- 上面方法两个变量时仍然可读,并且避免了get方法的调用。但是更多变量时并不实用。
3.17.2 讨论
声明Option字段的关键点在于没有初始化赋值:
var in = None: Option[FileInputStream] var out = None: Option[FileOutputStream]
可以通过以下方式理解:
var x has No Option[yeT] var x = None: Option[Type]
下面演示声明变量为null的处理,但是Scala中完全不需要考虑null值,所以下面的方法并不推荐:
// (1) declare the null variables var store: Store = null var inbox: Folder = null try { // (2) use the variables/fields in the try block store = session.getStore("imaps") inbox = getFolder(store, "INBOX") // rest of the code here ... } catch { case e: NoSuchProviderException => e.printStackTrace case me: MessagingException => me.printStackTrace } finally { // (3) call close() on the objects in the finally clause if (inbox != null) inbox.close if (store != null) store.close }
- 查看更多
3.18 创建自己的控制结构体
- 问题:自定义控制结构体改善Scala语言,简化代码,或者创建DSL给其他人使用
3.18.1 解决方案
- Scala语言创建者有意不实现一些关键词,反而通过Scala库实现功能。比如3.5章节的“Implementing break and continue”,尽管Scala语言没有break和continue关键词,同样可以通过库里的方法实现同样的功能。
下面例子演示,假设不喜欢while循环,创建自己的whilst循环,可以像下面方法使用:
package foo import com.alvinalexander.controls.Whilst._ object WhilstDemo extends App { var i = 0 whilst (i < 5) { println(i) i += 1 } }
- 创建whilst控制结构体,定义一个叫做whilst的函数并且需要两个参数,第一个参数处理测试条件(i < 5),第二个参数运行用户代码块。
可以通过包裹while操作来实现方法:
// 1st attempt def whilst(testCondition: => Boolean)(codeBlock: => Unit) { while (testCondition) { codeBlock } }
更好的方法是不调用while:
package com.alvinalexander.controls import scala.annotation.tailrec object Whilst { // 2nd attempt @tailrec def whilst(testCondition: => Boolean)(codeBlock: => Unit) { if (testCondition) { codeBlock whilst(testCondition)(codeBlock) } } }
3.18.2 讨论
第二个例子中使用了递归调用,但是在其他简单例子中不需要递归。假设需要一个执行两个条件判断的控制结构体。如果两个都为true,则运行代码块。表达式可能如下方法调用:
doubleif(age > 18)(numAccidents == 0) { println("Discount!") }
定义一个三个参数的函数:
// two 'if' condition tests def doubleif(test1: => Boolean)(test2: => Boolean)(codeBlock: => Unit) { if (test1 && test2) { codeBlock } }
- 查看更多