1、样例类(case class)和模式匹配(pattern matching),这组孪生的语法结构为我们编写规则的、未封装的数据结构提供支持,用于树形的递归数据结构处理。
2、样例类是Scala用来对对象进行模式匹配而并不需要大量的样板代码的方式。Scala允许我们省去空定义体的花括号,即class C跟class C {}是相同的。希望能做模式匹配的类加上一个case关键字。case修饰符会让Scala编译器对我们的类添加一些语法上的便利,第一:它会添加一个跟类同名的工厂方法。这意味着我们可以用Var(“x”)来构造一个Var对象,而不用稍长版本的new Var(“x”)。当你需要嵌套定义时,工厂方法尤为有用。由于代码中不再到处落满new关键字,可以一眼就看明白表达式的结构。第二个语法上的便利是参数列表中的参数都隐式地获得了一个val前缀,因此它们会被当作字段处理。再次,编译器会帮我们以“自然”的方式实现toString、hashCode和equals方法。这些方法分别会打印、哈希、比较包含类及所有入参的整棵树。由于Scala的==总是代理给equals方法,这意味着以样例类表示的元素总是以结构化的方式做比较。第三:编译器还会添加一个copy方法用于制作修改过的拷贝。这个方法可以用于制作除一两个属性不同之外其余完全相同的该类的新实例。样例类最大的好处是它们支持模式匹配。
3、simplifyTop的右边由一个match表达式组成。match表达式对应Java的switch,不过match关键字出现在选择器表达式后面。跟java的match中关键字放置的位置不同。模式匹配包含一系列以case关键字打头的可选分支(alternative)。每一个可选分支都包括一个模式(pattern)以及一个或多个表达式,如果模式匹配了,这些表达式就会被求值。箭头符=>用于将模式和表达式分开。一个match表达式的求值过程是按照模式给出的顺序逐一尝试的。第一个匹配上的模式被选中,跟在这个模式后面的表达式被执行。类似"+"和1这样的常量模式(constant pattern)可以匹配那些按照==的要求跟它们相等的值。而像e这样的变量模式(variable pattern)可以匹配任何值。匹配后,在右侧的表达式中,这个变量将指向这个匹配的值。在本例中,注意前三个可选分支都求值为e,一个在对应的模式中绑定的变量。通配模式(wildcard pattern),即_也匹配任何值,不过它并不会引入一个变量名来指向这个值。
4、match表达式可以被看作Java风格的switch的广义化。Java风格的switch可以很自然地用match表达式表达,其中每个模式都是常量且最后一个模式可以是一个通配模式(代表switch中的默认case)。Scala的match是一个表达式(也就是说它总是能得到一个值)。其次,Scala的可选分支不会贯穿(fall through)到下一个case。最后,如果没有一个模式匹配上,会抛出名为MatchError的异常。这意味着你需要确保所有的case被覆盖到,哪怕这意味着你需要添加一个什么都不做的默认case。
对于case _ => ,我们并没有给出任何代码,因此如果这个case被运行,什么都不会发生。两个case的结果都是unit值,即(),这也是整个match表达式的结果
5、模式的种类:
配模式(_)会匹配任何对象;通配模式还可以用来忽略某个对象中你并不关心的局部。
常量模式仅匹配自己。任何字面量都可以作为常量(模式)使用。例如,5、true和"hello"都是常量模式。同时,任何val或单例对象也可以被当作常量(模式)使用。例如,Nil这个单例对象能且仅能匹配空列表。
6、变量模式匹配任何对象,这一点跟通配模式相同。不过不同于通配模式的是,Scala将对应的变量绑定成匹配上的对象。在绑定之后,你就可以用这个变量来对对象做进一步的处理。常量模式也可以有符号形式的名称。当我们把Nil当作一个模式的时候,实际上就是在用一个符号名称来引用常量。Scala编译器是如何知道Pi是从scala.math包引入的常量,而不是一个代表选择器值本身的变量呢?Scala采用了一个简单的词法规则来区分:一个以小写字母打头的简单名称会被当作模式变量处理;所有其他引用都是常量。
7、如果需要,仍然可以用小写的名称来作为模式常量,有两个小技巧。首先,如果常量是某个对象的字段,可以在字段名前面加上限定词。例如,虽然pi是个变量模式,但this.pi或obj.pi是常量(模式),尽管它们以小写字母打头。如果这样不行(比如说pi可能是个局部变量),也可以用反引号将这个名称包起来。
8、构造方法模式是真正体现出模式匹配威力的地方。一个构造方法模式看上去像这样:“BinOp("+", e, Number(0))”。它由一个名称(BinOp)和一组圆括号中的模式:"+"、e和Number(0)组成。假定这里的名称指定的是一个样例类,这样的一个模式将首先检查被匹配的对象是否是以这个名称命名的样例类的实例,然后再检查这个对象的构造方法参数是否匹配这些额外给出的模式。这些额外的模式意味着Scala的模式支持深度匹配(deep match)。这样的模式不仅检查给出的对象的顶层,还会进一步检查对象的内容是否匹配额外的模式要求。
序列模式就跟与样例类匹配一样,也可以跟序列类型做匹配,比如List或Array。使用的语法是相同的,不过现在可以在模式中给出任意数量的元素。如果你想匹配一个序列,但又不想给出多长,你可以用_*作为模式的最后一个元素。这个看上去有些奇怪的模式能够匹配序列中任意数量的元素,包括0个元素。
元组模式,我们还可以匹配元组(tuple)。形如(a, b, c)这样的模式能匹配任意的三元组。
带类型的模式,可以用带类型的模式(typed pattern)来替代类型测试和类型转换
9、isInstanceOf和asInstanceOf两个操作符会被当作Any类的预定义方法处理,这两个方法接收一个用方括号括起来的类型参数。事实上,xasInstanceOf[String]是该方法调用的一个特例,它带上了显式的类型参数String。
你现在应该已经注意到了,在Scala中编写类型测试和类型检查会比较啰唆。我们是有意为之,因为这并不是一个值得鼓励的做法。通常,使用带类型的模式会更好,尤其是当你需要同时做类型测试和类型转换的时候,因为这两个操作所做的事情会被并在单个模式匹配中完成。类型模式(type pattern)中的下画线就像是其他模式中的通配符。除了用下画线,也可以用(小写的)类型变量。
Scala采用了擦除式的泛型,就跟Java一样。这意味着在运行时并不会保留类型参数的信息。这么一来,我们在运行时就无法判断某个给定的Map对象是用两个Int的类型参数创建的,还是其他什么类型参数创建的。对于这个擦除规则唯一的例外是数组,因为Java和Scala都对它们做了特殊处理。数组的元素类型是跟数组一起保存的,因此我们可以对它进行模式匹配。
10、除了独自存在的变量模式,我们还可以对任何其他模式添加变量。只需要写下变量名、一个@符和模式本身,就得到一个变量绑定模式。意味着这个模式将跟平常一样执行模式匹配,如果匹配成功,就将匹配的对象赋值给这个变量,就像简单的变量模式一样。
11、def simplifyAdd(e: Expr) = e match {
case BinOp("+",x,x) => BinOp("*",x,Number(2))
case _ => e
},以上第一个case是错误的,scala要求模式都是线性(linear)的:同一个模式变量在模式中只能出现一次。不过,我们可以用一个模式守卫(pattern guard)来重新定义这个匹配逻辑
。第一个case可以写成 case BinOp("+",x,y) if x == y => BinOp("*",x,Number(2))
模式守卫出现在模式之后,并以if打头。模式守卫可以是任意的布尔表达式,通常会引用到模式中的变量。如果存在模式守卫,这个匹配仅在模式守卫求值得到true时才会成功。
12、模式重叠
模式会按照代码中的顺序逐个被尝试。模式匹配中代码顺序非常重要,当出现一个模式包含另外一个模式时会出现编译失败
13、每当我们编写一个模式匹配时,都需要确保完整地覆盖了所有可能的case。有时候可以通过在末尾添加一个默认case来做到,不过这仅限于有合理兜底的场合。如果没有这样的默认行为,我们如何确信自己覆盖了所有的场景呢?我们可以寻求Scala编译器的帮助,帮我们检测出match表达式中缺失的模式组合。为了做到这一点,编译器需要分辨出可能的case有哪些。一般来说,在Scala中这是不可能的,因为新的样例类随时随地都能被定义出来。解决这个问题的手段是将这些样例类的超类标记为密封(sealed)的。密封类除在同一个文件中定义的子类之外,不能添加新的子类。解决这个问题的手段是将这些样例类的超类标记为密封(sealed)的。密封类除在同一个文件中定义的子类之外,不能添加新的子类。这一点对于模式匹配而言十分有用,因为这样一来我们就只需要关心那些已知的样例类。不仅如此,我们还因此获得了更好的编译器支持。如果我们对继承自密封类的样例类做匹配,编译器会用警告消息标示出缺失的模式组合。如果你的类打算被用于模式匹配,那么你应该考虑将它们做成密封类。只需要在类继承关系的顶部那个类的类名前面加上sealed关键字。这样,使用你的这组类的程序员在模式匹配这些类时,就会信心十足。这也是为什么sealed关键字通常被看作模式匹配的执照的原因。当对一个密封类定义模式匹配时漏掉了相关的模式,则编译器将给出警告,提示存在产生MatchError异常的风险,有时候你也会遇到编译器过于挑剔的情况,被迫添加了永远不会被执行的代码,为了让编译器不出现警告信息,一个更轻量的做法是给match表达式的选择器部分添加一个@unchecked注解。一般来说,可以像添加类型声明那样对表达式添加注解:在表达式后加一个冒号和注解的名称(以@打头)。例如,在本例中我们给变量e添加了@unchecked注解,即“e: @unchecked”。@unchecked注解对模式匹配而言有特殊的含义。如果match表达式的选择器带上了这个注解,那么编译器对后续模式分支的覆盖完整性检查就会被压制。
14、Scala由一个名为Option的标准类型来表示可选值。这样的值可以有两种形式:Some(x),其中x是那个实际的值;或者None对象,代表没有值。Scala集合类的某些标准操作会返回可选值。比如,Scala的Map有一个get方法,当传入的键有对应的值时,返回Some(value);而当传入的键在Map中没有定义时,返回None。将可选值解开最常见的方式是通过模式匹配。
15、Scala程序经常用到Option类型。可以把这个跟Java中用null来表示无值做比较。举例来说,java.util.HashMap的get方法要么返回存放在HashMap中的某个值,要么(在值未找到时)返回null。这种方式对Java来说是可以的,但很容易出错,因为在实践当中要想跟踪某个程序中的哪些变量可以为null是一件很困难的事。如果某个变量允许为null,那么必须记住在每次用到它的时候都要判空(null)。如果忘记了,那么运行时就有可能出现NullPointerException。由于这样的类异常可能并不经常发生,在测试过程中也就很难发现。对Scala而言,这种方式完全不能工作,因为Scala允许在哈希映射中存放值类型的数据,而null并不是值类型的合法元素。例如,一个HashMap[Int, Int]不可能用返回null来表示“无值”。cala鼓励我们使用Option来表示可选值。这种处理可选值的方式跟Java相比有若干优势。首先,对于代码的读者而言,某个类型为Option[String]的变量对应一个可选的String,跟某个类型为String的变量是一个可选的String(可能为null)相比,要直观得多。
16、变量定义中的模式,每当我们定义一个val或var,都可以用模式而不是简单的标识符。
作为偏函数的case序列,用花括号包起来的一系列case(即可选分支)可以用在任何允许出现函数字面量的地方。从本质上讲,case序列就是一个函数字面量,只是更加通用。不像普通函数那样只有一个入口和参数列表,case序列可以有多个入口,每个入口都有自己的参数列表。每个case对应该函数的一个入口,而该入口的参数列表用模式来指定。每个入口的逻辑主体是case右边的部分。通过case序列得到的是一个偏函数(partialfunction)。如果我们将这样一个函数应用到它不支持的值上,它会产生一个运行时异常。如果想检查某个偏函数是否对某个入参有定义,必须首先告诉编译器你知道你要处理的是偏函数。List[Int] => Int这个类型涵盖了所有从整数列表到整数的函数,不论这个函数是偏函数还是全函数。仅涵盖从整数列表到整数的偏函数的类型写作PartialFunction[List[Int], Int]。我们重新写一遍second函数,这次用偏函数的类型声明:
偏函数定义了一个isDefinedAt方法,可以用来检查该函数是否对某个特定的值有定义。偏函数的典型用例是模式匹配函数字面量,就像前面这个例子。事实上,这样的表达式会被Scala编译器翻译成偏函数,这样的翻译发生了两次:一次是实现真正的函数,另一次是测试这个函数是否对指定值有定义。
17、只要函数字面量声明的类型是PartialFunction,这样的翻译就会生效。如果声明的类型只是Function1,或没有声明,那么函数字面量对应的就是一个全函数(complete function)。
一般来说,我们应该尽量用全函数,因为偏函数允许运行时出现错误,而编译器帮不了我们。不过有时候偏函数也特别有用。你也许确信不会有不能处理的值传入,也可能会用到那种预期偏函数的框架,在调用函数之前,总是会先用isDefinedAt做一次检查。能匹配给定模式的值会被直接丢弃。
18、beside方法是把两个元素靠在一起。