返回介绍

数学基础

统计学习

深度学习

工具

Scala

二、模式匹配

发布于 2023-07-17 23:38:22 字数 16750 浏览 0 评论 0 收藏 0

  1. 模式匹配由match 表达式实现,其语法为:

    
    
    xxxxxxxxxx
    选择器 match { 可选分支一 可选分支二 ... }
    • 选择器是一个表达式,match 将对该表达式的结果进行选择。

    • 可选分支 是以case 关键字打头的、包含一个模式以及一个或者多个表达式。

      • 模式和表达式通过箭头符 => 分开。
      • 如果匹配到该模式,则这些表达式就会被求值,求值结果将作为match 表达式整体的结果。
      • 如果没有表达式,则结果是 unit 值,即()
    • 一个 match 表达式的求值过程是按照给出的模式顺序逐一尝试的。

      • 一旦某个模式被匹配,则该模式后面跟着的表达式被执行。
      • 一旦某个模式被匹配,则后续的模式将不再尝试。
  2. matchJavaswitch 很相似。但是它们有重要区别:

    
    
    xxxxxxxxxx
    switch(选择器) { 可选分支一 可选分支二 ... }
    • Scalamatch 是一个表达式,它总会返回一个值。

    • Scala 的可选分支并不会贯穿到下一个case

    • 如果没有任何一个模式匹配上,则Scala 会抛出MatchError 异常。

      这意味着你需要确保所有的 case 都会被覆盖到。

  3. 如果没有任何一个模式匹配上,此时你可以为选择器部分添加一个 @unchecked 注解。那么编译器对后续模式分支的覆盖完整性检查就会被抑制,这样就不会抛出MatchError 异常。

    
    
    xxxxxxxxxx
    (选择器 : @unchecked) match { 可选分支一 可选分支二 ... }

2.1 模式种类

  1. 所有的模式跟相应的表达式看上去完全一样。

  2. 模式会按照代码中的顺序逐个被尝试。通常要求捕获通用的case 出现在更具体的case 之后。

    如果我们将顺序颠倒过来,那么捕获通用的case 就会在更具体的规则之前执行。在许多场景下,编译器甚至会拒绝编译。

