中国领先的IT技术网站
|
|

Scala编程指南 更少的字更多的事

本文为《Programming Scala》的中文译文《Scala 编程指南》的第二章,在前文中我们已经简单的介绍了Scala语言的基础以及安装、试用,在本章中我们将详细介绍如何使用Scala 来写出精炼的,灵活的代码。

作者:bbsmrdj译来源:51CTO|2010-09-14 14:28


本文为《Programming Scala》的中文译文《Scala 编程指南》的第二章,在《Scala语言编程入门指南》我们介绍了Scala语言编程的入门,在上一章中我们以几个撩拨性质的Scala 代码范例作为章节结束,在本章中我们将详细介绍如何使用Scala 来写出精炼的,灵活的代码。

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

章节概要

在这一章我们将讨论如何使用Scala 来写出精炼的,灵活的代码。我们会讨论文件和包的组织结构,导入其他的类型和变量声明,一些语法习惯和概念。我们会着重讨论Scala 简明的语法如何帮助你更好更快地工作。

Scala 的语法对于书写脚本特别有用。单独的编译和运行步骤对于简单的,仅有少量独立于Scala 提供的库之外的程序不是必须的。你可以用scala 命令一次性编译和运行这些程序。如果你已经下载了本书的实例代码,它们中的许多小程序可以用scala 命令来运行,比如scala filename.scala。参见每一章节代码实例中的README.txt 可以获取更多细节。也可以参见《第14章 - Scala 工具,库和IDE 支持》中的“命令行工具”章节来获取更多使用scala 命令的信息。

分号

你可能已经注意到,在上一章的代码示例中很少有分号出现。你可以使用分号来隔离各个声明和表达式,就像Java,C,PHP 以及其他类似的语言一样。然而在大多数情况下,Scala 的行为和许多脚本语言一样把一行的结尾看作是声明或者表达式的结尾。当一个声明或者表达式太长,一行不够的时候,Scala 通常可以推断你在什么时候要在下一行继续,就像这个例子中一样。

  1. // code-examples/TypeLessDoMore/semicolon-example-script.scala  
  2.  
  3. // Trailing equals sign indicates more code on next line  
  4. def equalsign = {  
  5.   val reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine =  
  6.     "wow that was a long value name" 
  7.   println(reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine)  
  8. }  
  9.  
  10. // Trailing opening curly brace indicates more code on next line  
  11. def equalsign2(s: String) = {  
  12.   println("equalsign2: " + s)  
  13. }  
  14.  
  15. // Trailing comma, operator, etc. indicates more code on next line  
  16. def commas(s1: String,  
  17.            s2: String) = {  
  18.   println("comma: " + s1 +  
  19.           ", " + s2)  
  20. }  

当你需要在同一行中放置多个声明或者表达式的时候,你可以使用分号来隔开它们。我们在《第1章 - 从0分到60分:Scala 介绍》的“初尝并发”章节中的ShapeDrawingActor 示例里面使用了这样的技巧。

  1. case "exit" => println("exiting..."); exit 

这样的代码也可以写成如下的样子。

  1. ...  
  2. case "exit" => 
  3.   println("exiting...")  
  4.   exit  
  5. ... 

你可能会想为什么在case... => 这行的后面不需要用大括号{ } 把两个语句括起来。。如果你想,你可以这么做,但是编译器其实会知道你什么时候会到达语句块的结尾,因为它会看到下一个case 块或者终结所有case 块的大括号。

省略可选的分号意味着更少的符号输入和更少的符号混乱。把各个语句隔离到它们自己单独的行上面可以提高你的代码的可读性。

变量声明

当你声明一个变量的时候,Scala 允许你来决定它是不变的(只读的)还是可变的(可读写的)。一个不变的“变量”可以用val 关键字来声明(想象一个值对象)。

  1. val array: Array[String] = new Array(5) 

更准确的说,这个引用array 不能被修改指向另外一个Array (数组),但是这个数组本身可以被修改,正如下面的scala 会话中演示的。

  1. scala> val array: Array[String] = new Array(5)  
  2. array: Array[String] = Array(null, null, null, null, null)  
  3. scala> array = new Array(2)  
  4. :5: error: reassignment to val  
  5.        array = new Array(2)  
  6.         ^  
  7. scala> array(0) = "Hello"  
  8. scala> array  
  9. res3: Array[String] = Array(Hello, null, null, null, null)  
  10. scala> 

一个不变的val 必须被初始化,也就是说在声明的时候就必须定义。

一个可变的变量用关键字var 来声明。

  1. scala> var stockPrice: Double = 100.  
  2. stockPrice: Double = 100.0  
  3. scala> stockPrice = 10.  
  4. stockPrice: Double = 10.0  
  5. scala> 

Scala 同时也要求你在声明一个var 时将其初始化。你可以在需要的时候给一个var 赋予新的值。这里再次严谨说明一下:引用stockPrice 可以被修改指向一个不一样的Double 对象(比如10)。在这个例子里,stockPrice 引用的对象不能被修改,因为Double 在Scala 里是不可变的。

在这里,对于val 和var 声明时即定义的规则有一些例外。这两个关键字都可以被用作构造函数参数。当作为构造函数参数时,这些可变或者不可变的变量会在一个对象被实例化的时候被初始化。两个关键字可以在抽象类型中被用来声明“抽象”(没有初始化的)的变量。同时,继承类型可以重写在父类型中声明的值。我们会在《第5章 - Scala 基础面向对象编程》中讨论这些例外。

Scala 鼓励你在任何可能的时候使用不可变的值。正如我们即将看到的,这会促进更佳的面向对象设计,而且这和“纯”函数式编程的原则相一致。

注意

var 和val 关键字指定了该引用能否被修改指向另一个对象。它们并不指定它们引用的对象是否可变。

方法声明

我们在《第1章 - 从0分到60分:Scala 介绍》中见到了几个如何定义方法的例子,它们都是类的成员函数。方法定义由一个def 关键字开始,紧接着是可选的参数列表,一个冒号“:” 和方法的返回类型,一个等于号“=”,最后是方法的主体。如果你不写等于号和方法主体,那么方法会被隐式声明为“抽象”。包含它的类型于是也是一个抽象类型。我们会在《第5章,Scala 基础面向对象编程》中详细讨论抽象类型。

我们刚才说到“可选的参数列表”,这意味着一个或更多。Scala 可以让你为方法定义一个以上的参数列表。这是级联方法(currying methods)所需要的。我们会在《第8章 - Scala 函数式编程》中的“级联(Currying)章节讨论它。这个功能对于定义你自己的域特定语言(DSLs)也很有帮助。我们会在《第11章 - Scala 中的域特定语言》 中看到它。注意,每一个参数列表会被括号所包围,并且所有的参数由逗号隔开。

如果一个方法的主体包含多于一个的表达式,你必须用大括号{ } 来把它们包起来。你可以在方法主体只有一个表达式的时候省略大括号。

方法的默认参数和命名参数(Scala 版本2.8)

许多语言都允许你为一个方法的一个或多个参数定义默认值。考虑下面的脚本,一个StringUtil 对象允许你用一个用户定义的分隔符来连接字符串。

  1. // code-examples/TypeLessDoMore/string-util-v1-script.scala  
  2. // Version 1 of "StringUtil".  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String): String =  
  5.     strings.mkString(separator)  
  6.   def joiner(strings: List[String]): String = joiner(strings, " ")  
  7. }  
  8. import StringUtil._  // Import the joiner methods.  
  9. println( joiner(List("Programming", "Scala")) ) 

