《Scala 程序设计》学习笔记 Chapter 4:模式匹配

  • Scala 模式匹配支持获取对象状态;获取对象状态的操作往往称为“提取”或“解构”。[P86]

match 中的值、变量和类型

  • 可以使用一个 Any 类的变量放到最后来充当 default 。[P88]

  • 编译器会自动推断所有 case 子句返回值类型的最近公共父类型。[P88]

  • 在被匹配或提取的值中,编译器假定以大写字母开头的为类型名,以小写字母开头的为变量名。[P89]

  • 注意一下模式匹配的变量含义与作用域:[P89]

    def checkY(y: Int) = {
        for {
            x <- Seq(1, 2, 3)
        } {
            val str = x match {
                case y => "found y!" // 错误:并不是与变量 y 的值匹配,而是声明了一个 Any 类型的 y,这样会收到系统警告。
                case i: Int => "int: " + i
            }
            println(str)
        }
    }
    checkY(1)
    

    使用 `` 包围变量以引用已经定义的变量。

    def checkY(y: Int) = {
        for {
            x <- Seq(1, 2, 3)
        } {
            val str = x match {
                case `y` => "found y!" // 正确
                case i: Int => "int: " + i
            }
            println(str)
        }
    }
    checkY(1)
    
  • 逻辑或语法:case _: Int | _: Double => ...

序列的匹配

  • 序列的基础语法 [P91]

    • 使用 .empty[A] 构造空序列。
    • 任意类型的空序列 用 Nil 表示。
  • 关于 Seq 的特殊语法:[P91 - 92]

    def seqToString[T](seq: Seq[T]): String = seq match {
        case head +: tail => s"$head +: " + seqToString(tail) // 1
        case Nil => "Nil"
    }
    
    1. +: 是“构造” 操作符,以 : 结尾的方法向右结合,即向 Seq 的尾部结合。headtailSeq 自带的两个方法,但在这里,按照惯例被解释为一个变量,作用是提取一个非空序列的头部(第一个元素)和尾部(除了第一个元素外的其他元素)。
  • Map 不是 Seq 的子类型,要通过 map.toSeq 生成 seq。[P92]

case 中的 guard 语句 [P95]

for ( i <- Seq(1, 2, 3, 4)) {
    i match {
        case _ if i % 2 == 0 => println(s"even: $i")
        case _ => println(s"odd: $i")
    }
}

case 类的匹配

  • 使用 Seq.zipWithIndex 方法将一个 Seq 连同序号一起打印出来:[P95 -96]

    val itemsCosts = Seq(("Pencil", 0.52), ("Paper", 1.35))
    val itemsCostsIndices = itemsCosts.zipWithIndex
    for (itemCostIndex <- itemsCostsIndices) {
        itemCostIndex match {
            case ((item, cost), index) => println(s"$index: $item costs $cost each")
        }
    }
    

    调用 zipWithIndex 返回的元组形式为 ((name, cost), index)

