Scala Cookbook读书笔记 Chapter 3.Control Structures 第二部分

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
        }
    }
    
  • 查看更多