Scala编程指南 揭示Scala的本质

开发 后端
在上一章中我们从几个方面见识了Scala 简洁,可伸缩,高效的语法。我们也描述了许多Scala 的特性。本章,我们会在深入Scala 对面向对象编程和函数式编程的支持前,完成对Scala 本质的讲解。

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

51CTO推荐专题:Scala编程语言

Scala 本质

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

操作符?操作符?

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

  1. // code-examples/Rounding/one-plus-two-script.scala  
  2. 1 + 2 

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

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

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

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

1 不能使用保留字

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

2 普通标识符 - 字母,数字以及 ‘$’, ‘_’ , 操作符的组合

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

3 普通标识符 - 操作符

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

4 反引用字面值

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

5 模式匹配标识符

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

语法糖蜜

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

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

警告

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

不用点号和括号的方法

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

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

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

  1. // code-examples/Rounding/no-dot-script.scala  
  2. List(1, 2, 3) size 

很整洁,但是又令人疑惑。在什么时候这样的语法灵活性会变得有用呢?是当我们把方法调用链接成自表达性的,自我解释的语“句” 的时候:

  1. // code-examples/Rounding/no-dot-better-script.scala  
  2. def isEven(n: Int) = (n % 2) == 0  
  3. List(1, 2, 3, 4) filter isEven foreach println 

就像你所猜想的,运行上面的代码会产生如下输出:

  1. 24 

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

优先级规则

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

◆所有字母

◆|

◆^

◆&

◆< >

◆= !

◆:

◆+ -

◆* / %

◆所有其它特殊字符

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

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

  1. scala> 2.0 * 4.0 / 3.0 * 5.0res2: Double = 13.333333333333332  
  2. scala> (((2.0 * 4.0) / 3.0) * 5.0)res3: Double = 13.333333333333332 

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

  1. scala> val list = List('b', 'c', 'd')  
  2. list: List[Char] = List(b, c, d)  
  3. scala> 'a' :: list  
  4. res4: List[Char] = List(a, b, c, d) 

第二个表达式等效于list.::(a)。在一个右结合的方法调用序列中,它们从右向左绑定。那左绑定和有绑定混合的表达式呢?

  1. scala> 'a' :: list ++ List('e', 'f')  
  2. res5: List[Char] = List(a, b, c, d, e, f) 

(++ 方法链接了两个list。)在这个例子里,list 被加入到List(e,f) 中,然后a 被插入到前面来创建最后的list。通常我们最好加上括号来消除可能的不确定因素。

提示

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

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

#p#

领域特定语言

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

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

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

  1. // code-examples/Rounding/specs-script.scala  
  2. // Example fragment of a Specs script. Doesn't run standalone  
  3.  
  4. "nerd finder" should {  
  5.   "identify nerds from a List" in {  
  6.     val actors = List("Rick Moranis", "James Dean", "Woody Allen")  
  7.     val finder = new NerdFinder(actors)  
  8.     finder.findNerds mustEqual List("Rick Moranis", "Woody Allen")  
  9.   }  
  10. }  

注意这段代码和英语语法的相似性:“this should test that in the following scenario(这应该在以下场景中测试它)”,“this value must equal that value (这个值必须等于那个值)”,等等。这个例子使用了华丽的Specs 库,它提供了一套高效的DSL 来用于行为驱动开发测试和工程方法学。通过最大化利用Scala 的自有语法和诸多方法,Specs 测试组即使对于非开发人员来说也是可读的。

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