实际上,有两个“重载”的jioner 方法。第二个方法使用了一个空格作为“默认”分隔符。写两个函数似乎有点浪费,如果我们能消除第二个joiner 方法,在第一个jioner 方法里为separator 参数声明一个默认值,那就太好了。事实上,在Scala 2.8 版本里,你可以这么做。

  1. // code-examples/TypeLessDoMore/string-util-v2-v28-script.scala  
  2. // Version 2 of "StringUtil" for Scala v2.8 only.  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String = " "): String =  
  5.     strings.mkString(separator)  
  6. }  
  7. import StringUtil._  // Import the joiner methods.println(joiner(List("Programming", "Scala")))  

对于早些版本的Scala 还有另外一种选择。你可以使用隐式参数,我们会在《第8章 - Scala 函数式编程》的“隐式函数参数”章节讨论。

2.8 版本的Scala 提供了另外一种对方法参数列表进行增强,就是命名参数。我们实际上可以用多种方法重写上一个例子的最后一行。下面所有的println 语句在功能上都是一致的。

  1. println(joiner(List("Programming", "Scala")))  
  2. println(joiner(strings = List("Programming", "Scala")))  
  3. println(joiner(List("Programming", "Scala"), " "))   // #1  
  4. println(joiner(List("Programming", "Scala"), separator = " ")) // #2  
  5. println(joiner(strings = List("Programming", "Scala"), separator = " ")) 

为什么这样有用呢?第一,如果你为方法参数选择了好的名字,那么你对那些函数的调用事实上为每一个参数记载了一个名字。举例来说,比较注释#1 和#2 的两行。在第一行,第二个参数“ ”的用处可能不是很明显。在第二行中,我们提供了参数名separator,同时也暗示了参数的用处。

第二个好处则是你可以以任何顺序指定参数的顺序。结合默认值,你可以像下面这样写代码

  1. // code-examples/TypeLessDoMore/user-profile-v28-script.scala  
  2. // Scala v2.8 only.  
  3. object OptionalUserProfileInfo {  
  4.   val UnknownLocation = "" 
  5.   val UnknownAge = -1  
  6.   val UnknownWebSite = "" 
  7. }  
  8.  
  9. class OptionalUserProfileInfo(  
  10.   location: String = OptionalUserProfileInfo.UnknownLocation,  
  11.   age: Int         = OptionalUserProfileInfo.UnknownAge,  
  12.   webSite: String  = OptionalUserProfileInfo.UnknownWebSite)  
  13.  
  14. println( new OptionalUserProfileInfo )  
  15. println( new OptionalUserProfileInfo(age = 29) )  
  16. println( new OptionalUserProfileInfo(age = 29location="Earth") )  

OptionalUserProfileInfo 为你的下一个Web 2.0 社交网站提供了“可选的”用户概要信息。它定义了所有字段的默认值。这段脚本在创建实例的时候提供了0个或者更多的命名参数。而参数的顺序却是任意的。

在这个我们展示的例子里,常量值被用来作为默认值。大多数支持默认参数的语言只允许编译时能决定的常量或者值作为默认值。然而,在Scala 里,任何表达式都可以被作为默认值,只要它可以在被使用的时候正确编译。比如说,一个表达式不能引用类或者对象主体内才被计算的实例字段,但是它可以引用一个方法或者一个单例对象。

一个类似的限制是一个参数的默认表达式不能引用列表中的另外一个参数,除非被引用的参数出现在列表的更前面,或者参数已经被级联(我们会在《第8章 - Scala 函数式编程》的“级联”这一章节详细讨论)。

最后,还有一个对命名参数的约束就是一旦你为一个方法掉哦那个指定了参数名称,那么剩下的在这个参数之后的所有参数都必须是命名参数。比如,new OptionalUserProfileInfo(age =29, "Earch") 就不能被编译,因为第二个参数不是通过命名方式调用的。

我们会在《第6章 - Scala 高级面向对象编程》中的“Case Class(案例类)”中看到另外一个使用命名参数和默认参数的例子。

嵌套方法定义

方法定义也可以被嵌套。这里是一个阶乘计算器的实现,我们会使用一种常规的方法,通过调用第二个,嵌套的方法来完成计算。

  1. // code-examples/TypeLessDoMore/factorial-script.scala  
  2. def factorial(i: Int): Int = {  
  3.   def fact(i: Int, accumulator: Int): Int = {  
  4.     if (i <= 1)  
  5.       accumulator  
  6.     else  
  7.       fact(i - 1, i * accumulator)  
  8.   }  
  9.   fact(i, 1)  
  10. }  
  11.  
  12. println( factorial(0) )  
  13. println( factorial(1) )  
  14. println( factorial(2) )  
  15. println( factorial(3) )  
  16. println( factorial(4) )  
  17. println( factorial(5) )  

第二个方法递归地调用了自己,传递一个accumulator 参数,这个参数是计算结果累积的地方。注意,我们当计数器i 达到1 的时候返回了累积的值。(我们会忽略负整数。实际上这个函数在i<0 的时候会返回1 。)在嵌套方法的定义后面,factorial 以传入值i 和初始accumulator 值1 来调用它。

就像很多语言中声明局部变量一样,一个嵌套方法尽在方法内部可见。如果你尝试在factorial 之外去调用fact,你会得到一个编译错误。

你注意到了吗,我们两次把i 作为一个参数名字,第一次是在factorial 方法里,然后是在嵌套的fact 方法里。就像在其它许多语言中一样,在fact 中的i 参数会屏蔽掉外面factorial 的i 参数。这样很好,因为我们在fact 中不需要在外面的i 的值。我们只在第一次调用fact 的时候需要它,也就是在factorial 的最后。

那如果我们需要使用定义在嵌套函数外面的变量呢?考虑下面的例子。

  1. // code-examples/TypeLessDoMore/count-to-script.scala  
  2. def countTo(n: Int):Unit = {  
  3.   def count(i: Int): Unit = {  
  4.     if (i <= n) {  
  5.       println(i)  
  6.       count(i + 1)  
  7.     }  
  8.   }  
  9.   count(1)  
  10. }  
  11. countTo(5) 

注意嵌套方法count 使用了作为参数传入countTo 的n 的值。这里没有必要把n 作为参数传给count。因为count 嵌套在countTo 里面,所以n对于count 来说是可见的。

字段(成员变量)的声明可以用可见程度关键字来做前缀,就像Java 和C# 这样的语言一样。和非嵌套方法的生命类似,这些嵌套方法也可以用这些关键字来修饰。我们会在《第5章 - Scala 面向对象编程》中的“可见度规则”章节来讨论可见度的规则和对应的关键字。

类型推断

静态类型书写的代码可能会非常冗长,考虑下面典型的Java 声明。

  1. import java.util.Map;  
  2. import java.util.HashMap;  
  3. ...  
  4. Map intToStringMap = new HashMap(); 

我们不得不两次指明参数类型。(Scala 使用类型注解作为显式类型声明的方式,比如HashMap。)

Scala 支持类型推断(参考,例如[ 类型推断] 和[Pierce2002,Benjamin C. Pierce, 类型与编程语言, 麻省理工出版社, 2002])。即使没有显示的类型注解,语言的编译器仍可以从上下文中分辨出相当多的类型信息。这里是Scala 的声明,使用了类型信息的推断。

  1. import java.util.Map  
  2. import java.util.HashMap  
  3. ...  
  4. val intToStringMap: Map[Integer, String] = new HashMap 

回忆在第1章中Scala 使用方括号来指明范型类型参数。我们在等号左边指定了Map[Integer, String]。(我们在例子中还是继续使用Java 的类型。)在右边,我们实例化了一个我们实际需要的对象,一个HashMap,但是我们不用重复地声明类型参数。

再补充一点,假设我们实际上并不关心实例的类型是不是Map (Java 的接口类型)。我们只需要知道它是HashMap 类型。

  1. import java.util.Map  
  2. import java.util.HashMap  
  3. ...  
  4. val intToStringMap2 = new HashMap[Integer, String] 

这样的声明不需要在左边指定类型注解,因为所有需要的类型信息都已经在右边有了。编译器自动给intToStringMap2 赋予HashMap[Integer, String] 类型。