unapply 方法

  • unapply 方法用于提取和解构。当在 match 中对 case 类使用诸如 case Person("Alice", 25, Address(_, "Chicago", _)) => ... 之类的语法时,会调用其 unapply 方法。(当然在本例中 Address 也需要 unapply 方法)[P96]

  • unapply 方法的一种定义:[P96 - 97]

    def unapply(P: Person): Option[Tuple3[String, Int, Address]] = 
        Some((p.name, p.age, p.address))
    
    • Option 的原因是,unapply 方法可以选择“否决”这个请求,返回 None 详见 unapplySeq

    • 从 Scala 2.11.1 开始,unapply 方法可以返回任意类型�,只要该类型具有以下方法:

      def isEmpty: Boolean
      def get: T
      
  • 有必要时,unapply 会被递归调用(比如本例中的 Address )。[P97]

  • 元组字面量语法:[P97 -> P50]

    val t1 = Option[Tuple3[String, Int, Address]] = ...
    val t2 = Option[(String, Int, Address)] = ...
    val t3 = Option[ (String, Int, Address) ] = ... // 更容易阅读
    
  • unapply 支持任意非空集合:使用 :+ [P97 - 98]

    • :+ 是一个单例对象,它的 unapply 使用以下语法:

      def unapply[T, Coll](collection: Coll): Option[(T, Coll)]
      

      但是这样的 unapply 使用的调用方法为:case +: (head, tail) => ...
      可以写成如下形式:

      def processSeq2[T](l: Seq[T]): Unit = l match {
          case +: (head, tail) => 
              println("%s +: ", head)
              processSeq2(tail)
          case Nil => println("Nil")
      }
      

      当然,也可以使用 head +: tail ,这是编译器提供的语法糖。同样的语法糖还有:

      case class With[�A, B](a: A, b: B)
      val with1: With[Stirng, Int] = With("Foo", 1)
      val with2: With[String, Int] = With("Bar", 2)
      Seq(with1, with2) foreach {
          w match { 
              case s With i => println(s"$s with $i)
              case _ => println(s"Unknown $w")
          }
      }
      

      但是,同样的语法不能用于初始化

    • 使用 :+ 逆序处理一个序列。[P99]

      def reverseSeqToString[T](l: Seq[T]): String = l match {
          case prefix :+ end => reverseSeqToString(prefix) + s" :+ $end"
          case Nil => "Nil"
      }
      
  • 补充:对于 List:+ / +: 需要 O(n) 的时间复杂度,对于 Vector 之类的其他某些序列,只需要 O(1) 的时间复杂度。[P99]

unapplySeq 方法

  • 除了 apply 方法外,Seq 的伴随对象还实现了 unapplySeq 方法:[P100]

    def apply[A](elems: A*): Seq[A]
    def unapplySeq[A](x: Seq[A]): Some[Seq[A]]
    

    case 中,使用如下语法调用 unapplySeq

    def windows[T](seq: Seq[T]): String = seq match {
        case Seq(head1, head2, _*) => s"($head1, $head2), " +����� windows(seq.tail)
        case ...
        ...
    }
    
  • 当然也可以使用 +: 语法:[P101]

    def windows2[T](seq: Seq[T]): String = seq match {
        case head1 +: head2 +: tail => s"($head1, $head2), " +����� windows2(seq.tail)
        case ...
        ...
    }
    
  • Seqsliding 方法:[P101]

    • 返回一个“惰性”迭代器。对这个迭代器调用 toSeq 方法,可以将迭代器转为一个 collection.immutable.Stream (一个惰性列表,创建时即对列表的头部元素求值,但只在需要的时候才会对列表的尾部元素求值。toList 会在创建时对所有元素求值)。

可变参数列表的匹配

  • 使用 name @ _* 匹配可变参数:[P102]

    case WhereIn(col, val1, vals @ _*) => ...
    

正则表达式匹配

  • 使用 .r 方法生成正则表达式。[P103]

    val BookExtractorRE = """Book: title=([^,]+),\s+author=(.+)""".r
    

    match 中使用

    case BookExtractorRE(title, author) => ...
    
  • 使用三重引号表示正则表达式字符串的原因是可以不用对正则中的 \ 等符号单独进行转义。[P103]

  • 在三重引号内的正则表达式中使用变量插值是无效的,如果使用了变量插值,就需要对 \ 等符号进行转义操作。[P103]

  • scala.util.matching.Regex

再谈 case 语句的变量绑定

  • name @ object 语法:[P104]

    person match {
        case p @ Person("Alice", 25, address) => ...
        case p @ Person("Bob", 29, a @ Addres(street, city, country)) => ...
    }
    

    p @ ... 的语法将整个 Person 类的实例赋值给了变量 p 。如果不需要从 Person 实例中提取属性值,只要写为 p: Person => ... 就可以了。

再谈类型匹配

  • JVM 类型擦除:为了避免与旧版本代码断代,JVM 的字节码不会记住一个泛型实例(如 List )中实际传入的类型与参数信息。所以在 match 中,不能区分 Seq[String]Seq[Double] ,要自定义匹配函数:

    x match {
        case seq: Seq[_] => (s"seq ${doSeqMatch(seq)}", seq)
        case _ => ("Unknown!", x)
    }
    def doSeqMatch[T](seq: Seq[T]): String = seq match {
        case Nil => "Noting"
        case head +: _ => head match {
            case _: Double => "Double"
            case _: String => "String"
            case _ => "Unmatched seq element"
        }
    }
    

封闭继承层级与全覆盖匹配

  • 如果类型的继承层级可能发生变化,就应当避免使用 sealed 。[P107]

  • 在父类型中,不带参数的抽象方法可以再子类中用 val 变量实现。推荐的做法是:在抽象父类型中声明一个不带参数的抽象方法,这样就给子类型如何具体实现该方法留下了巨大的自由,既可以用方法实现,又可以用 val 变量实现。[P107]

    sealed abstract class HttpMethod() {
        def body: String
        def bodyLength: body.length
    }
    
  • 编译器无法判断 Enumeration 相应的 match 语句是否全覆盖。[P107]

模式匹配的其他用法

  • 定义变量:

    val Person(name, age, Address(_, state, _)) = Person("Dean", 29, Address("1 Scala Way", "CA", "USA))
    // 得到 name, age, state;
    
    val head +: tail = List(1, 2, 3)
    // head: Int = 1
    // tail: List[Int] = List(2, 3)
    
    val Seq(a, b, c) = List(1, 2, 3)
    // 得到 a, b, c
    
    val Seq(a, b, c) = List(1, 2, 3, 4) // MatchError
    
  • if 中也可以使用模式匹配,但不能用 _ 占位符。[P108]

    val p = Person("Dean", 29, Address("1 Scala Way", "CA", "USA"))
    if (p == Person("Dean", 29, Address("1 Scala Way", "CA", "USA"))) "yes" else "no" // "yes"
    
  • Scala 对一些非字母数字的字符做了”字符映射“,使得他们符合 JVM 规范。比如: = 会被映射为 $eq 。[�P108]

  • 元组:[P109]

    def sum_count(ints: Seq[Int]) = (ints.sum, ints.size)
    val (sum, count) = sum_count(List(1, 2, 3, 4, 5))
    // sum: Int = 15
    // count: Int = 5
    
  • 在带复杂参数的函数字面量中使用:[P109 - 110]

    case class Address(street: String, city: String, country: String)
    case class Person(name: String, age: Int)
    val as = Seq(
        Address("1 Scala Lane", "Anytown", "USA"),
        Address("2 Clojure Lane", "Othertown", "USA"))
    val ps = Seq(
        Person("Buck Trends", 29)
        Person("Clo Jure", 28)
    )
    val pas = ps zip as // Seq[(Person, Address)]
    pas map {
        case (Person(name, age), Address(street, city, country)) => 
            s"$name (age: $age) lives at $street, $city, in $country"
    }
    
  • 在正则表达式中使用模式匹配去解构字符串:[P110]

    val cols = """\*|[\w, ]+"""
    val table = """\w+"""
    val tail = """.*"""
    val selectRE = s"""SELECT\\s*(DISTINCT)?\\s+($cols)\\s*FROM\\s+($table)\\s*($tail)?;""".r
    
    val selectRE(distinct1, cols1, table1, otherClauses) = "SELECT DISTINCT * FROM atable;"
    /*
        distinct1: String = DISTINCT
        cols1: String = *
        table1: String = atable
        otherClauses: String = ""
    */
    
    val selectRE(distinct2, cols2, table2, otherClauses) = "SELECT col1, col2 FROM atable;"
    /*
        distinct1: String = null
        cols1: String = "col1, col2"
        table1: String = atable
        otherClauses: String = ""
    */
    

    由于使用了变量插值,在正则表达式字符串中必须增加 \ 转义。

  • 要谨慎对待默认 case 子句:什么情况下才应该出现“以上均不匹配”。[P111]

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 数组是一种可变的、可索引的数据集合。在Scala中用Array[T]的形式来表示Java中的数组形式 T[]。 v...
    时待吾阅读 953评论 0 0
  • lang 2.1.和Java的异同 2.1.1.语法 Java++:增加的语法 -》纯OO;操作符重载;closu...
    时待吾阅读 2,370评论 0 0
  • 第一章 命令行中使用:load命令来加载(编译并运行)文件(脚本文件):scala> :load example....
    Azur_wxj阅读 407评论 0 0
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,744评论 0 33
  • 1.1(所以和黑粉结婚了) 26岁的方淼淼从来没有想到过,自己会与这个几个月前仍剑拔弩张的男人相知相恋。曾经的她肯...
    城畔空山阅读 546评论 0 0