电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

Scala编程指南 揭示Scala的本质


发布日期:2020/12/20
 

Scala 是一种基于JVM集合了面向对象编程和函数式编程优点的高级程序设计语言在《Scala编程指南 更少的字更多的事》中我们从几个方面见识了Scala 简洁可伸缩高效的语法我们也描述了许多Scala 的特性本文为《Programming Scala》第三章我们会在深入Scala 对面向对象编程和函数式编程的支持前完成对Scala 本质的讲解

CTO推荐专题Scala编程语言

Scala 本质

在我们深入Scala 对面向对象编程以及函数式编程的支持之前让我们来先完成将来可能在程序中用到的一些Scala 本质和特性的讨论

操作符?操作符?

Scala 一个重要的基础概念就是所有的操作符实际上都是方法考虑下面这个最基础的例子

// codeexamples/Rounding/oneplustwoscriptscala + 两个数字之间的加号是个什么呢?是一个方法第一Scala 允许非字符型方法名称你可以把你的方法命名为+$ 或者其它任何你想要的名字(译着后面会提到例外)第二这个表达式等同于 +()(我们在 的后面加了一个空格因为 会被解释为Double 类型)当一个方法只有一个参数的时候Scala 允许你不写点号和括号所以方法调用看起来就像是操作符调用这被称为中缀表示法也就是操作符是在实例和参数之间的我们会很快见到很多这样的例子

类似的一个没有参数的方法也可以不用点号直接调用这被称为后缀表示法

Ruby 和SmallTalk 程序员现在应该感觉和在家一样亲切了因为那些语言的使用者知道这些简单的规则有着广大的好处它可以让你用自然的优雅的方式来创建应用程序

