Chapter 15 《Case Classes and Pattern Matching》

样例类是Scala对对象进行模式匹配而不需要大量样板代码的方式,对希望做模式匹配的类加上关键字case即可。在Scala中如果类的定义体是空时,可以省去定义体的花括号。


样例类
  • case class Var(String: x)
    带有case关键字的类称为样例类,生成了一个伴生对象,在这个对象中存在接收一个参数的apply工厂方法。可以使用Var(x)而不是new Var(x)这样繁琐的语句来构建新的对象;参数列表中的参数都是有隐式的val从而称为类字段,编译器以自然的方式实现toString,hashCodeequals方法,编译器会添加一个copy用于拷贝,使用到了带名参数和缺省参数。样例类的最主要贡献还是可用于模式匹配。

模式匹配
    1. 模式匹配包含了一系列以case关键字打头的可选分支,每一个分支都包含一个模式,如果模式匹配了,模式对应的表达式就会被求值。一个match表达式的求值过程是按照模式给出的顺序逐一尝试的。
    1. 常量模式匹配常量,变量模式可以匹配任何值,_表示通用模式,并不会引入一个变量名来指向这个值。
    1. 还有就是构造者模式,相对于ifelse的判断,非常非常的方便。case _ =>返回的是Unit类型的()值。

模式的种类
    1. 通配模式:_用于任何对象,还可以用来忽略某个对象中并不关心的局部,在模式中使用名字,其实是将等效的对象或者变量绑定在这个名字上。
    1. 常量模式:仅能匹配自己。任何字面量都可以作为常量模式来使用。单例对象也可以被当做常量模式使用。val对象也可以被当做常量模式使用。
    1. 变量模式:变量模式可以匹配任何对象,scala将匹配上的对象绑定为对应的变量。绑定之后,可以利用这个变量来对对象做进一步的处理。
    • 注意:常量也可以有符号,比如Pi表示3.1415926。在模式中通过标识符的首个字母来表示名称是常量还是变量,所有以大写字母打头的标识符对应的模式都是常量模式。如果必须使用小写字母来表示常量,则需要使用反引号`pi`,在这种情况下,编译器认为pi是一个常量而不是变量,或者使用限定词,比如obj.pi等。`Scala中有两种用途,第一是将首字母为小写字母的标识符作为常量模式使用,二是将关键字当做普通的标识符,比如`Thread.`yield`
    1. 构造方法模式:构造方法模式是真正体现出模式匹配威力的地方。首先检测被匹配的对象是否是该样例类的实例,然后检查该对象的构造方法参数是否匹配模式中的构造参数。构造参数可能也是构造模式,匹配时也对他们进行检测,因此匹配可以达到任意的深度。
    1. 序列模式:类似于样例类匹配,可以跟序列类型做匹配。比如List或者Array,使用的语法是相同的,但可以在模式中给出任意数量的元素,因为ListArray中含有接收任意参数的apply工厂构造方法。在该模式中使用_*表示任意多个元素,如果要使用的话,必须放在构造列表的最后面。
    1. 元组模式,(a,b,c)可以匹配任意的三元组。
    1. 带类型的模式:来代替类型检测和类型转换
      case s: String => s.length
      case m: Map[_,_] => m.size
    • 和带类型的模式匹配但等效的方法是使用类型检测并进行强制转换。在Scala中类型检测为expr.isInstanceOf[String]expr.asInstanceOf[String]
    • 这其中涉及到的一个问题是类型擦除,ScalaJava一样,采用了擦除式的泛型,在运行时并不会保留类型参数的信息,所以不能使用类似的语句来判断,case m: Map[Int,Int] => m.size,编译器只能判断某个变量是不是Map,而不能判断这个Mapkeyvalue的类型。唯一例外的是数组,可以使用Array[String]这样的来进行验证,数组的元素类型被保存了。(where???)
  • 变量绑定,除了各自存在的变量模式外,还可以对任何其他模式添加变量,在模式之前添加变量名以及一个@符号,即可得到。case UnOp("abs",e@Unop("abs",_)) => e,出现两次求绝对值的操作,只需要计算一次即可。

模式守卫
  • Scala要求模式都是线性的:同一个模式变量在模式中只能出现一次。但是可以出现在多个case语句中。比如
def simplifyAdd(e: Expr) =
 e match {
case BinOp("+", x, x) => BinOp("*", x, Number(2))
case _ => e
}

是不被允许的。使用模式守卫,以if打头。模式守卫可以是任意的布尔表达式,一般会引用到模式中的变量,如果存在模式守卫,这个匹配仅在模式守卫为true的时候才会成功。

 def simplifyAdd(e: Expr) = e match {
case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2))
case _ => e
}

模式重叠
  • 模式会按照顺序进行逐个尝试,所以在match中,case的顺序是很重要的。

密封类
    1. 当编写模式匹配的时候,需要确保完整地覆盖了所有可能的case。为了使得Scala编译器能够帮助我们检测是否穷举了所有的情况,需要阻止子类的不可控再增加。手段是将这些样例类的超类标记为sealed,密封的。密封类除了在同一个文件中定义子类之外,不能添加新的子类。对于模式匹配非常有用,可以获得更好的编译器支持。如果类被打算用于样例类,一般会在类继承关系的顶部类前加上sealed关键字。
    1. 如果在match中可以明确确定只处理某些情况,对缺失不检查的情况可以使用@unchecked注解。在match的选择器部分进行添加(e: @unchecked) match {},对分支的覆盖完整性会被压制。

Option类型
  • Option表示可选值。将可选值解开的最常见方式是通过模式匹配,利用构造器模式。可以轻易的避免由null带来的空指针异常问题。

Pattern is everywhere
    1. Scala中很多地方都可以使用模式。利用可以使用元组模式 val (a, b) = (123, 234),在处理样例类的时候非常有用,如果知道要处理的样例类是什么,可以使用一个模式来析构它。
    1. 作为偏函数的case序列。用花括号包起来的一系列case可以用在任何允许出现函数字面量的地方。本质上将,case序列就是一个函数字面量,只不过是有多个入口,每个入口都有自己的参数列表。每个case对应该函数的一个入口,该入口的参数列表使用模式来指定。每个入口的逻辑主体是case右边的部分。通过case序列得到的是一个偏函数。偏函数如果应用在它不支持的值上(虽然该参数的类型是正确的),会发生运行时异常。
val second: List[Int] => Int = {
case x :: y :: _ => y
}
  • 如果要检查偏函数对某个入参是否与定义,必须首先声明处理的是偏函数。List[Int] => Int表示的是所有从整数列表到正数的函数,定义偏函数需要使用到
val second: PartialFunction[List[Int],Int] = {
case x :: y :: _ => y
}

PartialFunction中定义了一个isDefinedAt方法,可以用来检查该函数对某个特定的数值是否有定义。偏函数的典型应用是模式匹配函数字面量(case序列),这个模式匹配会出现在PartialFunction定义中的两处,一处用于apply,一处用于isDefinedAt。在默认没有声明的情况下,函数字面量对应的就是全函数。

    1. for表达式中的模式
      for表达式中也可以使用元组表达式,构造器表达式等等。如果在for表达式中迭代器代表的值没有匹配到模式,则该值会被直接丢弃。

tips
  • a -> 1只是Tuple2(a, 1)的一个语法糖,->是一个方法调用,在Predef中有隐式类ArrowAssoc对值a进行隐式转换,可以调用->方法。

你可能感兴趣的:(Chapter 15 《Case Classes and Pattern Matching》)