美文网首页
Scala模式匹配简述

Scala模式匹配简述

作者: 安静1337 | 来源:发表于2016-03-22 13:38 被阅读145次

    什么是匹配模式?

    模式匹配并不很新,(上世纪)七十年代中期就已经有语言采用。据我所知,第一种语言是ML,但可能也有更早的语言支持。它在许多函数式语言中都算是标准功能,包括ML、Caml、Erlang、以及Haskell。

    那么什么是模式匹配呢?它可以让你给一个值匹配多种情况,有点像Java中的switch语句。但它不仅可以像switch语句一样用来匹配数字,还可以匹配对象的内在构建形式。

    比如,Scala中的List存在两种情况:要么是空List,写做Nil;要么由一个head元素紧接着另一List tail组成。有了模式匹配,你可以询问:给定的List是空List吗?只要编写case Nil、箭头(=>)以及后续表达式即可:

    case Nil => // 后续表达式
    

    你还可以询问:它是非空List吗?只要编写case x :: xs、箭头、以及后续表达式即可:

    case x :: xs => // 后续表达式
    

    双冒号(::)表示cons操作符;x表示List的首元素,xs表示剩余部分。于是,模式匹配会首先区分List是否为空。而如果List空,它会把List的首元素命名为x然后把List剩余部分命名为xs。接下来,这些变量可以被箭头右侧表达式所用。(参见示例1)

    示例1:match表达式

    list match { 
        case Nil => "was an empty list" 
        case car :: cdr => "head was " + car + ", tail was " + cdr
    }
    

    如果list不为空,将匹配到第二种情况,List首元素将赋值给x,而列表剩余部分赋值给xs。接下来,这些变量将被箭头符号右侧的字符串连接表达式所用。例如,如果list内容是List("hello", "world"),那么匹配表达式的结果将是字符串"head was hello, tail was List(world)"。

    上例的模式非常简单。但实际上模式还支持嵌套,类似表达式的嵌套,能让你编写层数很深的模式。总的来说,亮点在于,模式和表达式看起来很像。模式本质上和表达式属于完全一类东西,看上去就像构造表达式一样,可以用来构造复杂树状对象,但却不需要编写new。事实上,在Scala中,该对象构造时一样不需要new。然后你可以在某些位置填上占位变量,对应树对象中实际存在的值。(参见示例2)

    示例2:嵌套模式的match表达式

    object match { 
        case Address(Name(first, last), street, city, state, zip) => println(last + ", " + zip) 
        case _ => println("not an address") // 默认情况
    }
    

    在第一种情况下,模式Name(first, last)嵌在模式Address(...)中。last放在了Name构造函数内,可以“提取”出值,因而,可供箭头右边的表达式使用。

    因为匹配是发生在运行期的,而且JVM中泛型的类型信息会被擦掉(跟Java里范型一样不能匹配)。

    case m: Map[String, Int] => ...  // 不行,类型不起作用
    case m: Map[_, _] => ...  // 匹配通用的Map,OK
    

    但对于数组来说,类型信息是完好的,所以可以在Array上匹配。

    对于嵌套结构,举例就能一目了然。

    abstarct class Item
    case class Article(description: String, price: Double) extends Item
    case class Bundle(description: String, price: Double, items: Item*) extends Item
     
    Bundle("Father's day special", 20.0, 
      Article("Scala for the Impatient", 39.95),
      Bundle("Anchor Distillery Sampler", 10.0,
        Article("Old Potrero Straight Rye Whisky", 79.95),
        Article("Junipero Gin", 32.95)
      )
    )
    

    模式可以匹配到特定的嵌套:

    case Bundle(_, _, Article(descr, _), _*) => ...
    

    上面的代码中descr这个变量被绑定到第一个Article的description。另外还可以使用@来将值绑定到变量:

    // art被绑定为第一个Article,rest是剩余的Item序列
    case Bundle(_, _, art @ Article(_, _), rest @ _*) => ...
    

    样例类
    样例类是种特殊的类,经过优化以用于模式匹配。

    abstract class Amount
    // 继承了普通类的两个样例类
    case class Dollar(value: Double) extends Amount
    case class Currency(value: Double, unit: String) extends Amount
     
    // 样例对象
    case object Nothing extends Amount
    

    使用:

    amt match {
      case Dollar(v) => "$" + v
      case Currency(_, u) => "Oh noes, I got " + u
      case Nothing => ""  // 样例对象没有()
    }
    

    在声明样例类时,下面的过程自动发生了:
    构造器的每个参数都成为val,除非显式被声明为var,但是并不推荐这么做;
    在伴生对象中提供了apply方法,所以可以不使用new关键字就可构建对象;
    提供unapply方法使模式匹配可以工作;
    生成toString、equals、hashCode和copy方法,除非显示给出这些方法的定义。
    除了上述之外,样例类和其他类型完全一样,方法字段等。

    密封类
    当使用样例类来做模式匹配时,如果要让编译器确保已经列出所有可能的选择,可以将样例类的通用超类声明为sealed。
    密封类的所有子类都必须在与该密封类相同的文件中定义。
    如果某个类是密封的,那么在编译期所有的子类是可知的,因而可以检查模式语句的完整性。
    让所有同一组的样例类都扩展某个密封的类或特质是个好的做法。

    模式匹配的目的

    那么,为什么你需要模式匹配?我们每个人都有复杂的数据。如果我们坚持严格的面向对象的风格,那么我们并不希望直接访问数据内部的树状结构。相反,我们希望调用方法,然后在方法中访问。如果我们能够这样做,那么我们就再也不需要模式匹配了,因为这些方法已经提供了我们需要的功能。但很多情况下,对象并不提供我们需要的方法,而且我们无法(或者不愿)向这些对象添加方法。

    例如XML。如果给你一棵XML树,那么树就只是单纯的数据。要么是节点,要么是节点的序列。XML是一种非常通用的数据表现形式。例如,DOM本质上只是节点的数组,其中每个节点的类型都未知。现在我们设想一下,如果把XML树转换到某种更强的框架中,可以给你一个列表,容纳各种不同类型的对象。组成列表的元素可能包括诸如电话号码、备忘录或地址等。如果你想以静态类型的方式获取所有这些东西,就会遇上一个问题:你不知道每个元素的类型。在传统面向对象的编程语言中,唯一可行方式是,编写一大堆instanceof检测,一一测试每个元素是PhoneNumber实例、Memo实例,还是其他实例。一旦这些instanceof语句之一检测成功,你还需要进行类型转换。上述做法相当丑陋和笨拙,有了模式匹配就能避免了。模式匹配能以更安全、更自然的方式完成相同功能。

    从本质上讲,当你从外部取得具有结构的对象图时,模式匹配就必不可少。你会在若干情况下遇到这种现象,XML是其中之一。各种从文本解析而来的数据,都属于这一类。例如,有一种典型情况下模式匹配必不可少,即,处理编译器中的抽象语法树的情况。如果你要对表达式进行化简操作,表达式会被表示为树,你需要通过模式匹配对这些树进行提取操作。类似那样的情况还有许多。遇到这些情况时,模式匹配真的必不可少。

    相关文章

      网友评论

          本文标题:Scala模式匹配简述

          本文链接:https://www.haomeiwen.com/subject/rxcllttx.html