那么哪些字符可以被用在标识符里呢?这里有一个标识符规则的概括它应用于方法类型和变量等的名称要获取更精确的细节描述参见[ScalaSpec]Scala 允许所有可打印的ASCII 字符比如字母数字下划线和美元符号$除了括号类的字符比如( ) [ ] { }和分隔类字符比如` ;除了上面的列表Scala 还允许其他在u 到uF 之间的字符比如数学符号和其它 符号这些余下的字符被称为操作符字符包括了/ <

不能使用保留字

正如大多数语言一样你不能是用保留字作为标识符我们在《第 打更少的字做更多的事》的保留字 章节列出了所有的保留字回忆一下其中有些保留字是操作符和标点的组合比如说简单的一个下划线(_) 是一个保留字!

普通标识符 字母数字以及 $ _ 操作符的组合

和Java 以及很多其它语言一样一个普通标志符可以以一个字母或者下划线开头紧跟着更多的字母数字下划线和美元符号和Unicode 等同的字符也是被允许的然而和Java 一样Scala 保留了美元符号作为内部使用所以你不应该在你自己的标识符里使用它在一个下划线之后你可以接上字母数字或者一个序列的操作符字符下划线很重要它告诉编译器把后面直到空格之前所有的字符都处理为标识符比如val xyz__++ = 把值 赋值给变量xyz__++而表达式val xyz++= = 却不能通过编译因为这个标识符同样可以被解释为xyz ++=看起来像是要把某些东西加到xyz 后面去类似的如果你在下划线后接有操作符字符你不能把它们和字母数字混合在一起这个约束避免了像这样的表达式的二义性abc_=这是一个标识符abc_= 还是给abc_ 赋值 呢?

普通标识符 操作符

如果一个标识符以操作符为开头那么余下的所有字符都必须是操作符字符

反引用字面值

一个标识符可以是两个反单引号内一个任意的字符串(受制于平台的限制)比如val `this is a valid identifier` = Hello World!回忆一下我们可以发现这个语法也是引用Java 或者NET 的类库中和Scala 保留字的名称一样的方法时候所用的方式比如Proxy`type`()

模式匹配标识符

在模式匹配表达式中以小写字母开头的标识都会被解析为变量标识符而以大写字母开头的标识会被解析为常量标识符这个限定避免了一些由于非常简洁的变量语法而带来的二义性例如不用写val 关键字

语法糖蜜

一旦你知道所有的操作符都是方法那么理解一些不熟悉的Scala 代码就会变的相对容易些了你不用担心那些充满了新奇操作符的特殊案例在《第 分到Scala 介绍》中的初尝并发 章节中我们使用了Actor 类你会注意到我们使用了一个惊歎号(!)来发送消息给一个Actor现在你知道!只是另外一个方法而已就像其它你可以用来和Actor 交互的快捷操作符一样类似的Scala 的XML 库提供了 操作符来渗入到文档结构中去这些只是scalaxmlNodeSeq 类的方法而已

灵活的方法命名规则能让你写出就像Scala 原生扩展一样的库你可以写一个数学类库处理数字类型加减乘除以及其它常见的数学操作你也可以写一个新的行为类似Actors 的并发消息层各种的可能性仅受到Scala 方法命名限制的约束

警告

别因为你可以就觉得你应该这么作当用Scala 来设计你自己的库和API 的时候记住晦涩的标点和操作符会难以被程序员所记住过量使用这些操作符会导致你的代码充满难懂的噪声坚持已有的约定当一个快捷符号没有在你脑海中成型的时候清晰地把它拼出来吧

不用点号和括号的方法

为了促进阅读性更加的编程风格Scala 在方法的括号使用上可谓是灵活至极如果一个方法不用接受参数你可以无需括号就定义它调用者也必须不加括号地调用它如果你加上了空括号那么调用者可以有选择地加或者不加括号例如List 的size 方法没有括号所以你必须写List()size如果你尝试写List()size() 就会得到一个错误然而String 类的length 方法在定义时带有括号所以hellolength() 和hellolength 都可以通过编译

Scala 社区的约定是在没有副作用的前提下省略调用方法时候的空括号所以查询一个序列的大小(size)的时候可以不用括号但是定义一个方法来转换序列的元素则应该写上括号这个约定给你的代码使用者发出了一个有潜在的巧妙方法的信号

当调用一个没有参数的方法或者只有一个参数的方法的时候还可以省略点号知道了这一点我们的List()size 例子就可以写成这样

// codeexamples/Rounding/nodotscriptscala List( ) size 很整洁但是又令人疑惑在什么时候这样的语法灵活性会变得有用呢?是当我们把方法调用链接成自表达性的自我解释的语 的时候

// codeexamples/Rounding/nodotbetterscriptscala def isEven(n: Int) = (n % ) == List( ) filter isEven foreach println 就像你所猜想的运行上面的代码会产生如下输出

Scala 这种对于方法的括号和点号不拘泥的方式为书写域特定语言(DomainSpecific Language)定了基石我们会在简短地讨论一下操作符优先级之后再来学习它

优先级规则

那么如果这样一个表达式 * / * 实际上是Double 上的一系列方法调用那么这些操作符的调用优先级规则是什么呢?这里从低到高表述了它们的优先级[ScalaSpec]

◆所有字母

◆|

◆^

◆&

◆< >

◆= !

◆:

◆+

◆* / %

◆所有其它特殊字符

在同一行的字符拥有同样的优先级一个例外是当= 作为赋值存在时它拥有最低的优先级

因为* 和/ 有一样的优先级下面两行scala 对话的行为是一样的

scala> * / * res: Double = scala> ((( * ) / ) * )res: Double = 在一个左结合的方法调用序列中它们简单地进行从左到右的绑定你说左绑定?在Scala 中任何以冒号: 结尾的方法实际上是绑定在右边的而其它方法则是绑定在左边举例来说你可以使用:: 方法(称为consconstructor 构造器的缩写)在一个List 前插入一个元素

scala> val list = List(b c d) list: List[Char] = List(b c d) scala> a :: list res: List[Char] = List(a b c d) 第二个表达式等效于list::(a)在一个右结合的方法调用序列中它们从右向左绑定那左绑定和有绑定混合的表达式呢?

scala> a :: list ++ List(e f) res: List[Char] = List(a b c d e f) (++ 方法链接了两个list)在这个例子里list 被加入到List(ef) 中然后a 被插入到前面来创建最后的list通常我们最好加上括号来消除可能的不确定因素

提示

任何名字以: 结尾的方法都向右边绑定而不是左边

最后注意当你使用scala 命令的时候无论是交互式还是使用脚本看上去都好像可以在类型之外定义全局变量和方法这其实是一个假象解释器实际上把所有定义都包含在一个匿名的类型中然后才去生成JVM 或者NET CLR 字节码

领域特定语言

领域特定语言也称为DSL为特定的问题领域提供了一种方便的语意来表达自己的目标比如SQL 为处理与数据库打交道的问题提供了刚刚好的编程语言功能使之成为一个领域特定语言

有些DSL 像SQL 一样是自我包含的而今使用成熟语言来实现DSL 使之成为母语言的一个子集变得流行起来这允许程序员充分利用宿主语言来涵盖DSL 不能覆盖到的边缘情况而且节省了写词法分析器解析器和其它语言基础的时间

Scala 的丰富灵活的语法使得写DSL 轻而易举你可以把下面的例子看作使用Specs 库(参见Specs 章节)来编写行为驱动开发[BDD] 程序的风格

// codeexamples/Rounding/specsscriptscala // Example fragment of a Specs script Doesnt run standalone nerd finder should { identify nerds from a List in { val actors = List(Rick Moranis James Dean Woody Allen) val finder = new NerdFinder(actors) finderfindNerds mustEqual List(Rick Moranis Woody Allen) } } 注意这段代码和英语语法的相似性this should test that in the following scenario(这应该在以下场景中测试它)this value must equal that value (这个值必须等于那个值)等等这个例子使用了华丽的Specs 库它提供了一套高效的DSL 来用于行为驱动开发测试和工程方法学通过最大化利用Scala 的自有语法和诸多方法Specs 测试组即使对于非开发人员来说也是可读的

这只是对Scala 强大的DSL 的一个简单尝试我们会在后面看到更多其它例子以及在讨论更高级议题的时候学习如何编写你自己的DSL(参见《第 Scala 的领域特定语言》)

Scala if 指令

即使是最常见的语言特性在Scala 里也被增强了让我们来看看简单的if 指令和大多数语言一样Scala 的if 测试一个条件表达式然后根据结果为真或假来跳转到响应语句块中一个简单的例子

// codeexamples/Rounding/ifscriptscala if ( + == ) { println(Hello from ) } else if ( + == ) { println(Hello from Remedial Math class?) } else { println(Hello from a nonOrwellian future) } 在Scala 中与众不同的是if 和其它几乎所有指令实际上都是表达式所以我们可以把一个if 表达式的结果赋值给其它(变量)像下面这个例子所展示的

// codeexamples/Rounding/assignedifscriptscala val configFile = new javaioFile(myapprc) val configFilePath = if (configFileexists()) { configFilegetAbsolutePath() } else { configFilecreateNewFile() configFilegetAbsolutePath() } 注意 if 语句是表达式意味着它们有值在这个例子里configFilePath 的值就是if 表达式的值它处理了配置文件不存在的情况并且返回了文件的绝对路径这个值现在可以在程序中被重用了if 表达式的值只有在被使用到的时候才会被计算

因为在Scala 里if 语句是一个表达式所以就不需要C 类型子语言的三重条件表达式了你不会在Scala 里看到x ? doThis() : doThat() 这样的代码因为Scala 提供了一个即强大又更具有可读性的机制

如果我们在上面的例子里省略else 字句会发生什么?在scala 解释器里输入下面的代码会告诉我们发生什么

scala> val configFile = new javaioFile(~/myapprc) configFile: javaioFile = ~/myapprc scala> val configFilePath = if (configFileexists()) { | configFilegetAbsolutePath() | } configFilePath: Unit = () scala> 注意现在configFilePath 是Unit 类型了(之前是String)类型推断选择了一个满足if 表达式所有结果的类型Unit 是唯一的可能因为没有值也是一个可能的结果

Scala for 推导语句

Scala 另外一个拥有丰富特性的类似控制结构是for 循环在Scala 社区中也被称为for 推导语句或者for 表达式语言的这个功能绝对对得起一个花哨的名字因为它可以做一些很酷的戏法

实际上术语推导(comprehension)来自于函数式编程它表达了这样个一个观点我们正在遍历某种集合推导我们所发现的然后从中计算出一些新的东西出来

一个简单的小狗例子

让我们从一个基本的for 表达式开始

// codeexamples/Rounding/basicforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for (breed < dogBreeds) println(breed) 你可能已经猜到了这段代码的意思是对于列表dogBreeds 里面的每一个元素创建一个临时变量叫breed并赋予这个元素的值然后打印出来把< 操作符看作一个箭头引导集合中一个一个的元素到那个我们会在for 表达式内部引用的局部变量中去这个左箭头操作符被称为生成器之所以这么叫是因为它从一个集合里产生独立的值来给一个表达式用

过滤器

那如果我们需要更细的粒度呢? Scala 的for 表达式通过过滤器来我们指定集合中的哪些元素是我们希望使用的所以要在我们的狗品种列表里找到所有的梗类犬我们可以把上面的例子改成下面这样

// codeexamples/Rounding/filteredforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for (breed < dogBreeds if ntains(Terrier) ) println(breed) 如果需要给一个for 表达式添加多于一个的过滤器用分号隔开它们

// codeexamples/Rounding/doublefilteredforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for (breed < dogBreeds if ntains(Terrier); if !breedstartsWith(Yorkshire) ) println(breed) 现在你已经找到了所有不出生于约克郡的梗类犬但愿也知道了过滤器在过程中是多么的有用

产生器

如果说你不想把过滤过的集合打印出来而是希望把它放到程序的另外一部分去处理呢?yeild 关键字就是用for 表达式来生成新集合的关键在下面的例子中注意我们把for 表达式包裹在了一对大括号中就像我们定义任何一个语句块一样

提示

for 表达式可以用括号或者大括号来定义但是使用大括号意味着你不必用分号来分割你的过滤器大部分时间里你会在有一个以上过滤器赋值的时候倾向使用大括号

// codeexamples/Rounding/yieldingforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) val filteredBreeds = for { breed < dogBreeds if ntains(Terrier) if !breedstartsWith(Yorkshire) } yield breed 在for 表达式的每一次循环中被过滤的结果都会产生一个名为breed 的值这些结果会随着每运行而累积最后的结果集合被赋给值filteredBreeds(正如我们上面用if 指令做的那样)由foryield 表达式产生的集合类型会从被遍历的集合类型中推断在这个例子里filteredBreeds 的类型是List[String]因为它是类型为List[String] 的dogBreeds 列表的一个子集

扩展的作用域

Scala 的for 推导语句最后一个有用的特性是它有能力把在定义在for 表达式第一部分里的变量用在后面的部分里这个例子是一个最好的说明

// codeexamples/Rounding/scopedforscriptscala val dogBreeds = List(Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog) for { breed < dogBreeds upcasedBreed = breedtoUpperCase() } println(upcasedBreed) 注意即使没有声明upcaseBreed 为一个val你也可以在你的for 表达式主体内部使用它这个方法对于想在遍历集合的时候转换元素的时候来说是很理想的

最后在《第 应用程序设计》的Options 和For 推导语句章节我们会看到使用Options 和for 推导语句可以大大地减少不必要的null 和空判断从而减少代码数量

其它循环结构

Scala 有几种其它的循环结构

Scala while 循环

和许多语言类似while 循环在条件为真的时候会持续执行一段代码块例如下面的代码在下一个星期五同时又是号之前每天打印一句抱怨的话

// codeexamples/Rounding/whilescriptscala // WARNING: This script runs for a LOOOONG time! import javautilCalendar def isFridayThirteen(cal: Calendar): Boolean = { val dayOfWeek = calget(CalendarDAY_OF_WEEK) val dayOfMonth = calget(CalendarDAY_OF_MONTH) // Scala returns the result of the last expression in a method (dayOfWeek == CalendarFRIDAY) && (dayOfMonth == ) } while (!isFridayThirteen(CalendargetInstance())) { println(Today isnt Friday the th Lame) // sleep for a day Threadsleep() } 你可以在下面找到一张表它列举了所有在while 循环中工作的条件操作符

Scala dowhile 循环

和上面的while 循环类似一个dowhile 循环当条件表达式为真时持续执行一些代码唯一的区别是dowhile 循环在运行代码块之后才进行条件检查要从 数到我们可以这样写

// codeexamples/Rounding/dowhilescriptscala var count = do { count += println(count) } while (count < ) 这也展示了在Scala 中遍历一个集合还有一种更优雅的方式我们会在下一节看到

生成器表达式

还记得我们在讨论for 循环的时候箭头操作符吗(<)?我们也可以让它在这里工作让我们来整理一下上面的dowhile 循环

// codeexamples/Rounding/generatorscriptscala for (i < to ) println(i) 这就是所有需要的了是Scala 的RichInt(富整型)使得这个简洁的单行代码成为可能编译器执行了一个隐式转换一个Int (整型)转换成了RichInt 类型(我们会在《第 Scala 对象系统》的Scala 类型结构 章节以及《第 Scala 函数式编程》的隐式转换 章节中讨论这些转换)RichInt 定义了以讹to 方法它接受另外一个整数然后返回一个RangeInclusive 的实例也就是说Inclusive 是Rang 伴生对象(Companion Object我们在《第 分到Scala 介绍》中间要介绍过参考《第 Scala 高级面向对象编程》获取更多信息)的一个嵌套类类Range 的这个嵌套类继承了一系列方法来和序列以及可迭代的数据结构交互包括那些在for 循环中必然会使用到的

顺便说一句如果你想从 数到 但是不包括 你可以使用until 来代替to比如for (i < until )

这样就一幅清晰的图画展示了Scala 的内部类库是如何结合起来形成简单易用的语言结构的

注意

当和大多数语言的循环一起工作时你可以使用break 来跳出循环或者continue 来继续下一个迭代Scala 没有这两个指令但是当编写地道的Scala 代码时它们是不必要的你应该使用条件表达式来测试一个循环是否应该继续或者利用递归更好的方法是在这之前就用过滤器来出去循环中复杂的条件状态然而因为大众需求 版本的Scala 加入了对break 的支持不过是以库的一个方法实现而不是内建的break 关键字

条件操作符

Scala 从Java 和它的前辈身上借用了绝大多数的条件操作符你可以在下面的if 指令while 循环以及其它任何可以运用条件判断的地方发现它们

我们会在《第 Scala 高级面向对象编程》的对象的相等 章节中更深入讨论对象相等性例如我们会看到== 在Scala 和Java 中有着不同的含义除此以外这些操作符大家应该都很熟悉所以让我们继续前进到一些新的激动人心的特性上去

模式匹配

模式匹配是从函数式语言中引入的强大而简洁的多条件选择跳转方式你也可以把模式匹配想象成你最喜欢的C 类语言的case 指令当然是打了激素的在典型的case 指令中通常只允许对序数类型进行匹配产生一些这样的表达式在i 为 的case 里打印一个消息在i 为 的case里离开程序而有了Scala 的模式匹配你的case 可以包含类型通配符序列甚至是对象变量的深度检查

一个简单的匹配

让我们从模拟抛硬币匹配一个布尔值开始

// codeexamples/Rounding/matchbooleanscriptscala val bools = List(true false) for (bool < bools) { bool match { case true => println(heads) case false => println(tails) case _ => println(something other than heads or tails (yikes!)) } } 看起来很像C 风格的case 语句对吧?唯一的区别是最后一个case 使用了下划线_ 通配符它匹配了所有上面的case 中没有定义的情况所以它和JavaC# 中的switch 指令的default 关键字作用相同

模式匹配是贪婪的只有第一个匹配的情况会赢所以如果你在所有case 前方一个case _ 语句那么编译器会在下一个条件抛出一个无法执行到的代码的错误因为没人能跨过那个default 条件

提示

使用case _ 来作为默认的满足所有的匹配

那如果我们希望获得匹配的变量呢?

匹配中的变量

// codeexamples/Rounding/matchvariablescriptscala import scalautilRandom val randomInt = new Random()nextInt() randomInt match { case => println(lucky seven!) case otherNumber => println(boo got boring ol + otherNumber) } 在这个例子里我们把通配符匹配的值赋给了一个变量叫otherNumber然后在下面的表达式中打印出来如果我们生成了一个我们会对它称颂道德反之我们则诅咒它让我们经历了一个不幸运的数字

类型匹配

这些例子甚至还没有开始接触到Scala 的模式匹配特性的最表面让我们来尝试一下类型匹配

// codeexamples/Rounding/matchtypescriptscala val sundries = List( Hello q) for (sundry < sundries) { sundry match { case i: Int => println(got an Integer: + i) case s: String => println(got a String: + s) case f: Double => println(got a Double: + f) case other => println(got something else: + other) } } 这次我们从一个元素为Any 类型的List 中拉出所有元素包括了StringDoubleInt和Char对于前三种类型我们让用户知道我们拿到了那种类型以及它们的值当我们拿到其它的类型(Char)我们简单地让用户知道值我们可以添加更多的类型到那个列表它们会被最后默认的通配符case 捕捉

序列匹配

鑒于用Scala 工作通常意味着和序列打交道要是能和列表数组的长度和内容来匹配岂不美哉?下面的例子就做到了它测试了两个列表来检查它们是否包含个元素并且第二个元素是

// codeexamples/Rounding/matchseqscriptscala val willWork = List( ) val willNotWork = List( ) val empty = List() for (l < List(willWork willNotWork empty)) { l match { case List(_ _ _) => println(Four elements with the nd being ) case List(_*) => println(Any other list with or more elements) } } 在第二个case 里我们使用了一个特殊的通配符来匹配一个任意大小的List甚至个元素任何元素的值都行你可以在任何序列匹配的最后使用这个模式来解除长度制约

回忆一下我们提过的List 的cons 方法::表达式a :: list 在一个列表前加入一个元素你也可以使用这个操作符来从一个列表中解出头和尾

// codeexamples/Rounding/matchlistscriptscala val willWork = List( ) val willNotWork = List( ) val empty = List() def processList(l: List[Any]): Unit = l match { case head :: tail => format(%s head) processList(tail) case Nil => println() } for (l < List(willWork willNotWork empty)) { print(List: ) processList(l) } processList 方法对List 参数l 进行匹配像下面这样开始一个方法定义可能看起来比较奇怪

def processList(l: List[Any]): Unit = l match { } 用省略号来隐藏细节以后应该会更加清楚一些processList 方法实际上是一个跨越了好几行的单指令

它先匹配head :: tail这时head 会被赋予这个列表的第一个元素tail 会被赋予列表剩余的部分也就是说我们使用:: 来从列表中解出头和尾当这个case 匹配的时候它打印出头然后递归调用processList 来处理列表尾

第二个case 匹配空列表Nil它打印出一行的最后一个字符然后终止递归

元组匹配(以及守卫)

另外如果我们只是想测试我们是否有一个有 个元素的元组我们可以进行元组匹配

// codeexamples/Rounding/matchtuplescriptscala val tupA = (Good Morning!) val tupB = (Guten Tag!) for (tup < List(tupA tupB)) { tup match { case (thingOne thingTwo) if thingOne == Good => println(A twotuple starting with Good) case (thingOne thingTwo) => println(This has two things: + thingOne + and + thingTwo) } } 例子里的第二个case我们已经解出了元组里的值并且附给了局部变量然后在结果表达式中使用了这些变量

在第一个case 里我们加入了一个新的概念守卫(Guard)这个元组后面的if 条件是一个守卫这个守卫会在匹配的时候进行评估但是只会解出本case 的变量守卫在构造cases 的时候提供了额外的尺度在这个例子里两个模式的唯一区别就是这个守卫表达式但是这样足够编译器来区分它们了

提示

回忆一下模式匹配的cases 会被按顺序依次被评估例如如果你的第一个case 比第二个case 更广那么第二个case 就不会被执行到(不可执行到的代码会导致一个编译错误)你可以在模式匹配的最后包含一个default 默认case可以使用下划线通配符或者有含义的变量名当使用变量时它不应该显式声明为任何类型除非是Any这样它才能匹配所有情况另外一方面尝试通过设计让你的代码规避这样全盘通吃的条件保证它只接受指定的意料之中的条目

Case 类匹配

让我们来尝试一次深度匹配在我们的模式匹配中检查对象的内容

// codeexamples/Rounding/matchdeepscriptscala case class Person(name: String age: Int) val alice = new Person(Alice ) val bob = new Person(Bob ) val charlie = new Person(Charlie ) for (person < List(alice bob charlie)) { person match { case Person(Alice ) => println(Hi Alice!) case Person(Bob ) => println(Hi Bob!) case Person(name age) => println(Who are you + age + yearold person named + name + ?) } } 从上面例子的输出中我们可以看出可怜的Charlie 被无视了

Hi Alice! Hi Bob! Who are you yearold person named Charlie? 我们收线定义了一个case 类一个特殊类型的类我们会在《第 Scala 高级面向对象编程》的Case 类章节中学到更多细节现在我们只需要知道一个case 类允许精炼的简单对象的构造以及一些预定义的方法然后我们的模式匹配通过检查传入的Person case 类的值来查找Alice 和BobCharlie 则直到最后那个饑不择食的case 才被捕获尽管他和Bob 有一样的年龄但是我们同时也检查了名字属性

我们后面会看到这种类型的模式匹配和Actor 配合工作时会非常有用Case 类经常会被作为消息发送到Actor对一个对象的内容进行深度模式匹配是分析这些消息的方便方式

正则表达式匹配

正则表达式用来从有着非正式结构的字符串中提取数据是很方便的但是对结构性数据(就是类似XML或者JSON 那样的格式)则不是正则表达式是几乎所有现代编程语言的共有特性之一通常被简称为regexes(regex 的复数Regular Expression 的简称)它们提供了一套简明的语法来说明复杂的匹配其中一种通常被翻译成后台状态机来获得优化的性能

如果已经在其它编程语言中使用正则表达式那么Scala 的应该不会让你感觉到惊讶让我们来看一个例子

// codeexamples/Rounding/matchregexscriptscala val BookExtractorRE = Book: title=([^]+)s+authors=(+)r val MagazineExtractorRE = Magazine: title=([^]+)s+issue=(+)r val catalog = List( Book: title=Programming Scala authors=Dean Wampler Alex Payne Magazine: title=The New Yorker issue=January Book: title=War and Peace authors=Leo Tolstoy Magazine: title=The Atlantic issue=February BadData: text=Who put this here?? ) for (item < catalog) { item match { case BookExtractorRE(title authors) => println(Book + title + written by + authors) case MagazineExtractorRE(title issue) => println(Magazine + title + issue + issue) case entry => println(Unrecognized entry: + entry) } } 我们从两个正则表达式开始其中一个记录书的信息另外一个记录杂志在一个字符串上调用r 会把它变成一个正则表达式我们是用了原始(三重引号)字符串来避免诸多双重转义的反斜槓如果你觉得字符串的r 转换方法不是很清晰你也可以通过创建Regex 类的实例来定义正则表达式比如new Regex(W)

注意每一个正则表达式都定义了两个捕捉组由括号表示每一个组捕获记录上的一个单独字段自如书的标题或者作者Scala 的正则表达式会把这些捕捉组翻译成抽取器每个匹配都会把捕获结果设置到到对应的字段去要是没有捕捉到就设为null

这在实际中有什么意义呢?如果提供给正则表达式的文本匹配了case BookExtractorRE(titleauthors) 会把第一个捕捉组赋给title第二个赋给authors我们可以在case 语句的右边使用这些值正如我们在上面的例子里看到的抽取器中的变量名title 和author 是随意的从捕捉组来的匹配结果会简单地从左往右被赋值你可以叫它们任何名字

这就是Scala 正则表达式的简要介绍scalautilmatchingRegex 类提供了几个方便的方法来查找和替代字符串中的匹配不管是所有的匹配还是第一个好好利用它们

我们不会在这里涵盖书写正则表达式的细节Scala 的Regex 类使用了对应平台的正则表达式API(就是Java或者NET 的)参考这些API 的文档来获取详细信息不同语言之间可能会存在微妙的差别

在Case 字句中绑定嵌套变量

有时候你希望能够绑定一个变量到匹配中的一个对象同时又能在嵌套的对象中指定匹配的标准我们修改一下前面一个例子来匹配map 的键值对我们把同样的Person 对象作为值员工ID 作为键我们会给Person 加一个属性- 角色用来指定对应的实例是类型层次结构中的哪一种

// codeexamples/Rounding/matchdeeppairscriptscala class Role case object Manager extends Role case object Developer extends Role case class Person(name: String age: Int role: Role) val alice = new Person(Alice Developer) val bob = new Person(Bob Manager) val charlie = new Person(Charlie Developer) for (item < Map( > alice > bob > charlie)) { item match { case (id p @ Person(_ _ Manager)) => format(%s is overpaidn p) case (id p @ Person(_ _ _)) => format(%s is underpaidn p) } } 这个case 对象和我们之前看到的单体对象一样就是多了一些特殊的case 类所属的行为我们最关心的是嵌套在case 子句的p @ Person() 我们在闭合元组里匹配特定类型的Person 对象我们同时希望把Person 赋给一个变量这样我们就能够打印它

Person(AliceDeveloper) is underpaid Person(BobManager) is overpaid Person(CharlieDeveloper) is underpaid 如果我们在Person 本身使用匹配标准我们可以直接写 p: Person例如前面的match 字句可以写成这样

item match { case (id p: Person) => prole match { case Manager => format(%s is overpaidn p) case _ => format(%s is underpaidn p) } } 主意p @ Person() 语法给了我们一个把嵌套的match 语句平坦化成一个语句的方法这类似于我们在正则表达式中使用捕捉组来提取我们需要的子字符串时来替代把一个字符串分隔成好几个的方法你可以使用任何一种你偏好的方法

使用trycatch 和finally 语句

通过使用函数式构造和强类型特性Scala 鼓励减少对异常和异常处理依赖的编程风格但是当Scala 和Java 交互时异常还是很普遍的

注意

Scala 不支持Java 那样的异常检查(Checked Exception)即使在Java 中异常是检查的在Scala 中也会被转换为未检查异常在方法的声明中也没有throws 子句不过有一个@throws 注解可以用于和Java 交互参见《第 应用程序设计》的注解章节

感谢Scala 实际上把异常处理作为了另外一种模式匹配允许我们在遇到多样化的异常时能做出更聪明的决定让我们实际地来看一个例子

// codeexamples/Rounding/trycatchscriptscala import javautilCalendar val then = null val now = CalendargetInstance() try { pareTo(then) } catch { case e: NullPointerException => println(One was null!); Systemexit() case unknown => println(Unknown exception + unknown); Systemexit() } finally { println(It all worked out) Systemexit() } 在上面的例子里我们显示地扑捉了NullPointerException 异常它在尝试把一个Calendar 实例和null 被抛出我们同时也将unknown 定义为捕捉所有异常的字句以防万一如果我们没有硬编码使得程序失败finally 块会被执行到用户会被告知一切正常

注意

你可以使用一个下划线(Scala 的标准通配符)作为占位符来捕捉任意类型的异常(不骗你它可以匹配模式匹配表达式的任何case)然而如此你就不能再访问下面表达式中的异常了如果需要你还可以命名那个异常例如如果你需要打印出异常信息就像我们在前一个例子中的全获性case 中做的一样e 或者ex 都是一个不错的名字

有了模式匹配Scala 异常操作的处理对于熟悉JavaRubyPython 和其它主流语言的人来说应该很容易上手而且一样的你可以通过写throw new MyBadException() 来抛出异常这就是有关异常的一切了

模式匹配结束语

模式匹配在使用恰当时是一个强大的优雅的从对象中抽取信息的方式回顾《第 分到Scala 介绍》中我们强调了模式匹配和多态之间的协作大多数时候你希望能够在清楚类结构的时候避免switch语句因为它们必须在每次结构改变的同时被改变

在我们的画画执行者(Actor)例子中我们使用了模式匹配来分离不同的消息种类但是我们使用了多态来画出我们传给它的图形我们可以修改Shape 继承结构Actor 部分的代码却不需要修改

在你遇到需要从对象内部提取数据的设计问题时模式匹配也有用但是仅限一些特殊的情况JavaBean 规格的一个没有预料到的结果是它鼓励人们通过getters 和setters 来暴露对象内部的字段这从来都不应该是一个默认的决策状态信息的存取应该被封装并且只在对于该类型有逻辑意义的时候被暴露和对其抽象的观察一致

相反地在你需要通过可控方式获取信息的时候考虑使用模式匹配正如我们即将在《第 Scala 高级面向对象编程》中的取消应用(Unapply)章节看到的我们所展示的模式匹配例子使用了预定义的unapply 方法来从实例中获取信息这些方法让你在不知道实现细节的同时获取了这些信息实际上unapply 方法返回的信息可能是实例中实际信息的变种

最后当设计模式匹配指令时对于默认case 的依赖要小心在什么情况下以上都不匹配才是正确的答案?它可能象征着设计需要被完善以便于你更精确地知道所有可能发生的匹配我们会在《第 Scala 对象系统》的完成类(sealed class)结构讨论完成类的结构时学习到其中一种技术

枚举

还记得我们上一个涉及到很多种狗的例子吗?在思考这些程序的类型时我们可能会需要一个顶层的Breed 类型来记录一定数量的breeds 这样一个类型被称为枚举类型它所包含的值被称为枚举值

虽然枚举是许多程序语言的内置支持Scala 走了一条不同的路把它作为标准库的一个类来实现这意味着Scala 中没有Java 和C# 那样特殊的枚举语法相反你只是定义个对象让它从Enumeration 类继承因此在字节码的层次Scala 枚举和JavaC# 中构造的枚举没有任何联系

这里有一个例子

// codeexamples/Rounding/enumerationscriptscala object Breed extends Enumeration { val doberman = Value(Doberman Pinscher) val yorkie = Value(Yorkshire Terrier) val scottie = Value(Scottish Terrier) val dane = Value(Great Dane) val portie = Value(Portuguese Water Dog) } // print a list of breeds and their IDs println(IDtBreed) for (breed < Breed) println(breedid + t + breed) // print a list of Terrier breeds println(nJust Terriers:) Breedfilter(_toStringendsWith(Terrier))foreach(println) 运行时你会得到如下输出

ID Breed Doberman Pinscher Yorkshire Terrier Scottish Terrier Great Dane Portuguese Water Dog Just Terriers: Yorkshire Terrier Scottish Terrier 你可以看到我们的Breed 枚举类型包含了几种Value 类型的值像下面的例子所展示的

val doberman = Value(Doberman Pinscher) 每一个声明实际上都调用了一个名为Value 的方法它接受一个字符串参数我们使用这个方法来给每一个枚举值赋一个长的品种名称也就是上面输出中的ValuetoString 方法所返回的值

注意类型和方法名字都为Value 并没有名称空间的沖突我们还有其他Value 方法的重载其中一个没有参数另外一个接受整型ID 值还有一个同时接收整型和字符串参数这些Value 方法返回一个Value 对象它们会把这些值加到枚举值的集合中去

实际上Scala 的枚举类支持和集合协作需要的一般方法所以我们可以简单地在循环中遍历这些种类或者通过名字过滤它们上面的输出也展示了枚举中的每一个Value 都被自动赋予一个数字标识除非你调用其中一个Value 方法显式指定ID 值

你常常希望给你的枚举值可读的名字就像我们这里做的一样然而有些时候你可能不需要它们这里有另一个从Scala 文档中中改编过来的枚举例子

// codeexamples/Rounding/daysenumerationscriptscala object WeekDay extends Enumeration { type WeekDay = Value val Mon Tue Wed Thu Fri Sat Sun = Value } import WeekDay_ def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun) WeekDay filter isWorkingDay foreach println 运行这段脚本会产生如下输出

Main$$anon$$WeekDay() Main$$anon$$WeekDay() Main$$anon$$WeekDay() Main$$anon$$WeekDay() Main$$anon$$WeekDay() 当名字没有用接受字符串的Value 方法构造的时候ValuetoString 打印出来的名字是由编译器生成的捆绑了自动生成的ID 值

注意我们导入了WeekDay_这使得每一个枚举值比如MonTue 等都暴露在了可见域里否则你必须写完整的WeekDayMonWeekDayTue 等

同时import 使得类型别名类型 WeekDay = Value 也暴露在了可见域里我们在isWorkingDay 方法中接受一个该类型的参数如果你不定义这样的别名你就要像这样声明这个方法 def isWorkingDay(d:WeekDayValue)

因为Scala 的枚举值是通常的对象你可以使用任何val 对象来指示不同的枚举值然而扩展枚举有几个优势比如它自动把所有值作为集合以供遍历等正如我们的例子所示它同时也自动对每一个值都赋予一个唯一的整型ID

Case 类(参见《第 Scala 高级面向对象编程》的Case 类章节)在Scala 经常被作为枚举的替代品因为它们的用例经常涉及到模式匹配我们会在《第 应用程序设计》的枚举 vs 模式匹配章节重温这个话题

概括及下章预告

我们已经在这章中涵盖了很多基础内容我们了解了Scala 的语法有多灵活以及它如何作用于创建域特定语言然后我们探索了Scala 增强的循环结构和条件表达式我们实验了不同的模式匹配作为对我们熟悉的caseswitch 指令的一种强大增强最后我们学习了如何封装枚举中的值

你现在应该准备好阅读更多的Scala 代码了但是这门语言仍然有很多的知识可以让你充实起来在下一章我们会探索Scala 对面向对象编程的支持从traits (特性)开始

上一篇:JBuilder2005实现重构之类内部提炼

下一篇:实践对jar包的代码签名