2.1.1 通配模式

  1. 通配符_ 可以匹配任何对象。

    • 它常用于缺省的、捕获所有剩余可选路径。

      
      
      xxxxxxxxxx
      def f(expr : String) = expr match{ case "name" => println("catch:"+expr) case _ => println("default") // 默认 case }
    • 也可以用于忽略某个对象中你并不关心的局部信息。

      
      
      xxxxxxxxxx
      abstract class Parent case class C(name:String,age:Int) extends Parent ​ def f(p: Parent) = p match{ case C(_,_) => println("catch:"+p) // 不关心 C 的参数 case _ => println("default") // 默认 case }

2.1.2 常量模式

  1. 常量模式仅仅匹配自己:

    • 任何字面量都可以作为常量模式使用。如: "+"1 这样的常量模式可以匹配那些按照 == 的要求跟它们相等的值。
    • 任何val 或单例对象也可以被当作常量模式使用。如:Nil 这个单例对象仅能匹配空列表。
    
    
    xxxxxxxxxx
    val S = "world" def f(x:Any) = x match{ case 1 => "catch:1" case "hello" => "catch:hello" case Nil => "catch:empty list" // 单例对象作为常量模式 case S => "catch:"+S // val 作为常量模式 case _ => "default" }

2.1.3 变量模式

  1. 类似 e 这样的变量模式可以匹配任何值。匹配后,在右侧的表达式中,这个变量e 将绑定成该匹配的值。

    在绑定之后,你可以用这个变量来做进一步处理。

    
    
    xxxxxxxxxx
    def f(x:Any) = x match{ case e => println(e) }
  2. 通配符模式_ 也可以匹配任何值,但是它并不会引入一个变量名来指向这个值。

  3. 采用变量模式之后,就没必要继续跟着通配符模式_。因为变量模式已经可以匹配任何值了,因此不可能走到后面的case

  4. 常量模式中,也有可能出现符号形式的名字,如Nil。当我们将Nil 当作一个模式的时候,实际上就是在用一个符号名称来引用常量。

    Scala 通过一个简单的词法规则来区分:

    • 以一个小写字母打头的简单名称会被当作模式变量处理。

    • 所有其它引用都是常量。

    • 如果需要用小写的名称作为模式常量,有两个办法:

      • 如果常量是某个对象的字段,则可以在字段名之前加上限定词。如this.n

        尽管它们是以小写开头,但是由于. 的存在,它们会被解析为常量模式。

      • 使用反引号将这个名字包围起来。

        Scala 中反引号有两个用途:

        • 将小写字母打头的标识符用作模式匹配中的常量。
        • 将关键字当作普通的标识符。
    
    
    xxxxxxxxxx
    val name = "hello" def f(x:Any) = x match{ case `name` => println("catch hello") case Nil => println("catch Nil") case nil => println("catch:"+nil) } f("hello") // 匹配到 name: 常量模式 f(List()) // 匹配到 Nil :常量模式 f("world") // 匹配到 nil :变量模式

2.1.4 构造方法模式

  1. 构造方法模式看上去就像 UnOP("_",e) 。这个模式匹配所有类型为 UnOP,并且首个入参匹配 "_"、第二个入参匹配e 的值。

    构造方法模式由一个名称和一组圆括号中的模式组成。假设这里的名称是一个样例类,则这样的一个模式:

    • 首先检查被匹配的对象是否是以这个名称命名的样例类的实例。

    • 然后检查这个对象的构造方法参数是否匹配这些额外给出的模式。

      • 这些额外的模式意味着Scala 的模式支持深度匹配。这样的模式不仅检查给定的对象的顶层,还将进一步检查对象的内容是否匹配额外的模式要求。
      • 由于额外的模式也可能是构造方法模式,因此检查对象内部时可以到任意的深度。
      • 额外的模式可以通过通配符_ 或者其它的模式来匹配。尤其可以通过变量模式来绑定匹配的值。
    
    
    xxxxxxxxxx
    case class Worker(name:String,life_time:Double) case class System(worker:Worker,lift_time:Double) def f(x:Any) = x match{ case System(Worker(name,10.0),t) => println("a:worker name="+ name + " ; system life="+t) case System(Worker(name,_),t) => println("b:worker name="+ name + " ; system life="+t) case other => println("catch other: "+other) } f(System(Worker("first",10.0),100.0)) // 匹配到第一个 case f(System(Worker("second",11.0),100.0)) // 匹配到第二个 case f(Worker("second",11.0)) // 匹配到第三个 case

2.1.5 序列模式

  1. 和样例类匹配一样,也可以和序列类型做匹配,如ListArray ,使用的语法是相同的。

    • 可以在模式中给出任意数量的元素。

    • 如果想匹配一个序列,但又不想给出很长的元素,则可以用*_ 作为模式的最后一个元素。

      这种方式可以匹配序列中任意数量的元素,包括 0 个元素。

    
    
    xxxxxxxxxx
    def f(x:Any) = x match { case List(0,_) => println("2 elements list,start with 0") case List(0,_,_) => println("3 elements list,start with 0") case List(e,_,_,_) => println("4 elements list,catch :"+e) // 绑定到 e case List(0,_*) => println("various elements list,start with 0") // 以 0 开始的任意长度 case _ => println("default") } f(List(0)) // case List(0,_*) f(List(0,1)) // case List(0,_) f(List(0,1,2)) // case List(0,_,_) f(List(0,1,2,3)) // case List(e,_,_,_) : e 为 0 f(List(0,1,2,3,4)) // case List(0,_*) f(Array(0,1)) // case _

2.1.6 元组模式

  1. 模式匹配还支持元组。形如(a,b,c) 这样的模式能够匹配任意的三元组。

    
    
    xxxxxxxxxx
    def f(x:Any) = x match { case (a,b,c) => println("a:"+a+" ;b:"+b+" ;c:"+c) case _ => println("default") } f(1,"second",3.0) // case (a,b,c)

2.1.7 带类型的模式

  1. 可以用带类型的模式来替换类型测试和类型转换。

  2. 假如需要设计一个函数,该函数返回不同类型的对象的大小或者长度。常规的设计思路时:通过类型测试以及类型转换:

    
    
    xxxxxxxxxx
    def get_size(x:Any) = { if x.isInstanceOf[String]{ // 如果是字符串 val s = x.asInstanceOf[String] // 强制类型转换 s.length }else ... // 如果是其它序列,如 List,Map 等 }
    • 通过expr.isInstanceOf[T] 可以判断expr 是否是 T 类型,通过expr.asInstanceOf[T] 可以将expr 转换成 T 类型。

      这两个操作符会被当成Any 类的预定义方法处理,它们接收一个用方括号括起来的类型参数。

    • Scala 中编写类型测试和类型检查会很罗嗦。这是有意为之,因为Scala 并不鼓励这样做。Scala 推荐使用带类型的模式,尤其是当你需要同时执行类型测试和类型转换时。因为这两个操作所作的事情会被并在单个模式匹配中完成。

      
      
      xxxxxxxxxx
      def get_size(x:Any) = x match { case s: String => s.length case m: Map[_,_] => m.size case _ => -1 }
  3. 在类型模式中,你也可以使用下划线来通配任意类型,这就像是其它模式中的通配符。如 Map[_,_]

  4. Java 一样,Scala 的泛型采取了类型擦除。这意味着运行时并不会保留类型参数的信息。因此我们在运行时无法判断某个给定的Map 对象是由两个Int 的类型参数创建,还是由其它类型参数创建。系统只能判断某个对象是不是Map

    对这个规则唯一例外的是数组。因为JavaScala 都对它们进行了特殊处理,数组的元素类型和数组是一起保存的,因此可以对其进行模式匹配。

    
    
    xxxxxxxxxx
    def isIntIntMap(x:Any) = x match{ case m: Map[Int,Int] => true case _ => false } def isIntArray(x:Any) = x match{ case a: Array[Int] => true case _ => false } println(isIntIntMap(Map[Int,Int](100 -> 1, 200 ->2))) // 输出: true println(isIntIntMap(Map[String,Int]("a" -> 1,"b"->2))) // 输出: true ​ println(isIntArray(Array[Int](1,2,3))) // 输出: true println(isIntArray(Array[String]("a","b","c"))) // 输出: false

2.2 变量绑定

  1. 除了独立存在的变量模式之外,还可以对任何其它模式添加变量。方式为:变量名@模式 ,这就得到一个变量绑定模式。

    变量绑定模式和常规模式一样执行模式匹配。如果匹配成功,就将匹配的对象赋值给这个变量,就像简单的变量模式一样。

    
    
    xxxxxxxxxx
    case class Worker(name:String,life_time:Double) case class System(worker:Worker,lift_time:Double) def f(x:Any) = x match{ case System(worker@Worker(_,_),t) => println("worker:"+ worker) // 绑定到 worker case other => println("catch other: "+other) } f(System(Worker("first",10.0),100.0))

2.3 模式守卫

  1. Scala 要求模式都是线性的:同一个模式变量在模式中只能出现一次。

    
    
    xxxxxxxxxx
    case class Worker(name:String,life_time:Double) case class System(worker:Worker,lift_time:Double) def f(x:Any) = x match{ case System(worker@Worker(_,t),t) => println(worker) // 希望两个 lift_time 是一样的,但是这里会编译失败 case other => println("catch other: "+other) }

    如果希望模式变量出现多次,则可以用模式守卫来重新定义匹配逻辑。模式守卫出现在模式之后,并以 if 打头。

    • 模式守卫可以是任意的布尔表达式,通常会引用到模式中的变量。
    • 如果存在模式守卫,则这个匹配仅在模式守卫求值得到true 时才会成功。
    
    
    xxxxxxxxxx
    case class Worker(name:String,life_time:Double) case class System(worker:Worker,lift_time:Double) def f(x:Any) = x match{ case System(worker@Worker(_,t1),t2) if t1==t2 => println("worker:"+ worker) // 模式守卫 case other => println("catch other: "+other) } f(System(Worker("first",10.0),10.0)) // 匹配到第一个 case f(System(Worker("second",10.0),20.0)) // 匹配到第二个 case

2.4 密封类

  1. 当我们编写一个模式匹配时,需要确保完整地覆盖了所有可能的case

    • 有时候可以通过在末尾添加一个缺省case 来做到,但是这仅限于有合理兜底的场景。

    • 如果没有一个缺省的case,我们可以求助于Scala 编译器,编译器帮我们检测出match 表达式中缺失的模式组合。

      为了做到这一点,编译器需要分辨出所有可能的case 有哪些。事实上这是不可能的。

    
    
    xxxxxxxxxx
    def f(x:Any) = x match{ case i:Int => println("catch int:"+i) case s:String => println("catch String:"+s) /* 如果没有缺省 case,Scala 很难知道所有可能的 case 有哪些 */ }
  2. 解决这个问题的方法是:将这些样例类的超类标记为密封的。语法是:在类继承关系顶部的那个类的类名前加上sealed 关键字。

    • 密封类除了在同一个文件中定义的子类之外,不能添加新的子类。这对于模式匹配而言非常有用,因此我们就只需要关心那些已知的样例类。

    • 此外编译器还能更好的支持模式匹配。如果对继承自密封类的样例类进行匹配,编译器会用警告消息标识出缺失的模式组合。

      因此如果你的类打算被用于模式匹配,则你应该将其作为密封类。这也是为什么sealed 关键字通常被看作模式匹配的通行证的原因。

      
      
      xxxxxxxxxx
      sealed abstract class Man case class Worker(name:String) extends Man case class Leader(name:String) extends Man ​ def f(x:Man) = x match{ case w:Worker => println("catch worker:" + w) } f(Worker("worker_1")) ​ /* 编译器警告: Warning:(5, 24) match may not be exhaustive. It would fail on the following input: Leader(_) def f(x:Man) = x match{ */

      解决办法是:添加一个缺省的case 用于捕获所有的模式,或者补全缺失的样例类。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文