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

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

10年积累的成都网站制作、成都网站建设经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站设计后付款的网站建设流程,更有盐湖免费网站建设让你可以放心的选择与我们合作。

推荐专题: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. // Trailing equals sign indicates more code on next line
  3. def equalsign = {
  4.   val reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine =
  5.     "wow that was a long value name"
  6.   println(reallySuperLongValueNameThatGoesOnForeverSoYouNeedANewLine)
  7. }
  8. // Trailing opening curly brace indicates more code on next line
  9. def equalsign2(s: String) = {
  10.   println("equalsign2: " + s)
  11. }
  12. // Trailing comma, operator, etc. indicates more code on next line
  13. def commas(s1: String,
  14.            s2: String) = {
  15.   println("comma: " + s1 +
  16.           ", " + s2)
  17. }

当你需要在同一行中放置多个声明或者表达式的时候,你可以使用分号来隔开它们。我们在《第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. class OptionalUserProfileInfo(
  9.   location: String = OptionalUserProfileInfo.UnknownLocation,
  10.   age: Int         = OptionalUserProfileInfo.UnknownAge,
  11.   webSite: String  = OptionalUserProfileInfo.UnknownWebSite)
  12. println( new OptionalUserProfileInfo )
  13. println( new OptionalUserProfileInfo(age = 29) )
  14. println( new OptionalUserProfileInfo(age = 29, location="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. println( factorial(0) )
  12. println( factorial(1) )
  13. println( factorial(2) )
  14. println( factorial(3) )
  15. println( factorial(4) )
  16. 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 面向对象编程》中的“可见度规则”章节来讨论可见度的规则和对应的关键字。

#p#

类型推断

静态类型书写的代码可能会非常冗长,考虑下面典型的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. println( upCase("") )
  10. 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)
  11. }

现在不能编译了。

 
 
 
  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(' ')
  8. }

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.   }
  7. }

如果你用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!
  4. }

唯一的区别是最后的对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 类型单体实例的真正名字!(这个名字是函数式编程的协定。)

#p#

常值

一个对象经常会用一个常值来初始化,比如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. scala> val b: Byte = 127
  11. 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. scala> val b2 = false
  4. 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 lin

文章名称:Scala编程指南更少的字更多的事
分享路径:http://www.shufengxianlan.com/qtweb/news24/40524.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联