知识大全 Scala编程指南 揭示Scala的本质
Posted 表达式
篇首语:出门莫恨无人随,书中车马多如簇。本文由小常识网(cha138.com)小编为大家整理,主要介绍了知识大全 Scala编程指南 揭示Scala的本质相关的知识,希望对你有一定的参考价值。
Scala编程指南 揭示Scala的本质 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!
Scala 是一种基于JVM 集合了面向对象编程和函数式编程优点的高级程序设计语言 在《Scala编程指南 更少的字更多的事》中我们从几个方面见识了Scala 简洁 可伸缩 高效的语法 我们也描述了许多Scala 的特性 本文为《Programming Scala》第三章 我们会在深入Scala 对面向对象编程和函数式编程的支持前 完成对Scala 本质的讲解
CTO推荐专题 Scala编程语言
Scala 本质
在我们深入Scala 对面向对象编程以及函数式编程的支持之前 让我们来先完成将来可能在程序中用到的一些Scala 本质和特性的讨论
操作符?操作符?
Scala 一个重要的基础概念就是所有的操作符实际上都是方法 考虑下面这个最基础的例子
// code examples/Rounding/one plus o script scala + 两个数字之间的加号是个什么呢?是一个方法 第一 Scala 允许非字符型方法名称 你可以把你的方法命名为+ - $ 或者其它任何你想要的名字(译著 后面会提到例外) 第二 这个表达式等同于 +( ) (我们在 的后面加了一个空格因为 会被解释为Double 类型 )当一个方法只有一个参数的时候 Scala 允许你不写点号和括号 所以方法调用看起来就像是操作符调用 这被称为 中缀表示法 也就是操作符是在实例和参数之间的 我们会很快见到很多这样的例子
类似的 一个没有参数的方法也可以不用点号 直接调用 这被称为 后缀表示法
Ruby 和SmallTalk 程序员现在应该感觉和在家一样亲切了 因为那些语言的使用者知道 这些简单的规则有着广大的好处 它可以让你用自然的 优雅的方式来创建应用程序
那么 哪些字符可以被用在标识符里呢?这里有一个标识符规则的概括 它应用于方法 类型和变量等的名称 要获取更精确的细节描述 参见[ScalaSpec ] Scala 允许所有可打印的ASCII 字符 比如字母 数字 下划线 和美元符号$ 除了括号类的字符 比如 ( ) [ ] 和分隔类字符比如 ` ; 和 除了上面的列表 Scala 还允许其他在u 到u F 之间的字符 比如数学符号和 其它 符号 这些余下的字符被称为操作符字符 包括了 / < 等
不能使用保留字
正如大多数语言一样 你不能是用保留字作为标识符 我们在《第 章 打更少的字 做更多的事》的 保留字 章节列出了所有的保留字 回忆一下 其中有些保留字是操作符和标点的组合 比如说 简单的一个下划线( _ ) 是一个保留字!
普通标识符 字母 数字以及 $ _ 操作符的组合
和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 库提供了 操作符来渗入到文档结构中去 这些只是scala xml NodeSeq 类的方法而已
灵活的方法命名规则能让你写出就像Scala 原生扩展一样的库 你可以写一个数学类库 处理数字类型 加减乘除以及其它常见的数学操作 你也可以写一个新的行为类似Actors 的并发消息层 各种的可能性仅受到Scala 方法命名限制的约束
警告
别因为你可以就觉得你应该这么作 当用Scala 来设计你自己的库和API 的时候 记住 晦涩的标点和操作符会难以被程序员所记住 过量使用这些操作符会导致你的代码充满难懂的噪声 坚持已有的约定 当一个快捷符号没有在你脑海中成型的时候 清晰地把它拼出来吧
不用点号和括号的方法
为了促进阅读性更加的编程风格 Scala 在方法的括号使用上可谓是灵活至极 如果一个方法不用接受参数 你可以无需括号就定义它 调用者也必须不加括号地调用它 如果你加上了空括号 那么调用者可以有选择地加或者不加括号 例如 List 的size 方法没有括号 所以你必须写List( ) size 如果你尝试写List( ) size() 就会得到一个错误 然而 String 类的length 方法在定义时带有括号 所以 hello length() 和 hello length 都可以通过编译
Scala 社区的约定是 在没有副作用的前提下 省略调用方法时候的空括号 所以 查询一个序列的大小(size)的时候可以不用括号 但是定义一个方法来转换序列的元素则应该写上括号 这个约定给你的代码使用者发出了一个有潜在的巧妙方法的信号
当调用一个没有参数的方法 或者只有一个参数的方法的时候 还可以省略点号 知道了这一点 我们的List( ) size 例子就可以写成这样
// code examples/Rounding/no dot script scala List( ) size 很整洁 但是又令人疑惑 在什么时候这样的语法灵活性会变得有用呢?是当我们把方法调用链接成自表达性的 自我解释的语 句 的时候
// code examples/Rounding/no dot better script scala def isEven(n: Int) = (n % ) == List( ) filter isEven foreach println 就像你所猜想的 运行上面的代码会产生如下输出
Scala 这种对于方法的括号和点号不拘泥的方式为书写域特定语言(Domain Specific Language)定了基石 我们会在简短地讨论一下操作符优先级之后再来学习它
优先级规则
那么 如果这样一个表达式 * / * 实际上是Double 上的一系列方法调用 那么这些操作符的调用优先级规则是什么呢?这里从低到高表述了它们的优先级[ScalaSpec ]
◆所有字母
◆|
◆^
◆&
◆< >
◆= !
◆:
◆+
◆* / %
◆所有其它特殊字符
在同一行的字符拥有同样的优先级 一个例外是当= 作为赋值存在时 它拥有最低的优先级
因为* 和/ 有一样的优先级 下面两行scala 对话的行为是一样的
scala> * / * res : Double = scala> ((( * ) / ) * )res : Double = 在一个左结合的方法调用序列中 它们简单地进行从左到右的绑定 你说 左绑定 ?在Scala 中 任何以冒号: 结尾的方法实际上是绑定在右边的 而其它方法则是绑定在左边 举例来说 你可以使用:: 方法(称为 cons constructor 构造器的缩写)在一个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(e f) 中 然后a 被插入到前面来创建最后的list 通常我们最好加上括号来消除可能的不确定因素
提示
任何名字以: 结尾的方法都向右边绑定 而不是左边
最后 注意当你使用scala 命令的时候 无论是交互式还是使用脚本 看上去都好像可以在类型之外定义 全局 变量和方法 这其实是一个假象 解释器实际上把所有定义都包含在一个匿名的类型中 然后才去生成JVM 或者 NET CLR 字节码
领域特定语言
领域特定语言 也称为DSL 为特定的问题领域提供了一种方便的语意来表达自己的目标 比如 SQL 为处理与数据库打交道的问题 提供了刚刚好的编程语言功能 使之成为一个领域特定语言
有些DSL 像SQL 一样是自我包含的 而今使用成熟语言来实现DSL 使之成为母语言的一个子集变得流行起来 这允许程序员充分利用宿主语言来涵盖DSL 不能覆蓋到的边缘情况 而且节省了写词法分析器 解析器和其它语言基础的时间
Scala 的丰富 灵活的语法使得写DSL 轻而易举 你可以把下面的例子看作使用Specs 库(参见 Specs 章节)来编写行为驱动开发[BDD] 程序的风格
// code examples/Rounding/specs script scala // Example fragment of a Specs script Doesn t 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) finder findNerds 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 测试一个条件表达式 然后根据结果为真或假来跳转到响应语句块中 一个简单的例子
// code examples/Rounding/if script scala if ( + == ) println( Hello from ) else if ( + == ) println( Hello from Remedial Math class? ) else println( Hello from a non Orwellian future ) 在Scala 中与众不同的是 if 和其它几乎所有指令实际上都是表达式 所以 我们可以把一个if 表达式的结果赋值给其它(变量) 像下面这个例子所展示的
// code examples/Rounding/assigned if script scala val configFile = new java io File( myapprc ) val configFilePath = if (configFile exists()) configFile getAbsolutePath() else configFile createNewFile() configFile getAbsolutePath() 注意 if 语句是表达式 意味着它们有值 在这个例子里 configFilePath 的值就是if 表达式的值 它处理了配置文件不存在的情况 并且返回了文件的绝对路径 这个值现在可以在程序中被重用了 if 表达式的值只有在被使用到的时候才会被计算
因为在Scala 里if 语句是一个表达式 所以就不需要C 类型子语言的三重条件表达式了 你不会在Scala 里看到x ? doThis() : doThat() 这样的代码 因为Scala 提供了一个即强大又更具有可读性的机制
如果我们在上面的例子里省略else 字句会发生什么?在scala 解释器里输入下面的代码会告诉我们发生什么
scala> val configFile = new java io File( ~/ myapprc ) configFile: java io File = ~/ myapprc scala> val configFilePath = if (configFile exists()) | configFile getAbsolutePath() | configFilePath: Unit = () scala> 注意现在configFilePath 是Unit 类型了 (之前是String )类型推断选择了一个满足if 表达式所有结果的类型 Unit 是唯一的可能 因为没有值也是一个可能的结果
Scala for 推导语句
Scala 另外一个拥有丰富特性的类似控制结构是for 循环 在Scala 社区中也被称为for 推导语句或者for 表达式 语言的这个功能绝对对得起一个花哨的名字 因为它可以做一些很酷的戏法
实际上 术语推导(prehension)来自于函数式编程 它表达了这样个一个观点 我们正在遍历某种集合 推导 我们所发现的 然后从中计算出一些新的东西出来
一个简单的小狗例子
让我们从一个基本的for 表达式开始
// code examples/Rounding/basic for script scala val dogBreeds = List( Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog ) for (breed < dogBreeds) println(breed) 你可能已经猜到了 这段代码的意思是 对于列表dogBreeds 里面的每一个元素 创建一个临时变量叫breed 并赋予这个元素的值 然后打印出来 把< 操作符看作一个箭头 引导集合中一个一个的元素到那个我们会在for 表达式内部引用的局部变量中去 这个左箭头操作符被称为生成器 之所以这么叫是因为它从一个集合里产生独立的值来给一个表达式用
过滤器
那如果我们需要更细的粒度呢? Scala 的for 表达式通过过滤器来我们指定集合中的哪些元素是我们希望使用的 所以 要在我们的狗品种列表里找到所有的梗类犬 我们可以把上面的例子改成下面这样
// code examples/Rounding/filtered for script scala val dogBreeds = List( Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog ) for (breed < dogBreeds if ntains( Terrier ) ) println(breed) 如果需要给一个for 表达式添加多于一个的过滤器 用分号隔开它们
// code examples/Rounding/double filtered for script scala val dogBreeds = List( Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog ) for (breed < dogBreeds if ntains( Terrier ); if !breed startsWith( Yorkshire ) ) println(breed) 现在你已经找到了所有不出生于约克郡的梗类犬 但愿也知道了过滤器在过程中是多么的有用
产生器
如果说 你不想把过滤过的集合打印出来 而是希望把它放到程序的另外一部分去处理呢?yeild 关键字就是用for 表达式来生成新集合的关键 在下面的例子中 注意我们把for 表达式包裹在了一对大括号中 就像我们定义任何一个语句块一样
提示
for 表达式可以用括号或者大括号来定义 但是使用大括号意味着你不必用分号来分割你的过滤器 大部分时间里 你会在有一个以上过滤器 赋值的时候倾向使用大括号
// code examples/Rounding/yielding for script scala val dogBreeds = List( Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog ) val filteredBreeds = for breed < dogBreeds if ntains( Terrier ) if !breed startsWith( Yorkshire ) yield breed 在for 表达式的每一次循环中 被过滤的结果都会产生一个名为breed 的值 这些结果会随着每运行而累积 最后的结果集合被赋给值filteredBreeds(正如我们上面用if 指令做的那样) 由for yield 表达式产生的集合类型会从被遍历的集合类型中推断 在这个例子里 filteredBreeds 的类型是List[String] 因为它是类型为List[String] 的dogBreeds 列表的一个子集
扩展的作用域
Scala 的for 推导语句最后一个有用的特性是它有能力把在定义在for 表达式第一部分里的变量用在后面的部分里 这个例子是一个最好的说明
// code examples/Rounding/scoped for script scala val dogBreeds = List( Doberman Yorkshire Terrier Dachshund Scottish Terrier Great Dane Portuguese Water Dog ) for breed < dogBreeds upcasedBreed = breed toUpperCase() println(upcasedBreed) 注意 即使没有声明upcaseBreed 为一个val 你也可以在你的for 表达式主体内部使用它 这个方法对于想在遍历集合的时候转换元素的时候来说是很理想的
最后 在《第 章 应用程序设计》的 Options 和For 推导语句 章节 我们会看到使用Options 和for 推导语句可以大大地减少不必要的 null 和空判断 从而减少代码数量
其它循环结构
Scala 有几种其它的循环结构
Scala while 循环
和许多语言类似 while 循环在条件为真的时候会持续执行一段代码块 例如 下面的代码在下一个星期五 同时又是 号之前 每天打印一句抱怨的话
// code examples/Rounding/while script scala // WARNING: This script runs for a LOOOONG time! import java util Calendar def isFridayThirteen(cal: Calendar): Boolean = val dayOfWeek = cal get(Calendar DAY_OF_WEEK) val dayOfMonth = cal get(Calendar DAY_OF_MONTH) // Scala returns the result of the last expression in a method (dayOfWeek == Calendar FRIDAY) && (dayOfMonth == ) while (!isFridayThirteen(Calendar getInstance())) println( Today isn t Friday the th Lame ) // sleep for a day Thread sleep( ) 你可以在下面找到一张表 它列举了所有在while 循环中工作的条件操作符
Scala do while 循环
和上面的while 循环类似 一个do while 循环当条件表达式为真时持续执行一些代码 唯一的区别是do while 循环在运行代码块之后才进行条件检查 要从 数到 我们可以这样写
// code examples/Rounding/do while script scala var count = do count += println(count) while (count < ) 这也展示了在Scala 中 遍历一个集合还有一种更优雅的方式 我们会在下一节看到
生成器表达式
还记得我们在讨论for 循环的时候箭头操作符吗(< )?我们也可以让它在这里工作 让我们来整理一下上面的do while 循环
// code examples/Rounding/generator script scala for (i < to ) println(i) 这就是所有需要的了 是Scala 的RichInt(富整型)使得这个简洁的单行代码成为可能 编译器执行了一个隐式转换 把 一个Int (整型) 转换成了RichInt 类型 (我们会在《第 章 Scala 对象系统》的 Scala 类型结构 章节以及《第 章 Scala 函数式编程》的 隐式转换 章节中讨论这些转换 )RichInt 定义了以讹to 方法 它接受另外一个整数 然后返回一个Range Inclusive 的实例 也就是说 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 可以包含类型 通配符 序列 甚至是对象变量的深度检查
一个简单的匹配
让我们从模拟抛硬币匹配一个布尔值开始
// code examples/Rounding/match boolean script scala 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 中没有定义的情况 所以它和Java C# 中的switch 指令的default 关键字作用相同
模式匹配是贪婪的 只有第一个匹配的情况会赢 所以 如果你在所有case 前方一个case _ 语句 那么编译器会在下一个条件抛出一个 无法执行到的代码 的错误 因为没人能跨过那个default 条件
提示
使用case _ 来作为默认的 满足所有 的匹配
那如果我们希望获得匹配的变量呢?
匹配中的变量
// code examples/Rounding/match variable script scala import scala util Random val randomInt = new Random() nextInt( ) randomInt match case => println( lucky seven! ) case otherNumber => println( boo got boring ol + otherNumber) 在这个例子里 我们把通配符匹配的值赋给了一个变量叫otherNumber 然后在下面的表达式中打印出来 如果我们生成了一个 我们会对它称颂道德 反之 我们则诅咒它让我们经历了一个不幸运的数字
类型匹配
这些例子甚至还没有开始接触到Scala 的模式匹配特性的最表面 让我们来尝试一下类型匹配
// code examples/Rounding/match type script scala 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 中拉出所有元素 包括了String Double Int 和Char 对于前三种类型 我们让用户知道我们拿到了那种类型以及它们的值 当我们拿到其它的类型(Char) 我们简单地让用户知道值 我们可以添加更多的类型到那个列表 它们会被最后默认的通配符case 捕捉
序列匹配
鉴于用Scala 工作通常意味着和序列打交道 要是能和列表 数组的长度和内容来匹配岂不美哉?下面的例子就做到了 它测试了两个列表来检查它们是否包含 个元素 并且第二个元素是
// code examples/Rounding/match seq script scala 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 在一个列表前加入一个元素 你也可以使用这个操作符来从一个列表中解出头和尾
// code examples/Rounding/match list script scala 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 它打印出一行的最后一个字符 然后终止递归
元组匹配(以及守卫)
另外 如果我们只是想测试我们是否有一个有 个元素的元组 我们可以进行元组匹配
// code examples/Rounding/match tuple script scala val tupA = ( Good Morning! ) val tupB = ( Guten Tag! ) for (tup < List(tupA tupB)) tup match case (thingOne thingTwo) if thingOne == Good => println( A o tuple starting with Good ) case (thingOne thingTwo) => println( This has o things: + thingOne + and + thingTwo) 例子里的第二个case 我们已经解出了元组里的值并且附给了局部变量 然后在结果表达式中使用了这些变量
在第一个case 里 我们加入了一个新的概念 守卫(Guard) 这个元组后面的if 条件是一个守卫 这个守卫会在匹配的时候进行评估 但是只会解出本case 的变量 守卫在构造cases 的时候提供了额外的尺度 在这个例子里 两个模式的唯一区别就是这个守卫表达式 但是这样足够编译器来区分它们了
提示
回忆一下 模式匹配的cases 会被按顺序依次被评估 例如 如果你的第一个case 比第二个case 更广 那么第二个case 就不会被执行到 (不可执行到的代码会导致一个编译错误 )你可以在模式匹配的最后包含一个 default 默认case 可以使用下划线通配符 或者有含义的变量名 当使用变量时 它不应该显式声明为任何类型 除非是Any 这样它才能匹配所有情况 另外一方面 尝试通过设计让你的代码规避这样全盘通吃的条件 保证它只接受指定的意料之中的条目
Case 类匹配
让我们来尝试一次深度匹配 在我们的模式匹配中检查对象的内容
// code examples/Rounding/match deep script scala 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 + year old person named + name + ? ) 从上面例子的输出中我们可以看出 可怜的Charlie 被无视了
Hi Alice! Hi Bob! Who are you year old person named Charlie? 我们收线定义了一个case 类 一个特殊类型的类 我们会在《第 章 Scala 高级面向对象编程》的 Case 类 章节中学到更多细节 现在 我们只需要知道 一个case 类允许精炼的简单对象的构造 以及一些预定义的方法 然后 我们的模式匹配通过检查传入的Person case 类的值来查找Alice 和Bob Charlie 则直到最后那个饥不择食的case 才被捕获 尽管他和Bob 有一样的年龄 但是我们同时也检查了名字属性
我们后面会看到 这种类型的模式匹配和Actor 配合工作时会非常有用 Case 类经常会被作为消息发送到Actor 对一个对象的内容进行深度模式匹配是 分析 这些消息的方便方式
正则表达式匹配
正则表达式用来从有着非正式结构的字符串中提取数据是很方便的 但是对 结构性数据 (就是类似XML 或者JSON 那样的格式)则不是 正则表达式是几乎所有现代编程语言的共有特性之一 通常被简称为regexes(regex 的复数 Regular Expression 的简称) 它们提供了一套简明的语法来说明复杂的匹配 其中一种通常被翻译成后台状态机来获得优化的性能
如果已经在其它编程语言中使用正则表达式 那么Scala 的应该不会让你感觉到惊讶 让我们来看一个例子
// code examples/Rounding/match regex script scala 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(title authors) 会把第一个捕捉组赋给title 第二个赋给authors 我们可以在case 语句的右边使用这些值 正如我们在上面的例子里看到的 抽取器中的变量名title 和author 是随意的 从捕捉组来的匹配结果会简单地从左往右被赋值 你可以叫它们任何名字
这就是Scala 正则表达式的简要介绍 scala util matching Regex 类提供了几个方便的方法来查找和替代字符串中的匹配 不管是所有的匹配还是第一个 好好利用它们
我们不会在这里涵盖书写正则表达式的细节 Scala 的Regex 类使用了对应平台的正则表达式API(就是Java 或者 NET 的) 参考这些API 的文档来获取详细信息 不同语言之间可能会存在微妙的差别
在Case 字句中绑定嵌套变量
有时候你希望能够绑定一个变量到匹配中的一个对象 同时又能在嵌套的对象中指定匹配的标准 我们修改一下前面一个例子 来匹配map 的键值对 我们把同样的Person 对象作为值 员工ID 作为键 我们会给Person 加一个属性- 角色 用来指定对应的实例是类型层次结构中的哪一种
// code examples/Rounding/match deep pair script scala 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 overpaid n p) case (id p @ Person(_ _ _)) => format( %s is underpaid n p) 这个case 对象和我们之前看到的单体对象一样 就是多了一些特殊的case 类所属的行为 我们最关心的是嵌套在case 子句的p @ Person( ) 我们在闭合元组里匹配特定类型的Person 对象 我们同时希望把Person 赋给一个变量 这样我们就能够打印它
Person(Alice Developer) is underpaid Person(Bob Manager) is overpaid Person(Charlie Developer) is underpaid 如果我们在Person 本身使用匹配标准 我们可以直接写 p: Person 例如 前面的match 字句可以写成这样
item match case (id p: Person) => p role match case Manager => format( %s is overpaid n p) case _ => format( %s is underpaid n p) 主意p @ Person( ) 语法给了我们一个把嵌套的match 语句平坦化成一个语句的方法 这类似于我们在正则表达式中使用 捕捉组 来提取我们需要的子字符串时来替代把一个字符串分隔成好几个的方法 你可以使用任何一种你偏好的方法
使用try catch 和finally 语句
通过使用函数式构造和强类型特性 Scala 鼓励减少对异常和异常处理依赖的编程风格 但是当Scala 和Java 交互时 异常还是很普遍的
注意
Scala 不支持Java 那样的异常检查(Checked Exception) 即使在Java 中异常是检查的 在Scala 中也会被转换为未检查异常 在方法的声明中也没有throws 子句 不过 有一个@throws 注解可以用于和Java 交互 参见《第 章 应用程序设计》的 注解 章节
感谢Scala 实际上把异常处理作为了另外一种模式匹配 允许我们在遇到多样化的异常时能做出更聪明的决定 让我们实际地来看一个例子
// code examples/Rounding/try catch script scala import java util Calendar val then = null val now = Calendar getInstance() try pareTo(then) catch case e: NullPointerException => println( One was null! ); System exit( ) case unknown => println( Unknown exception + unknown); System exit( ) finally println( It all worked out ) System exit( ) 在上面的例子里 我们显示地扑捉了NullPointerException 异常 它在尝试把一个Calendar 实例和null 被抛出 我们同时也将unknown 定义为捕捉所有异常的字句 以防万一 如果我们没有硬编码使得程序失败 finally 块会被执行到 用户会被告知 一切正常
注意
你可以使用一个下划线(Scala 的标准通配符)作为占位符来捕捉任意类型的异常(不骗你 它可以匹配模式匹配表达式的任何case) 然而 如此你就不能再访问下面表达式中的异常了 如果需要 你还可以命名那个异常 例如 如果你需要打印出异常信息 就像我们在前一个例子中的全获性case 中做的一样 e 或者ex 都是一个不错的名字
有了模式匹配 Scala 异常操作的处理对于熟悉Java Ruby Python 和其它主流语言的人来说应该很容易上手 而且一样的 你可以通过写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 枚举和Java C# 中构造的枚举没有任何联系
这里有一个例子
// code examples/Rounding/enumeration script scala 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(breed id + t + breed) // print a list of Terrier breeds println( nJust Terriers: ) Breed filter(_ toString endsWith( 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 的方法 它接受一个字符串参数 我们使用这个方法来给每一个枚举值赋一个长的品种名称 也就是上面输出中的Value toString 方法所返回的值
注意类型和方法名字都为Value 并没有名称空间的冲突 我们还有其他Value 方法的重载 其中一个没有参数 另外一个接受整型ID 值 还有一个同时接收整型和字符串参数 这些Value 方法返回一个Value 对象 它们会把这些值加到枚举值的集合中去
实际上 Scala 的枚举类支持和集合协作需要的一般方法 所以我们可以简单地在循环中遍历这些种类 或者通过名字过滤它们 上面的输出也展示了枚举中的每一个Value 都被自动赋予一个数字标识 除非你调用其中一个Value 方法显式指定ID 值
你常常希望给你的枚举值可读的名字 就像我们这里做的一样 然而 有些时候你可能不需要它们 这里有另一个从Scala 文档中中改编过来的枚举例子
// code examples/Rounding/days enumeration script scala 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 方法构造的时候 Value toString 打印出来的名字是由编译器生成的 捆绑了自动生成的ID 值
注意我们导入了WeekDay _ 这使得每一个枚举值 比如Mon Tue 等都暴露在了可见域里 否则 你必须写完整的WeekDay Mon WeekDay Tue 等
同时 import 使得类型别名 类型 WeekDay = Value 也暴露在了可见域里 我们在isWorkingDay 方法中接受一个该类型的参数 如果你不定义这样的别名 你就要像这样声明这个方法 def isWorkingDay(d:WeekDay Value)
因为Scala 的枚举值是通常的对象 你可以使用任何val 对象来指示不同的 枚举值 然而 扩展枚举有几个优势 比如它自动把所有值作为集合以供遍历等 正如我们的例子所示 它同时也自动对每一个值都赋予一个唯一的整型ID
Case 类(参见《第 章 Scala 高级面向对象编程》的 Case 类 章节)在Scala 经常被作为枚举的替代品 因为它们的用例经常涉及到模式匹配 我们会在《第 章 应用程序设计》的 枚举 vs 模式匹配 章节重温这个话题
概括 及下章预告
我们已经在这章中涵盖了很多基础内容 我们了解了Scala 的语法有多灵活 以及它如何作用于创建域特定语言 然后 我们探索了Scala 增强的循环结构和条件表达式 我们实验了不同的模式匹配 作为对我们熟悉的case switch 指令的一种强大增强 最后我们学习了如何封装枚举中的值
cha138/Article/program/Java/hx/201311/25822相关参考
知识大全 面向Java开发人员的Scala指南: 深入了解Scala并发性 了解 Scala 如何简化并发编
面向Java开发人员的Scala指南:深入了解Scala并发性了解Scala如何简化并发编 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后
知识大全 面向Java开发人员的Scala指南: Scala和servlet
面向Java开发人员的Scala指南:Scala和servlet 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起
知识大全 Java开发者的Scala指南: Scala+Twitter=Scitter
Java开发者的Scala指南:Scala+Twitter=Scitter 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我
知识大全 面向Java开发人员的Scala指南: Scala控制结构内部揭密
面向Java开发人员的Scala指南:Scala控制结构内部揭密 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起
知识大全 面向Java开发人员的Scala指南: 深入了解Scala并发性 了解 actor 如何提供新的应
面向Java开发人员的Scala指南:深入了解Scala并发性了解actor如何提供新的应 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后
Scala编程语言简介 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! Scala编程语言近来抓住
面向Java开发人员的Scala指南:实现继承 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧! S
知识大全 面向Java开发人员的Scala指南: 增强Scitter库
面向Java开发人员的Scala指南:增强Scitter库 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一
知识大全 面向Java开发人员的Scala指南: 关于特征和行为
面向Java开发人员的Scala指南:关于特征和行为 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!
知识大全 面向Java开发人员的Scala指南: 包和访问修饰符
面向Java开发人员的Scala指南:包和访问修饰符 以下文字资料是由(全榜网网www.cha138.com)小编为大家搜集整理后发布的内容,让我们赶快一起来看一下吧!