Scala if 指令

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

  1. // code-examples/Rounding/if-script.scala  
  2. if (2 + 2 == 5) {  
  3.   println("Hello from 1984.")  
  4. } else if (2 + 2 == 3) {  
  5.   println("Hello from Remedial Math class?")  
  6. } else {  
  7.   println("Hello from a non-Orwellian future.")  

在Scala 中与众不同的是,if 和其它几乎所有指令实际上都是表达式。所以,我们可以把一个if 表达式的结果赋值给其它(变量),像下面这个例子所展示的:

  1. // code-examples/Rounding/assigned-if-script.scala  
  2. val configFile = new java.io.File(".myapprc")  
  3. val configFilePath = if (configFile.exists()) {  
  4.   configFile.getAbsolutePath()  
  5. } else {  
  6.   configFile.createNewFile()  
  7.   configFile.getAbsolutePath()  

注意, if 语句是表达式,意味着它们有值。在这个例子里,configFilePath 的值就是if 表达式的值,它处理了配置文件不存在的情况,并且返回了文件的绝对路径。这个值现在可以在程序中被重用了,if 表达式的值只有在被使用到的时候才会被计算。

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

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

  1. scala> val configFile = new java.io.File("~/.myapprc")  
  2. configFile: java.io.File = ~/.myapprc  
  3. scala> val configFilePath = if (configFile.exists()) {  
  4.      |   configFile.getAbsolutePath()  
  5.      | }  
  6. configFilePath: Unit = ()  
  7. scala> 

注意现在configFilePath 是Unit 类型了。(之前是String。)类型推断选择了一个满足if 表达式所有结果的类型。Unit 是唯一的可能,因为没有值也是一个可能的结果。

Scala for 推导语句

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

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

一个简单的小狗例子

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

  1. // code-examples/Rounding/basic-for-script.scala  
  2. val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",  
  3.                      "Scottish Terrier", "Great Dane", "Portuguese Water Dog")  
  4. for (breed <- dogBreeds)  
  5.   println(breed) 

你可能已经猜到了,这段代码的意思是“对于列表dogBreeds 里面的每一个元素,创建一个临时变量叫breed,并赋予这个元素的值,然后打印出来。”把<- 操作符看作一个箭头,引导集合中一个一个的元素到那个我们会在for 表达式内部引用的局部变量中去。这个左箭头操作符被称为生成器,之所以这么叫是因为它从一个集合里产生独立的值来给一个表达式用。

过滤器

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

  1. // code-examples/Rounding/filtered-for-script.scala  
  2. val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",  
  3.                      "Scottish Terrier", "Great Dane", "Portuguese Water Dog")  
  4. for (breed <- dogBreeds  
  5.   if breed.contains("Terrier")  
  6. ) println(breed) 

如果需要给一个for 表达式添加多于一个的过滤器,用分号隔开它们:

  1. // code-examples/Rounding/double-filtered-for-script.scala  
  2. val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",  
  3.                      "Scottish Terrier", "Great Dane", "Portuguese Water Dog")  
  4. for (breed <- dogBreeds  
  5.   if breed.contains("Terrier");  
  6.   if !breed.startsWith("Yorkshire")  
  7. ) println(breed) 

现在你已经找到了所有不出生于约克郡的梗类犬,但愿也知道了过滤器在过程中是多么的有用。

产生器

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

提示

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

  1. // code-examples/Rounding/yielding-for-script.scala  
  2. val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",  
  3.                      "Scottish Terrier", "Great Dane", "Portuguese Water Dog")  
  4. val filteredBreeds = for {  
  5.   breed <- dogBreeds  
  6.   if breed.contains("Terrier")  
  7.   if !breed.startsWith("Yorkshire")  
  8. } yield breed 

在for 表达式的每一次循环中,被过滤的结果都会产生一个名为breed 的值。这些结果会随着每运行而累积,最后的结果集合被赋给值filteredBreeds(正如我们上面用if 指令做的那样)。由for-yield 表达式产生的集合类型会从被遍历的集合类型中推断。在这个例子里,filteredBreeds 的类型是List[String],因为它是类型为List[String] 的dogBreeds 列表的一个子集。

扩展的作用域

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

  1. // code-examples/Rounding/scoped-for-script.scala  
  2. val dogBreeds = List("Doberman", "Yorkshire Terrier", "Dachshund",  
  3.                      "Scottish Terrier", "Great Dane", "Portuguese Water Dog")  
  4. for {  
  5.   breed <- dogBreeds  
  6.   upcasedBreed = breed.toUpperCase()  
  7. } println(upcasedBreed) 