类型推断对方法也有用。在大多数情况下,方法的返回类型可以被推断,所以“:”和返回类型可以被省略。然而,对于所有的方法参数来说,类型注解是必须的。

像Haskell(参见,例如[O'Sullivan2009, Bryan O’Sullivan, John Goerzen, and Don Steward, Real World Haskell, O’Reilly Media, 2009] 这样的纯函数式语言使用类似于Hindley-Milner(参见[Spiewak2008] 获取简单摘要的解释)的算法来做类型推断。用这些语言写出的代码需要比Scala 更少的类型注解,因为Scala 的类型推断算法得同时支持面向对象类型和函数式类型。所以,Scala 比Haskell 这样的语言需要更多的类型注解。这里有一份关于Scala 何时需要显式类型注解规则的总结。

显式类型注解在何时是必要的。

从实用性来讲,你必须为下列情况提供显式的类型注解:

1。变量声明,除非你给变量赋予了一个值。(比如,val name = "Programming Scala")

2。所有的方法参数。(比如,def deposit(amount: Money)

3。下列情况中的方法返回值:

a 当你在方法里显式调用return 的时候 (即使是在最后)。

b 当一个方法是递归的时候。

c 当方法是重载的,并且其中一个调用了另外一个的时候。主调用的方法必须有一个返回类型的注解。

d 当推断的返回类型比你所想要的更普通时,比如Any。

注意

Any 类型是Scala 类型结构的根类型(参见《第7章 - Scala 对象系统的更多细节》中的“Scala 类型结构”章节)。如果一段代码意外地返回类一个Any 类型的值,那么很可能类型推断器不能算出需要返回的类型,所以选择了最有可能的最通常的类型。

让我们来看一些需要显式声明方法返回类型的例子。在下面的脚本中,upCase 方法有一个有条件的返回语句,返回非0长度的字符串。

  1. // code-examples/TypeLessDoMore/method-nested-return-script.scala  
  2. // ERROR: Won't compile until you put a String return type on upCase.  
  3. def upCase(s: String) = {  
  4.   if (s.length == 0)  
  5.     return s    // ERROR - forces return type of upCase to be declared.  
  6.   else  
  7.     s.toUpperCase()  
  8. }  
  9.  
  10. println( upCase("") )  
  11. println( upCase("Hello") )  

运行这段脚本你会获得如下错误。

  1. ... 6: error: method upCase has return statement; needs result type  
  2.         return s  
  3.          ^ 

你可以通过把方法第一行改成如下样子来修正这个错误。

  1. def upCase(s: String): String = { 

实际上,对于这段脚本,另外一种解决办法是删除return 关键字。没有它代码也可以很好的工作,但是它阐明了我们的目的。

递归方法也需要显式的返回类型。回忆我们在上一章中“嵌套方法的定义”章节看到的factorial 方法。让我们来删除嵌套的fact 方法的:Int 返回类型。

  1. // code-examples/TypeLessDoMore/method-recursive-return-script.scala  
  2. // ERROR: Won't compile until you put an Int return type on "fact".  
  3. def factorial(i: Int) = {  
  4.   def fact(i: Int, accumulator: Int) = {  
  5.     if (i <= 1)  
  6.       accumulator  
  7.     else  
  8.       fact(i - 1, i * accumulator)  // ERROR  
  9.   }  
  10.   fact(i, 1)  

现在不能编译了。

  1. ... 9: error: recursive method fact needs result type  
  2.             fact(i - 1, i * accumulator)  
  3.              ^ 

重载的方法有时候也需要显式返回类型。当一个这样的方法调用另外一个时,我们必须给调用者加上返回类型,如下面的例子。

  1. // code-examples/TypeLessDoMore/method-overloaded-return-script.scala  
  2. // Version 1 of "StringUtil" (with a compilation error).  
  3. // ERROR: Won't compile: needs a String return type on the second "joiner".  
  4. object StringUtil {  
  5.   def joiner(strings: List[String], separator: String): String =  
  6.     strings.mkString(separator)  
  7.   def joiner(strings: List[String]) = joiner(strings, " ")   // ERROR  
  8. }  
  9. import StringUtil._  // Import the joiner methods.  
  10. println( joiner(List("Programming", "Scala")) ) 

两个joiner 方法把一系列字符串串在一起。第一个方法还接受一个参数来作为分隔符。第二个方法调用第一个方法,并且传入一个空格作为“默认”分隔符。

如果你运行这段脚本,你会获得如下错误。

  1. ... 9: error: overloaded method joiner needs result type  
  2. def joiner(strings: List[String]) = joiner(strings, "") 

因为第二个jioner 方法调用了第一个,它需要一个显示的String 返回类型。它必须看起来像这样。

  1. def joiner(strings: List[String]): String = joiner(strings, " ") 

最后的一种场景的关系可能比较微妙,比你期望的类型更通用的类型可能会被推断返回。你通常会把函数返回值赋给拥有更特定类型变量的时候遇到这样的错误。比如,你希望获得一个String,但是函数推断返回类型为Any。让我们来看一个设计好的例子来反映会发生这种bug 的场景。

  1. // code-examples/TypeLessDoMore/method-broad-inference-return-script.scala  
  2. // ERROR: Won't compile. Method actually returns List[Any], which is too "broad".  
  3. def makeList(strings: String*) = {  
  4.   if (strings.length == 0)  
  5.     List(0)  // #1  
  6.   else  
  7.     strings.toList  
  8. }  
  9. val list: List[String] = makeList()  // ERROR 

运行这段脚本会获得如下错误。

  1. ...11: error: type mismatch;  
  2. found   : List[Any]  
  3. required: List[String]  
  4. val list: List[String] = makeList()  
  5.                           ^ 

我们希望makeList 能返回一个List[String],但是当strings.length 等于0 时,我们错误地假设List(0) 是一个空的列表并且将其返回。实际上,我们返回了一个有一个元素0 的List[Int] 对象。我们应该返回List()。因为else 表达式后返回了strings.toList 的返回值List[String],方法的推断返回类型就是离List[Int] 和List[String] 最近的公共父类型List[Any]。主意,编译错误并不是在函数定义的时候出现。我们只有当把makeList 返回值赋给一个List[String] 类型得变量的时候才看到这个错误。

在这种情况下,修正bug 才是正道。另外,有时候并没有bug,只是编译器需要一些显式声明的“帮助”来返回正确的类型。研究一下那些似乎返回了非期望类型的方法。以我们的经验,如果你修改了方法后发现它返回了比期望的更一般的类型,那么在这种情况下加上显式返回类型声明。

另一种避免这样的麻烦的方式是永远为方法返回值声明类型,特别是为公用API 定义方法的时候。让我们重新来看我们的StringUtil 例子来理解为什么显式声明是一个好主意(从[Smith2009a] 改写)。

这里是我们的StringUtil “API",和一个新的方法,toCollection。

  1. // code-examples/TypeLessDoMore/string-util-v3.scala  
  2. // Version 3 of "StringUtil" (for all versions of Scala).  
  3. object StringUtil {  
  4.   def joiner(strings: List[String], separator: String): String =  
  5.     strings.mkString(separator)  
  6.   def joiner(strings: List[String]): String = strings.mkString(" ")  
  7.   def toCollection(string: String) = string.split(' ')  

toCollection 方法以空格来分割字符串,然后返回一个包含这些子字符串的Array(数组)。返回类型是推断出的,我们会看到,这会是一个潜在的问题所在。这个方法是计划中的,但是会展示我们的重点。下面是一个使用StringUtil 的这个方法的客户端。

  1. // code-examples/TypeLessDoMore/string-util-client.scala  
  2. import StringUtil._  
  3. object StringUtilClient {  
  4.   def main(args: Array[String]) = {  
  5.     args foreach { s => toCollection(s).foreach { x => println(x) } }  
  6.   }  

如果你用scala 编译这些文件,你就能像这样运行客户端。

  1. $ scala -cp ... StringUtilClient "Programming Scala"  
  2. Programming  
  3. Scala 

注意

类路径参数 -cp,使用了scalac 写出class 文件的目录,默认是当前目录(比如,使用-cp.)。如果你使用了下载的代码示例中的编译过程,那些class 文件会被写到build 目录中区(使用scalac -d build ...)。在这个例子里,使用 -cp build.

这个时候,一切都工作正常。但是现在想象一下代码库扩大以后,StringUtil 和它的客户端被分别编译然后捆绑到不同的jar 文件中去。再想象一下StringUtil 的维护者决定返回一个List 来替代原来的默认值。

  1. object StringUtil {  
  2.   ...  
  3.   def toCollection(string: String) = string.split(' ').toList  // changed!  

唯一的区别是最后的对toList 的调用,把一个Array 转换成了List。重新编译StringUtil 并且部署为jar 文件。然后运行相同的客户端,先不要重新编译。

  1. $ scala -cp ... StringUtilClient "Programming Scala"  
  2. java.lang.NoSuchMethodError: StringUtil$.toCollection(...  
  3.   at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)  
  4.   at StringUtilClient$$anonfun$main$1.apply(string-util-client.scala:6)  
  5. ... 

发生了什么?当客户端被编译的时候,StringUtil.toCollection 返回了一个Array。然后toCollection 被修改为返回一个List。在两个版本里,方法返回值都是被推断出来的。因此,客户端也必须被重新编译。

然而,如果显式地声明返回类型是Seq,作为Array 和List 的共同父类型,这样的实现就不会对客户端要求重新编译。

注意

当开发独立于客户端的API 的时候,显式地声明方法返回类型,并且尽可能使用更一般的返回类型。这在API 被声明为抽象方法时尤其重要。(参见,比如《第4章 - 特性》。)

还有另外一种场景需要考虑集合声明的使用,比如val map = Map(),就像下面这个例子。

  1. val map = Map()  
  2. map.update("book", "Programming Scala")  
  3. ... 3: error: type mismatch;  
  4. found   : java.lang.String("book")  
  5. required: Nothing  
  6. map.update("book", "Programming Scala")  
  7.             ^ 

发生了什么?范型类型Map 的类型参数在map 被创建时被推断为[Nothing, Nothing]。(我们会在《第7章 - Scala 对象系统》的“Scala 类型结构”章节讨论Nothing。但是它的名字本身就解释了自己!)我们尝试插入一对不匹配的String,String 键值对。叫它拿都去不了的地图吧!解决方案是,在初始化map 声明的时候指出参数类型,例如val map = Map[String, String]() 或者指定初始值以便于map 参数被推断,例如val map = Map("Programming"->"Scala")。

最后,还有一个推断返回类型可能导致不可预知的令人困扰的结果[Scala 提示]的诡异行为。考虑下面的scala 对话例子。

  1. scala> def double(i: Int) { 2 * i }  
  2. double: (Int)Unit  
  3. scala> println(double(2))  
  4. () 

为什么第二个命令打印出() 而不是4?仔细看scala 解释器给出的第一个命令的返回值,double: (Int)Unit。我们定义了一个方法叫double,接受一个Int 参数,返回Unit。方法并不像我们期望的那样返回Int。

造成这样意外结果的原因是在方法定义中缺少的等于号。下面是我们实际上需要的定义。

  1. scala> def double(i: Int) = { 2 * i }  
  2. double: (Int)Int  
  3. scala> println(double(2)) 

注意double 主体前的等于号。现在,输出说明我们定义了一个返回Int 的double,第二个命令完成了我们期望的工作。

这样的行为是有原因的。Scala 把主体之前的部分包含等于号作为函数定义,而一个函数在函数式编程中永远都有返回值。另一方面来说,当Scala 看到一个函数主体而没有等于号前缀时,它就假设程序员希望这个方法变成一个“过程”定义,希望获得由返回值Unit 带来的副作用。而在实际中,结果往往是程序员简单地忘记了插入等于号!

警告

当方法的放回类型被推断而你又没有在方法主体的大括号前使用等于号的时候,即使方法的最后一个表达式是另外一个类型的值,Scala 也会推断出一个Unit 返回类型。

顺便说一句,之前我们修正bug 前打印出来的() 是哪里来的?事实上这是Unit 类型单体实例的真正名字!(这个名字是函数式编程的协定。)

常值

一个对象经常会用一个常值来初始化,比如val book = "Programming Scala"。下面我们来讨论一下Scala 支持的常值种类。这里我们只讨论字符常值。我们会在后面遇到函数(被用作值,而不是成员方法),tuples,Lists,Maps 等的常值语法的时候再继续讨论。

整数(Integer)

整数常值可以表达为:十进制,十六进制,或者八进制。细节总结参见“表2.1, 整数常值”

种类  格式  例子
十进制 0 ,或者非零数字后面跟随0 个或者多个十进制字符 (0 - 9) 0, 1, 321
十六进制 0x 后面跟随一个或多个十六进制字符 (0-9, A-F, a-f) 0xFF, 0x1a3b
八进制 0 后面跟随一个或多个八进制字符 (0-7) 013, 077

对于长整型值,必须在常值的后面加上L 或者l 字符。否则会被判定为普通整型。整数的有效值由被赋值的变量类型来决定。表2.2,“整型数的允许范围(包括边界)” 定义了整数的极限,包括边界值。

目标类型  最小值 (包括) 最大值 (包括)
Long(长整型) −263 263 - 1
Int (整型) −231 231 - 1
Short (短整型) −215 215 - 1
Char (字符) 0 216 - 1
Byte (字节) −27 27 - 1

如果一个整数的值超出了允许范围,就会发生编译错误,比如下面这个例子。

  1. scala > val i = 12345678901234567890 
  2. :1: error: integer number too large  
  3.        val i = 12345678901234567890 
  4. scala> val b: Byte = 128 
  5. :4: error: type mismatch;  
  6. found   : Int(128)  
  7. required: Byte  
  8.        val b: Byte = 128 
  9.                      ^  
  10.  
  11. scala> val b: Byte = 127 
  12. b: Byte = 127 


 浮点数(Float)

Float 由0 个或多个数字,加上一个点号,再加上0 个或多个数字组成。如果在点号前面没有数字,比如数字比1.0 要小,那么在点号后面必须有一个或者多个数字。对于浮点数,需要在常值的最后加上F 或者f 。否则默认判定为双精度浮点数(Double)。你可以选择给一个双精度浮点数加上D 或者d。

浮点数可以用指数方法表达。指数部分的格式是e 或者E,加上一个可选的+或者-,再加上一个或多个数字。
 
这里有一些浮点数的例子。

  1. 0.  
  2. .0  
  3. 0.0  
  4. 3.  
  5. 3.14  
  6. .14  
  7. 0.14  
  8. 3e5  
  9. 3E5  
  10. 3.E5  
  11. 3.e5  
  12. 3.e+5  
  13. 3.e-5  
  14. 3.14e-5  
  15. 3.14e-5f  
  16. 3.14e-5F  
  17. 3.14e-5d  
  18. 3.14e-5D  

Float 遵循了IEEE 754 32位单精度二进制浮点数值的规范。Double 遵循了IEEE 754 64位双精度二进制浮点数值的规范。

警告

为了防止解析时的二义性,如果一个浮点数后面跟随着一个字母开头的符号,你必须在浮点数后面跟随至少一个空格。比如,表达式1.toString 返回整数1 的字符串形式,而1. toString 则返回浮点数1.(0) 的字符串形式。

布尔值

布尔值可以是true (真) 或者false (假)。被赋值的变量的类型会被推断为Boolean。

  1. scala> val b1 = true 
  2. b1: Boolean = true 
  3.  
  4. scala> val b2 = false 
  5. b2: Boolean = false 

字符常值

一个字符常值是一个单引号内的可打印的Unicode 字符或者一个转义序列。一个可以用Unicode 值0 到255 表示的字符也可以用一个八进制转义来表示:一把反斜杠加上最多3个八进制字符序列。如果在字符或者字符串中反斜杠后面不是一个有效的转义序列则会出现编译错误。

这里有一些例子.

  1. ’A’  
  2. ’\u0041’  // 'A' in Unicode  
  3. ’\n’  
  4. '\012'    // '\n' in octal  
  5. ’\t’  

有效的转义序列参见:表格2.3 “字符转义序列”

序列 Unicode 含义
\b \u0008 backspace BS (退格)
\t \u0009 horizontal tab HT (水平Tab)
\n \u000a linefeed LF (换行)
\f \u000c form feed FF (换页)
\r \u000d carriage return CR (光标返回)
\" \u0022 double quote " (双引号)
\’ \u0027 single quote ’ (单引号)
\\ \u0009 backslash \(反斜杠)
字符串常值

一个字符串是在双引号内或者3重双引号内的一系列字符,比如"""..."""。

对于双引号内的字符串,允许的字符集和字符常值的范围一样。不过,如果字符传中出现了双引号,那么它必须用一个字符来转义表示。这里有一些例子。

  1. "Programming\nScala"  
  2. "He exclaimed, \"Scala is great!\""  
  3. "FirsttSecond"  

被三重双引号包含的字符串也被称为多行字符串。这些字符串可以跨越好几行;换行符也会被作为字符串的一部分。它们可以包含任何字符,包括一个或者两个双重双引号,但是不能有三重双引号。它们对于包含非有效Unicode 或者转义序列字符的字符串来说很有用,就和使用表2.3 字符转义序列里的有效序列的一样。一个典型的例子就是正则表达式,我们会在《第3章 - Scala 本质》中讨论它。不过,如果转义字符在这里不会被解释(转义)。

这里有3个示例字符串.

  1. """Programming\nScala"""  
  2. """He exclaimed, "Scala is great!" """  
  3. """First line\n  
  4. Second line\t  
  5.  
  6. Fourth line"""  

主意我们在第二个例子里面必须在结尾的""" 前加一个空格防止解析错误。如果想要把结束“Scala is great” 的引号转义,比如“Scala is great!\” ,是不行的。

拷贝然后粘贴这些字符串到scala 解释器。对前面的字符串例子也做一样的事情。看看他们是如何被解释的。

符号常值

Scala 支持符号,就是一些限定字符串。如果两个符号有同样的“名字”,比如同样的字符序列,那么它们本质上会指向内存中同样的对象。符号在Scala 中的使用频率要低于其它语言,比如Ruby, Smalltalk 和Lisp。它们对于Map 的Key 来说比字符串更有用。

一个符号常值是一个单引号',跟着一个字母,再跟着0个或多个数字和字母。注意,像'1 这样的表达式是无效的,因为编译器会认为这是一个不完整的字符常值。

符号常值'id 是表达式scala.Symbol("id") 的简写。

注意

如果你想创建包含空格的符号,使用这样的方式:scala.Symbol("Programming Scala ")。所有的空格都会被保留。

元组(Tuples)

你有多少次想要给一个方法返回两个或者更多的值?在许多语言,比如Java 中,你只有很少的选择,而且都不是上选。你可以传入一个参数给方法,然后为所有或者一些返回值而修改它,这看起来很难看。或者,你可以声明一些小的结构性类,可以让你保存两个或更多的值,然后返回那个类的一个实例。

Scala 则支持元组(tuples),一个包含两个或更多元素的组合,通常可以用一对小括号加上逗号分隔的元素序列来创建它们,比如(x1, x2, ...)。xi 的类型相互之间没有任何关系,你可以使用混合的或者一致的类型。这些“组合” 会被实例化成scala.TupleN 的实例,N 是里面元素的数目。Scala API 定义了N 从1 到22 的22个单独的TupleN 类。Tuple 实例是只读的,第一类的值。所以你可以把他们赋值给变量,把他们当值来传递,作为返回值由方法来返回。

下面的例子展示了Tuple 的使用.

  1.  
  2. // code-examples/TypeLessDoMore/tuple-example-script.scala  
  3.  
  4. def tupleator(x1: Any, x2: Any, x3: Any) = (x1, x2, x3)  
  5.  
  6. val t = tupleator("Hello", 1, 2.3)  
  7. println( "Print the whole tuple: " + t )  
  8. println( "Print the first item:  " + t._1 )  
  9. println( "Print the second item: " + t._2 )  
  10. println( "Print the third item:  " + t._3 )  
  11.  
  12. val (t1, t2, t3) = tupleator("World", '!', 0x22)  
  13. println( t1 + " " + t2 + " " + t3 )  
  14.  

运行这段脚本会获得如下输出。

  1. Print the whole tuple: (Hello,1,2.3)  
  2. Print the first item:  Hello  
  3. Print the second item: 1  
  4. Print the third item:  2.3  
  5. World ! 34  

方法tupleator 简单地返回一个包含输入参数的3元组。第一个语句使用这个方法把返回的元组赋值给变量t。下面4个语句用各种方法来打印t。第一个打印语句调用了Tuple3.toString,把元素周围加上括号。下面的3个语句分别打印了t 的各个元素。表达式t._N 获取第N 个元素,从1开始,不是0 (这个选择遵循了函数式编程的习惯)。

最后两行展示了我们可以把元组表达式作为左值。我们声明3个val,t1, t2, t3,来存储在元组里的每一个元素。本质上讲,元组的元素会被自动提取出来。
 
注意我们是怎么在元组里混合类型的。你可以使用在《第1章 - 从0 分到60 分,Scala 介绍》里介绍的scala 交互模式来看得更清楚一些。
 
不用任何脚本参数来调用scala 命令。在scala> 提示符后,输入val t = ("Hello", 1, 2.3) 然后观察输出结果,你会看到元组里各个元素的类型。

  1. scala> val t = ("Hello",1,2.3)  
  2. t: (java.lang.String, Int, Double) = (Hello,1,2.3) 

值得注意的是有许多方法可以定义一个元组。我们已经使用了最常见的括号语法,但是你可以可以使用两个值之间的箭头操作符,或者和tuple 有关的类的工厂方法来建立元组。

  1. scala> 1 -> 2  
  2. res0: (Int, Int) = (1,2)  
  3.  
  4. scala> Tuple2(1, 2)  
  5. res1: (Int, Int) = (1,2)  
  6.  
  7. scala> Pair(1, 2)  
  8. res2: (Int, Int) = (1,2)  
  9.    
  10.  

Option, Some, 和 None: 避免使用 null

我们会在《第7章 - Scala 对象系统》中的“Scala 类型结构” 章节来讨论标准类型结构。但是,我们现在要理解3个有用的类,即Option 和它的两个子类Some 和None。

大多数语言都有一个特殊的关键字或者对象来表示一个对象引用的是“无”。在Java,它是null;在Ruby,它是nil。在Java 里,null 是一个关键字,不是一个对象,所以对它调用任何方法都是非法的。但是这对语言设计者来说是一件令人疑惑的选择。为什么要在程序员希望返回一个对象的时候返回一个关键字呢?
 
为了让所有东西都是对象的目标更加一致,也为了遵循函数式编程的习惯,Scala 鼓励你在变量和函数返回值可能不会引用任何值的时候使用Option 类型。在没有值的时候,使用None,这是Option 的一个子类。如果有值可以引用,就使用Some 来包含这个值。Some 也是Option 的子类。

注意

None 被声明为一个对象,而不是一个类,因为我们只需要它的一个实例。这样,它多少有点像null 关键字,但它却是一个实实在在的,有方法的对象。

你可以在下面这个例子里看到Option,Some 和None 通力协作。我们会创建一张美国的各州首府的Map。

  1. // code-examples/TypeLessDoMore/state-capitals-subset-script.scala  
  2.  
  3. val stateCapitals = Map(  
  4.   "Alabama" -> "Montgomery",  
  5.   "Alaska"  -> "Juneau",  
  6.   // ...  
  7.   "Wyoming" -> "Cheyenne")  
  8.  
  9. println( "Get the capitals wrapped in Options:" )  
  10. println( "Alabama: " + stateCapitals.get("Alabama") )  
  11. println( "Wyoming: " + stateCapitals.get("Wyoming") )  
  12. println( "Unknown: " + stateCapitals.get("Unknown") )  
  13.  
  14. println( "Get the capitals themselves out of the Options:" )  
  15. println( "Alabama: " + stateCapitals.get("Alabama").get )  
  16. println( "Wyoming: " + stateCapitals.get("Wyoming").getOrElse("Oops!") )  
  17. println( "Unknown: " + stateCapitals.get("Unknown").getOrElse("Oops2!") )  

用来方便定义Map 的键值对的-> 语法会在《第7章 - Scala 对象系统》的“预定义对象” 章节被详细讨论。现在,我们希望专注在在两组println 语句上,它们展示了我们在从map 取值的时候会发生什么。如果你用scala 命令来运行这段脚本,你会获得如下输出。

  1. Get the capitals wrapped in Options:  
  2. Alabama: Some(Montgomery)  
  3. Wyoming: Some(Cheyenne)  
  4. Unknown: None  
  5. Get the capitals themselves out of the Options:  
  6. Alabama: Montgomery  
  7. Wyoming: Cheyenne  
  8. Unknown: Oops2! 

第一组println 语句在get 返回的实例上隐式调用了toString。因为Map.get 返回的值会在有匹配的值时被自动包在Some 里,所以我们实际上是调用了Some 或者None 的实例的toString。主意Scala 库并不在Map 里存储Some,它只在值被取出的时候被外包。相反的,如果我们要取的Key 没有对应的值,那么None 对象就会被返回,而不是null。从这3个语句里的最后一个println 可以看出来。

第二组println 语句展示了更多细节。在调用Map.get 之后,它们还对每个Option 实例调用了get 或者getOrElse 来获取它们实际包含的值。Option.get 要求Option 不为空,也就是说,这个Option 实例必须是一个Some。在这个例子里,get 返回被Some 包含道的值,正如println 所展示的,是阿拉巴马的首府。然而,如果这个Option 是一个None,None.get 会抛出一个NoSuchElementException 异常。
我们也可以选用另外一个方法,getOrElse,就像最后两个println 展示的。这个方法在这个Option 是Some 的实例时返回对应的值,而在是None 的实例时返回传入的参数。换句话说,传入getOrElse 的参数实际上是默认返回值。

所以,getOrElse 是两者中比较保守的方法。它避免了可能的异常。我们会在《第13章 - 程序设计》中的“异常和替代方法” 章节讨论get VS. getOrElse 的优缺点。

注意,因为Map.get 返回一个Option,它也就自己说明了指定的Key 可能不会有匹配的值。map 处理这种情况的方法是返回None。许多语言会在没有实际值返回时选择返回null(或者等价物)。你可能会根据经验来期望一个null。使用Option 使得方法的行为在签名中更加明显,也就是更加的“自我说明”。
 
实际上,多亏Scala 的静态类型,你并不能错误地尝试在一个可能为null 的值上调用方法。虽然在Java 中这是个很容易犯的错误,它在Scala 却通不过编译,因为你必须先把值从Option 中取出来。所以,Option 的使用极强地鼓励了更加弹性的编程习惯。
 
因为Scala 运行在JVM 和.NET 上,而且必须和其它的库合作,所以Scala 必须支持null。然而,你应该尽量在你的代码中避免使用null。Tony Hoare,在1965年工作在一个名叫ALGOL W 的面向对象语言上时发明了null 引用,他称他的发明为一个“百万美金的错误”[Hoare2009]。别再为这个名言做贡献了:)
 
那么,你该如何写一个返回Option 的方法呢?这里有一个可能的被Map 的具体子类使用的get 的实现(Map.get 本身是抽象的)。要获取更稳健的版本,参考Scala 库源代码发布包中的scala.collection.immutable.HashMap 的get 的实现。

  1. def get(key: A): Option[B] = {  
  2.   if (contains(key))  
  3.     new Some(getValue(key))  
  4.   else  
  5.     None  

contains 方法也在Map 中定义。它在map 包含特定key 对应的值时返回true。getValue 方法应该是一个内部方法,用来从底层存储的实现中获取对应的值,无论它是什么。

主意getValue 的返回值是如何被包裹在Some[B] 里的(B 的类型是推断的)。然而,如果调用contains(key) 返回false,那么None 对象就会被返回。

你可以在你的方法返回Option 的时候使用同样的方式。我们会在下面的章节中继续探索Option 的其它用处。它在Scala 代码中的普遍使用使之成为一个重要的需要掌握的概念。

用文件和名称空间来组织代码

Java 用包来处理名称空间,Scala 借鉴了这一概念,但是提供了更加灵活的语法。就像文件名不一定要和类型名称一致一样,包的结构和文件夹结构也不一定要一致。所以,你可以独立于文件的物理结构来构建你的包。

下面的例子在包com.example.mypkg 下定义了类MyClass,使用的是常规的Java 语法。

  1. // code-examples/TypeLessDoMore/package-example1.scala  
  2. package com.example.mypkg  
  3. class MyClass {  
  4.   // ...  
  5. }  
  6.  

下一个例子展示如何使用Scala 包的嵌套语法来定义包。这和C# 的namespace (名称空间)语法以及Ruby 中类似namespace 的modules (模块)语法。

  1. // code-examples/TypeLessDoMore/package-example2.scala  
  2.  
  3. package com {  
  4.   package example {  
  5.     package pkg1 {  
  6.       class Class11 {  
  7.         def m = "m11" 
  8.       }  
  9.       class Class12 {  
  10.         def m = "m12" 
  11.       }  
  12.     }  
  13.  
  14.     package pkg2 {  
  15.       class Class21 {  
  16.         def m = "m21" 
  17.         def makeClass11 = {  
  18.           new pkg1.Class11  
  19.         }  
  20.         def makeClass12 = {  
  21.           new pkg1.Class12  
  22.         }  
  23.       }  
  24.     }  
  25.  
  26.     package pkg3.pkg31.pkg311 {  
  27.       class Class311 {  
  28.         def m = "m21" 
  29.       }  
  30.     }  
  31.   }  
  32. }  
  33.  

在包com.example 下我们定义了另外两个包pkg1 和pkg2。而在这两个包下我们又一共定义了3个类。类Class21 的方法makeClass11 和makeClass12 举例说明了如何引用其它包内的类型。你也可以用它们的完整路径com.example.pkg1.Class11 和com.example.pkg1.Class12 来引用这些类。

包pkg3.pkg31.pkg311 展示了在一个语句里链接几个包的语法。你大可不必给每一个包都写一个单独的package 语句。

我们遵循Java 的习惯,给Scala 库的根包 取名为scala。

警告

Scala 不允许在scala 解释器直接运行写有包声明语句的脚本。原因是解释器在把脚本编译成字节码之前必须先把脚本语句转换为有效的Scala 代码。参见《第14章 - Scala 工具,库和IDE 支持》的“Scala 命令行工具” 章节获取详细信息。

导入类型和它们的成员

要使用包里面的类型,你必须导入它们,就像你在Java 或类似语言中做的一样。然而,和Java 相比,Scala 有更多的选项供你选择。下面的例子举例说明了几种导入Java 类型的方式。

  1. // code-examples/TypeLessDoMore/import-example1.scala  
  2. import java.awt._  
  3. import java.io.File  
  4. import java.io.File._  
  5. import java.util.{Map, HashMap}  
  6.  

你可以通过使用下划线_ 作为通配符导入一个包里的所有类型,如第一行。你也可以导入单个的Java 或者Scala 类型,例如第二行。

Java 使用星号* 作为通配符来匹配包里的所有类型或者在做静态导入时导入类型内的所有静态成员。在Scala 里,星号被允许作为函数名的一部分,所以我们使用_ 来作为通配符,正如我们前面看到的一样。

再看第三行,你可以导入Java 类型中所有的静态成员和字段。如果java.io.File 实际上是一个Scala 对象,那么这一行会导入那个对象的所有方法和字段,正如我们前面所讨论过的一样。

最后,你可以有选择地导入你所关心的类型。在第四行,我们只从java.util 包内导入了java.util.Map 和java.util.HashMap 类型。比较这样的一行导入语句和之前在“类型信息推断”章节的第一个例子中的两行导入语句,它们在功能上是一样的。

下一个例子展示了更多import 语句的高级用法。

  1. // code-examples/TypeLessDoMore/import-example2-script.scala  
  2. def writeAboutBigInteger() = {  
  3.   import java.math.BigInteger.{  
  4.     ONE => _,  
  5.     TEN,  
  6.     ZERO => JAVAZERO }  
  7.  
  8.   // ONE is effectively undefined  
  9.   // println( "ONE: "+ONE )  
  10.   println( "TEN: "+TEN )  
  11.   println( "ZERO: "+JAVAZERO )  
  12. }  
  13.  
  14. writeAboutBigInteger()  

这个例子展示了两个特性。第一,我们可以把import 语句放在任何想要的地方,而不像Java 要求的那样仅仅是文件的最上面。这个特性允许我们定义更加狭小的导入空间。比如说,我们不能在方法的定义范围外引用导入的BigInteger 类型。另一个优势是,它把import 语句放在了离实际需要的对象更近的地方。

第二个特性是,我们可以重命名导入的项目。首先,常量java.math.BigInteger.ONE 被重命名为下划线通配符。它使得ONE 对于其导入域来说变得不可见,无效。这对于想导入所有东西但有几个除外的项目的时候很有用。

然后,java.math.BigInteger.TEN 常量被导入,没有重命名,所以可以简单地通过TEN 来引用它。

最后,java.math.BigInteger.ZERO 常量被赋予一个别名JAVAZERO。

在你想给一个东西一个更方便的名字,或者想避免和这个域下的其他相同名称的二义性的时候,别名会非常有用。

导入是相对的

有一件关于导入必须要知道的是,它们是相对的。主意下面的导入语句的注释。

  1. // code-examples/TypeLessDoMore/relative-imports.scala  
  2.  
  3. import scala.collection.mutable._  
  4. import collection.immutable._         // Since "scala" is already imported  
  5. import _root_.scala.collection.jcl._  // full path from real "root"  
  6.  
  7. package scala.actors {  
  8.   import remote._                     // We're in the scope of "scala.actors"  
  9. }  
  10.  

注意在scala.actor 包的最后嵌入的那个导入语句是相对于scala.actors 包的域的。

[ScalaWiki] 在这里有另外一个例子。

虽然很少在导入的相对性上会遇到什么麻烦,但是这个方便的特性有时候确实会让人大吃一惊,特别是当你习惯于Java 那样的绝对导入的时候。如果你得到一个神秘的编译错误指出某个包没有找到,检查一下导入语句的相对性,或者加上_root_. 前缀。其实,你可能会看到某些IDE 或者工具在你的代码里插入import _root_... 语句。现在你知道是什么意思了。

警告

记住import 语句是相对的,不是绝对的。要创建绝对路径,以_root_. 来作为开头。

抽象类型和参数化类型

我们在《第1章 - 从0分到60 分:Scala 介绍》的“初尝Scala” 章节中提到,Scala 支持参数化类型,和Java 中的范型很像。(我们可以交换使用这两个术语,但是在Scala 社区中人们使用“参数化类型”更多,而在Java 社区中则是“范型”更多。)最明显的差别就是语法,Scala 使用方括号[],而Java 使用尖括号<>。

举例来说,一个字符串列表可以被声明为如下形式。

  1. val languages: List[String] = ... 

和Java 的范型还有其它重要的区别,我们会在《第12章 - Scala 类型系统》中的“理解参数化类型”章节进行探讨。

现在,我们在《第12章 - Scala 类型系统》进行更深入的解释之前要介绍另外一个有用的细节。如果你在Scaladocs 中看一下scala.List 的声明,你会发现它的声明是写成这样的 ... class List[+A]。在A 前面的加号+ 意味着如果B 是A 的子类型,那么List[B] 就是List[A] 的子类型。如果在类型参数之前有一个减号-,那么恰好相反,如果声明为Foo[-A],则Foo[B] 会是Foo[A] 的父类型。

Scala 支持另外另一种类型抽象机制称为抽象类型,在许多函数式编程语言中都有使用,比如Haskell。抽象类型在Java 引入范型时也被考虑过引入。我们现在就来介绍它,因为你会在我们深入《第12章 - Scala 类型系统》之前就见到很多的例子。要了解两种机制更细致的比较,参见[Bruce1998(Kim Bruce,Martin Odersky,和Philip Wadler,“虚拟类型的一种静态的安全的替代方式”,)]。

抽象类型可以解决许多参数类型可以解决的设计问题。然而,当两种机制重叠时,却并不多余。每一种机制都有对特定设计问题的强项和弱点。

这里是使用抽象类型的一个例子。

  1. // code-examples/TypeLessDoMore/abstract-types-script.scala  
  2.  
  3. import java.io._  
  4.  
  5. abstract class BulkReader {  
  6.   type In  
  7.   val source: In  
  8.   def read: String  
  9. }  
  10.  
  11. class StringBulkReader(val source: String) extends BulkReader {  
  12.   type In = String 
  13.   def read = source 
  14. }  
  15.  
  16. class FileBulkReader(val source: File) extends BulkReader {  
  17.   type In = File 
  18.   def read = {  
  19.     val in = new BufferedInputStream(new FileInputStream(source))  
  20.     val numBytes = in.available()  
  21.     val bytes = new Array[Byte](numBytes)  
  22.     in.read(bytes, 0, numBytes)  
  23.     new String(bytes)  
  24.   }  
  25. }  
  26.  
  27. println( new StringBulkReader("Hello Scala!").read )  
  28. println( new FileBulkReader(new File("abstract-types-script.scala")).read )  

用scala 来运行这段脚本会得到如下输出。

  1. Hello Scala!  
  2. import java.io._  
  3.  
  4. abstract class BulkReader {  
  5. ...  

抽象类型BulkReader 声明了3个抽象成员,一个名为In 的类型,一个val 字段source,和一个read 方法。和在Java 中一样,Scala 中实例只能从具体类中创建,实体类必须有所有成员的定义。

继承自它的子类StringBulkReader 和FileBulkReader 提供了三个抽象成员的实际定义。我们会在《第5章 - Scala 基础面向对象编程》中涵盖类声明的细节,在《第6章 - Scala 高级面向对象编程》的“在类和特性中重写成员” 章节中涵盖重写成员声明的细节。

现在,主意type 字段工作起来很像参数化类型的参数。实际上,我们可以重写这个例子,只是有些许不同。

  1. abstract class BulkReader[In] {  
  2.   val source: In  
  3.   ...  
  4. }  
  5.  
  6. class StringBulkReader(val source: String) extends BulkReader[String] {...}  
  7. class FileBulkReader(val source: File) extends BulkReader[File] {...}  

只是对于参数化类型,如果我们定义了In 类型为String,那么source 字段也必须被定义为String。主意StringBulkReader 的read 方法简单地返回了source 字段,而FileBulkReader 的read 方法读取了文件的具体内容。

正如[Bruce1998] 演示的,参数化类型是集合的最佳选择,也是Java 代码中使用的最频繁的地方。而抽象类型则对于类型族和其他类型的场景最有用。

我们会在《第12章 - Scala 类型系统》中讨论Scala 抽象类型的细节。比如,我们会看到如何限定可能会被使用的具体类型。

保留字(关键字)

表 2.4 “保留字”列出了Scala 的保留字,也被称为关键字,并且简要描述了他们在[ScalaSpec2009] 定义下是如何被使用的。

表2.4 保留字

关键字 描述 参见…
abstract 声明抽象类型。和Java 不一样的是,这个关键字对于抽象成员来说通常不是必要的。 (类和对象基础)《第5章 - Scala 基础面向对象编程》
case 在一个match 表达式里开始一个case 语句块。 (模式匹配) 《第3章 - Scala 本质》
catch 开始一个语句块来截取抛出的异常。 (使用try,catch,finally 语句块)《第3章 - Scala 本质》
class 开始一个类的声明。 (类和对象基础)《第5章 - Scala 基础面向对象编程》
def 开始一个方法声明。 (方法声明)《第2章 - 打更少的字,做更多的事》
do 开始一个do... while 循环。 (其它循环结构)《第3章 - Scala 本质》
else 对应一个if 语句块开始一个else 语句块。 (Scala  if 语句)《第3章 - Scala 本质》
extends 表示跟在后面的类或者特性这被声明的类或者特性的父类型。 (父类)《第5章 - Scala 基础面向对象编程》
FALSE 布尔值假。 (Scala 类型结构) 《第7章 - Scala 对象系统》
final 应用于类或者特性防止其它类型派生于它。应用于成员来防止在派生类或者特性中被重写。 (尝试重写final 声明)《第6章 - Scala 高级面向对象编程》
finally 在对应的try 语句块后面开始一个无论异常是否被截取都会被执行的语句块。 (使用try,catch,finally 语句块)《第3章 - Scala 本质》
for 开始一个for 循环。 (理解Scala)《第3章 - Scala 本质》
forSome 用于约束允许使用的具体类的已存在类型声明。 (已存在类型)《第12章 - Scala 类型系统》
if 开始一个if 语句块。 (Scala 的if 语句)《第3章 - Scala 本质》
implicit 标识一个方法允许做为类型隐式转换器。标识一个方法的参数为可选,只要在方法被调用的域里有一个类型兼容的替代对象。 (隐式转换)《第8章 - Scala 函数式编程》
import 导入一个或多个类型或者类型成员到当前域。 (导入类型和它的成员)《本章》
lazy 推迟对val 的估值。 (延迟计算值) 《第8章 - Scala 函数式编程》
match 开始一个模式匹配语句块。 (模式匹配)《第3章 - Scala 本质》
new 创建一个类的实例。 (类和对象基础)《第5章 - Scala 基础面向对象编程》
null 一个没有被赋值的引用类型变量的值。 (Scala 类型结构)《第7章 - Scala 对象系统》
object 开始一个单体模式的声明。一个只有一个实例的类。 (类和对象:静态成员呢?)《第5章 - Scala 基础面向对象编程》
override 如果原来的成员没有被标识为final,重写一个类或特性的具体成员。 (重写类和特性的成员) 《第6章 - Scala 高级面向对象编程》
package 开始一个包的域声明。 (用文件和名称空间来组织代码)《第2章 - 打更少的字,做更多的事》
private 限定一个声明的可见域。 (可见性规则)《第5章 - Scala 基础面向对象编程》
protected 限定一个声明的可见域。 (可见性规则)《第5章 - Scala 基础面向对象编程》
requires 不推荐使用. 曾用于自我类型(selftyping). (Scala 类型结构)《第7章 - Scala 对象系统》
return 从一个函数返回。 (初尝Scala )《第1章 - 从0 分到60 分,Scala 介绍》
sealed 应用于父类,要求所有直接继承的类都必须在同一个源文件中声明。 (案例类)《第6章 - Scala 高级面向对象编程》
super 类似this,但是指向父类型。 (重写抽象和具体方法)《第6章 - Scala 高级面向对象编程》
this 一个对象引用自己的方式。辅助构造函数的方法名。 (类和对象基础)《第5章 - Scala 基础面向对象编程》
throw 抛出一个异常。 (使用try,catch,finally 语句块)《第3章 - Scala 本质》
trait 对一个类的实例加入额外状态和行为的混合模块。 《第4章 - 特性》
try 开始一个语句块包含对可能抛出异常的语句。 (使用try,catch,finally 语句块)《第3章 - Scala 本质》
TRUE 布尔值真。 (Scala 类型结构)《第7章 - Scala 对象系统》
type 开始一个类型声明。 (抽象类型和参数化类型) 《第2章 - 打更少的字,做更多的事》
val 开始一个只读变量声明。 (变量声明)《第2章 - 打更少的字,做更多的事》
var 开始一个可读写变量声明。 (变量声明)《第2章 - 打更少的字,做更多的事》
while 开始一个while 循环。 (其它循环结构)《第3章 - Scala 本质》
with 对被声明的类或者实例化的对象包含这个特性。 《第4章 - 特性》
yield 返回for 循环的结构以组成一个序列。 (Yield 语句)《第3章 - Scala 本质》
_ 一个占位符,在imports,字面函数等地方使用。 许多地方都有涉及
: 标志符和类型注解之间的分隔符。 (初尝Scala)《第1章 - 从0 分到60 分,Scala 介绍》
= 赋值。 (初尝Scala)《第1章 - 从0 分到60 分,Scala 介绍》
=> 在字面函数中使用,用于隔离参数列表和函数主体。 (字面函数和闭包)《第8章 - Scala 函数式编程》
<- 用于生成器表达式。 (理解Scala)《第3章 - Scala 本质》
<: 用于参数化类型和抽象类型声明,用来约束允许的类型。 (类型边界)《第12章 - Scala 类型系统》
<% 用于参数化类型和抽象类型的视觉边界(view bounds) 声明。 (类型边界) 《第12章 - Scala 类型系统》
>: 用于参数化类型和抽象类型,用来约束允许的类型。 (类型边界)《第12章 - Scala 类型系统》
# 用于类型投射(type projection)。 (类型投射)《第12章 - Scala 类型系统》
@ 标识一个注解。 (注解) 《第13章 - 应用程序设计》
(Unicode \u21D2) 等同 =>. (字面函数和闭包) 《第8章 - Scala 函数式编程》
(Unicode \u2190) 等同 <-. (理解Scala)《第3章 - Scala 本质》

注意break 和continue 没有被列出。这些控制关键字在Scala 中并不存在。相反,Scala 鼓励你使用通常情况下足够用,而且更少犯错的函数式编程方法。我们会在讨论循环的时候讨论一些其它的处理方法。(参见《第三章 - Scala 本质》的“生成器表达式”)。

有些Java 方法使用了Scala 的保留字,比如java.util.Scanner.match。为了避免编译错误,用单反引号来括住它们,比如java.util.Scanner.‵match‵。

概括

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

【编辑推荐】

  1. Scala语言编程入门指南
  2. “Scala” 一个有趣的语言
  3. 51CTO专访Scala创始人:Scala拒绝学术化
  4. 编程思想碰撞 Scala不是改良的Java
  5. 对象函数式编程 Scala简史
【责任编辑:立方 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

超级网管员——网络安全

本书全面深入地介绍网络安全的配置与实现技术,包括系统管理、用户账户、病毒防御、灾难恢复、文件备份、安全策略、注册表等服务器安全,用...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× 学习达标赢Beats耳机