注意,即使没有声明upcaseBreed 为一个val,你也可以在你的for 表达式主体内部使用它。这个方法对于想在遍历集合的时候转换元素的时候来说是很理想的。

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

#p#

其它循环结构

Scala 有几种其它的循环结构

Scala while 循环

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

  1. // code-examples/Rounding/while-script.scala  
  2.  // WARNING: This script runs for a LOOOONG time!  
  3.  import java.util.Calendar  
  4.  def isFridayThirteen(cal: Calendar): Boolean = {  
  5.    val dayOfWeek = cal.get(Calendar.DAY_OF_WEEK)  
  6.    val dayOfMonth = cal.get(Calendar.DAY_OF_MONTH)  
  7.    // Scala returns the result of the last expression in a method  
  8.    (dayOfWeek == Calendar.FRIDAY) && (dayOfMonth == 13)  
  9.  }  
  10.  while (!isFridayThirteen(Calendar.getInstance())) {  
  11.    println("Today isn't Friday the 13th. Lame.")  
  12.    // sleep for a day  
  13.    Thread.sleep(86400000)  
  14.  } 

你可以在下面找到一张表,它列举了所有在while 循环中工作的条件操作符。

Scala do-while 循环

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

  1. // code-examples/Rounding/do-while-script.scala  
  2.  var count = 0 
  3.  do {  
  4.    count += 1  
  5.    println(count)  
  6.  } while (count < 10

这也展示了在Scala 中,遍历一个集合还有一种更优雅的方式,我们会在下一节看到。

生成器表达式

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

  1. // code-examples/Rounding/generator-script.scala  
  2.  for (i <- 1 to 10)  
  3.  println(i) 

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

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

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

注意

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

条件操作符

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

表格 3.1. 条件操作符

操作符 操作 描述
&& 在操作符左右的值都为真的时候结果为真。右边的值只有在左边的值为真的时候才会被评估(短路表达式)。
|| 或              操作符两边只要有一个为真结果就为真。右边的值只有在左边的值为假的时候才被评估(短路表达式)。
>        大于 左侧的值大于右侧的值时结果为真。
>= 大于等于 左侧的值大于等于右侧的值时结果为真。
< 小于 左侧的值小于右侧的值时结果为真。
<= 小于等于 左侧的值小于右侧的值时结果为真。
== 等于 左侧的值等于右侧的值时结果为真。
!= 不等于 左侧的值不等于右侧的值时结果为真。

注意&& 和|| 是“短路” 操作符。它们会在结果必然已知的情况下停止对表达式的评估。

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

#p#

模式匹配

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

一个简单的匹配

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

  1. // code-examples/Rounding/match-boolean-script.scala  
  2. val bools = List(true, false)  
  3. for (bool <- bools) {  
  4.   bool match {  
  5.     case true => println("heads")  
  6.     case false => println("tails")  
  7.     case _ => println("something other than heads or tails (yikes!)")  
  8.   }  

看起来很像C 风格的case 语句,对吧?唯一的区别是最后一个case 使用了下划线'_' 通配符。它匹配了所有上面的case 中没有定义的情况,所以它和Java、C# 中的switch 指令的default 关键字作用相同。

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

提示

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

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

匹配中的变量

  1. // code-examples/Rounding/match-variable-script.scala  
  2. import scala.util.Random  
  3. val randomInt = new Random().nextInt(10)  
  4. randomInt match {  
  5.   case 7 => println("lucky seven!")  
  6.   case otherNumber => println("boo, got boring ol' " + otherNumber)  

在这个例子里,我们把通配符匹配的值赋给了一个变量叫otherNumber,然后在下面的表达式中打印出来。如果我们生成了一个7,我们会对它称颂道德。反之,我们则诅咒它让我们经历了一个不幸运的数字。

类型匹配

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

  1. // code-examples/Rounding/match-type-script.scala  
  2. val sundries = List(23, "Hello", 8.5, 'q')  
  3. for (sundry <- sundries) {  
  4.   sundry match {  
  5.     case i: Int => println("got an Integer: " + i)  
  6.     case s: String => println("got a String: " + s)  
  7.     case f: Double => println("got a Double: " + f)  
  8.     case other => println("got something else: " + other)  
  9.   }  

这次,我们从一个元素为Any 类型的List 中拉出所有元素,包括了String,Double,Int,和Char。对于前三种类型,我们让用户知道我们拿到了那种类型以及它们的值。当我们拿到其它的类型(Char),我们简单地让用户知道值。我们可以添加更多的类型到那个列表,它们会被最后默认的通配符case 捕捉。

序列匹配

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

  1. // code-examples/Rounding/match-seq-script.scala  
  2. val willWork = List(1, 3, 23, 90)  
  3. val willNotWork = List(4, 18, 52)  
  4. val empty = List()  
  5. for (l <- List(willWork, willNotWork, empty)) {  
  6.   l match {  
  7.     case List(_, 3, _, _) => println("Four elements, with the 2nd being '3'.")  
  8.     case List(_*) => println("Any other list with 0 or more elements.")  
  9.   }  

在第二个case 里我们使用了一个特殊的通配符来匹配一个任意大小的List,甚至0个元素,任何元素的值都行。你可以在任何序列匹配的最后使用这个模式来解除长度制约。

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

  1. // code-examples/Rounding/match-list-script.scala  
  2. val willWork = List(1, 3, 23, 90)  
  3. val willNotWork = List(4, 18, 52)  
  4. val empty = List()  
  5. def processList(l: List[Any]): Unit = l match {  
  6.   case head :: tail => 
  7.     format("%s ", head)  
  8.     processList(tail)  
  9.   case Nil => println("")  
  10. }  
  11. for (l <- List(willWork, willNotWork, empty)) {  
  12.   print("List: ")  
  13.   processList(l)  

processList 方法对List 参数l 进行匹配。像下面这样开始一个方法定义可能看起来比较奇怪。

  1. def processList(l: List[Any]): Unit = l match {  
  2.   ...  

用省略号来隐藏细节以后应该会更加清楚一些。processList 方法实际上是一个跨越了好几行的单指令。

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

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

元组匹配(以及守卫)

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

  1. // code-examples/Rounding/match-tuple-script.scala  
  2. val tupA = ("Good", "Morning!")  
  3. val tupB = ("Guten", "Tag!")  
  4. for (tup <- List(tupA, tupB)) {  
  5.   tup match {  
  6.     case (thingOne, thingTwo) if thingOne == "Good" => 
  7.         println("A two-tuple starting with 'Good'.")  
  8.     case (thingOne, thingTwo) => 
  9.         println("This has two things: " + thingOne + " and " + thingTwo)  
  10.   }  

例子里的第二个case,我们已经解出了元组里的值并且附给了局部变量,然后在结果表达式中使用了这些变量。

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

提示

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

Case 类匹配

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

  1. // code-examples/Rounding/match-deep-script.scala  
  2. case class Person(name: String, age: Int)  
  3. val alice = new Person("Alice", 25)  
  4. val bob = new Person("Bob", 32)  
  5. val charlie = new Person("Charlie", 32)  
  6. for (person <- List(alice, bob, charlie)) {  
  7.   person match {  
  8.     case Person("Alice", 25) => println("Hi Alice!")  
  9.     case Person("Bob", 32) => println("Hi Bob!")  
  10.     case Person(name, age) => 
  11.       println("Who are you, " + age + " year-old person named " + name + "?")  
  12.   }  

从上面例子的输出中我们可以看出,可怜的Charlie 被无视了:

  1. Hi Alice!  
  2. Hi Bob!  
  3. Who are you, 32 year-old person named Charlie? 

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

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

正则表达式匹配

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

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

  1. // code-examples/Rounding/match-regex-script.scala  
  2. val BookExtractorRE = """Book: title=([^,]+),s+authors=(.+)""".r  
  3. val MagazineExtractorRE = """Magazine: title=([^,]+),s+issue=(.+)""".r  
  4. val catalog = List(  
  5.   "Book: title=Programming Scala, authors=Dean Wampler, Alex Payne",  
  6.   "Magazine: title=The New Yorker, issue=January 2009",  
  7.   "Book: title=War and Peace, authors=Leo Tolstoy",  
  8.   "Magazine: title=The Atlantic, issue=February 2009",  
  9.   "BadData: text=Who put this here??"  
  10. )  
  11. for (item <- catalog) {  
  12.   item match {  
  13.     case BookExtractorRE(title, authors) => 
  14.       println("Book "" + title + "", written by " + authors)  
  15.     case MagazineExtractorRE(title, issue) => 
  16.       println("Magazine "" + title + "", issue " + issue)  
  17.     case entry => println("Unrecognized entry: " + entry)  
  18.   }  

我们从两个正则表达式开始,其中一个记录书的信息,另外一个记录杂志。在一个字符串上调用.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 的文档来获取详细信息,不同语言之间可能会存在微妙的差别。

#p#

在Case 字句中绑定嵌套变量

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

  1. // code-examples/Rounding/match-deep-pair-script.scala  
  2. class Role  
  3. case object Manager extends Role  
  4. case object Developer extends Role  
  5. case class Person(name: String, age: Int, role: Role)  
  6.  
  7. val alice = new Person("Alice", 25, Developer)  
  8. val bob = new Person("Bob", 32, Manager)  
  9. val charlie = new Person("Charlie", 32, Developer)  
  10.  
  11. for (item <- Map(1 -> alice, 2 -> bob, 3 -> charlie)) {  
  12.   item match {  
  13.     case (id, p @ Person(_, _, Manager)) => format("%s is overpaid.n", p)  
  14.     case (id, p @ Person(_, _, _)) => format("%s is underpaid.n", p)  
  15.   }  
  16. }  
  17.  

这个case 对象和我们之前看到的单体对象一样,就是多了一些特殊的case 类所属的行为。我们最关心的是嵌套在case 子句的p @ Person(...) 。我们在闭合元组里匹配特定类型的Person 对象。我们同时希望把Person 赋给一个变量,这样我们就能够打印它。

  1. Person(Alice,25,Developer) is underpaid.  
  2. Person(Bob,32,Manager) is overpaid.  
  3. Person(Charlie,32,Developer) is underpaid.  
  4.  

如果我们在Person 本身使用匹配标准,我们可以直接写 p: Person。例如,前面的match 字句可以写成这样。

  1. item match {  
  2.   case (id, p: Person) => p.role match {  
  3.     case Manager => format("%s is overpaid.n", p)  
  4.     case _ => format("%s is underpaid.n", p)  
  5.   }  
  6. }  
  7.  

主意p @ Person(...) 语法给了我们一个把嵌套的match 语句平坦化成一个语句的方法。这类似于我们在正则表达式中使用“捕捉组”来提取我们需要的子字符串时来替代把一个字符串分隔成好几个的方法。你可以使用任何一种你偏好的方法。

使用try,catch 和finally 语句

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

注意

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

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

  1. // code-examples/Rounding/try-catch-script.scala  
  2. import java.util.Calendar  
  3. val then = null 
  4. val now = Calendar.getInstance()  
  5. try {  
  6.   now.compareTo(then)  
  7. } catch {  
  8.   case e: NullPointerException => println("One was null!"); System.exit(-1)  
  9.   case unknown => println("Unknown exception " + unknown); System.exit(-1)  
  10. } finally {  
  11.   println("It all worked out.")  
  12.   System.exit(0)  
  13. }  
  14.  

在上面的例子里,我们显示地扑捉了NullPointerException 异常,它在尝试把一个Calendar 实例和null 被抛出。我们同时也将unknown 定义为捕捉所有异常的字句,以防万一。如果我们没有硬编码使得程序失败,finally 块会被执行到,用户会被告知,一切正常。

注意

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

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

模式匹配结束语

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

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

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

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

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

枚举

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

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

这里有一个例子:

  1. // code-examples/Rounding/enumeration-script.scala  
  2. object Breed extends Enumeration {  
  3.   val doberman = Value("Doberman Pinscher")  
  4.   val yorkie = Value("Yorkshire Terrier")  
  5.   val scottie = Value("Scottish Terrier")  
  6.   val dane = Value("Great Dane")  
  7.   val portie = Value("Portuguese Water Dog")  
  8. }  
  9. // print a list of breeds and their IDs  
  10. println("IDtBreed")  
  11. for (breed <- Breed) println(breed.id + "t" + breed)  
  12. // print a list of Terrier breeds  
  13. println("nJust Terriers:")  
  14. Breed.filter(_.toString.endsWith("Terrier")).foreach(println)  
  15.  
  16.     
  17.  

运行时,你会得到如下输出:

  1. ID      Breed  
  2. 0       Doberman Pinscher  
  3. 1       Yorkshire Terrier  
  4. 2       Scottish Terrier  
  5. 3       Great Dane  
  6. 4       Portuguese Water Dog  
  7. Just Terriers:  
  8. Yorkshire Terrier  
  9. Scottish Terrier  
  10.  

你可以看到我们的Breed 枚举类型包含了几种Value 类型的值,像下面的例子所展示的。

  1. val doberman = Value("Doberman Pinscher")  
  2.  

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

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

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

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

  1. // code-examples/Rounding/days-enumeration-script.scala  
  2. object WeekDay extends Enumeration {  
  3.   type WeekDay = Value 
  4.   val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value 
  5. }  
  6. import WeekDay._  
  7. def isWorkingDay(d: WeekDay) = ! (d == Sat || d == Sun)  
  8. WeekDay filter isWorkingDay foreach println  
  9.  

运行这段脚本会产生如下输出:

  1. Main$$anon$1$WeekDay(0)  
  2. Main$$anon$1$WeekDay(1)  
  3. Main$$anon$1$WeekDay(2)  
  4. Main$$anon$1$WeekDay(3)  
  5. Main$$anon$1$WeekDay(4)  
  6.  

当名字没有用接受字符串的Value 方法构造的时候,Value。toString 打印出来的名字是由编译器生成的,捆绑了自动生成的ID 值。

注意我们导入了WeekDay._。这使得每一个枚举值,比如Mon,Tue 等都暴露在了可见域里。否则,你必须写完整的WeekDay.Mon,WeekDay.Tue 等。

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

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

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

概括,及下章预告

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

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

【编辑推荐】

  1. Scala语言编程入门指南
  2. Scala编程指南 更少的字更多的事
  3. “Scala” 一个有趣的语言
  4. 51CTO专访Scala创始人:Scala拒绝学术化
  5. 编程思想碰撞 Scala不是改良的Java

 

责任编辑:佚名 来源: 51CTO
相关推荐

2010-11-17 11:31:22

Scala基础面向对象Scala

2010-09-14 13:22:17

Scala编程指南Scala

2010-10-14 13:50:11

Scala

2010-09-14 14:28:58

Scala

2009-09-24 09:41:00

Scala讲座Scala

2009-07-08 12:43:59

Scala ServlScala语言

2020-10-31 17:33:18

Scala语言函数

2009-11-10 17:23:36

ScalaJDBCDAO

2010-03-11 10:34:22

Scala

2011-06-28 11:06:16

Scala

2009-02-06 09:08:04

Scala函数语言轻量级

2010-07-26 08:35:06

ScalaJava

2009-11-16 17:04:46

Inside Scal

2009-08-21 16:17:25

ScalaTwitter API

2009-09-28 11:01:39

从Java走进Scal

2009-07-09 00:25:00

ScalaListTuple

2009-07-09 00:25:00

ScalaSet类Map类

2009-10-19 11:26:08

Scala循环数组

2009-06-16 17:09:17

Scala面向对象函数编程

2020-12-29 06:44:18

GoScala编程语言
点赞
收藏

51CTO技术栈公众号