Skip to content

Latest commit

 

History

History
2423 lines (1642 loc) · 97.1 KB

File metadata and controls

2423 lines (1642 loc) · 97.1 KB

概要

  • scala.AnyRef 是所有类的父类

  • scala 即 scalable language, 它是面向对象和函数式编程的融合

  • 用 scala 编写的程序易裁剪, 因为很多数据结构和类型都是用库的方式实现的, 比如说 BigInt

  • Immutable data structures(不可变数据结构)是函数式编程的基石,另外一个特征是函数是第一类公民, 可以是参数,变量或返回值 第一条的另外一种表述方法是方法没有任何 side effects, 他们仅仅从运行环境中获取参数和返回结果

  • scala 支持类型推断

  • scala 最常见的方式是匿名函数用 "=>" 定义, <- 主要是迭代里用, -> 主要是 map 里用, => 主要是匿名函数用.

  • 语法糖 val 声明的变量是不可变的 var 声明的变量是可变的 scala 的所有类都是从 any 类派生而来的 scala 所有 value 都是 object, 所有运算符实际上都是方法 scala 中无论类型如何, == 都表示基于值的引用 ! actor 发送消息 !! 调用外部命令 println = print line

  • Traits 类似于 java 中的 interface, can then be mixed together

Programming in Scala, 3rd Edition

1.3 WHY SCALA?

Scala 的函数式特性也为编程提供了更高层次的思考. 其中最核心的理念是"函数是引用透明(referentially transparent)的"(引用透明指的是表达式可以在不影响程序行为的情况下被其计算后的值完全替换), 这意味着程序仅仅通过函数的返回结果来感知他.

不像引用, 不可变数据(Immutable data)可以自由共享, 因为副本和共享的引用在使用中没有区别. 编写并发代码时, 这一优势尤为重要.

Scala 是静态类型, 允许使用泛型参数, 使用交叉点合并类型, 并使用抽象类型隐藏类型细节. 这些机制为构建和构建自己的类型打下了坚实的基础, 以便设计出安全灵活的接口.

类型推断(type inference)

Scala 有一个非常复杂的类型推断系统, 几乎可以让你省略代码中所有多余的类型信息. Scala 中的类型推断可以走得更远. 事实上,用户代码中完全没有显式的类型信息并不罕见. 因此, Scala 程序通常看起来有点像动态脚本语言编写的程序. 这特别适用于构建客户侧应用程序代码, 因为它们通常用来粘合已完成的库. 然而对于库本身来说, 这种机制则是不太适合的, 因为库为了支持灵活的使用模式, 经常采用相当复杂的类型. 这是很自然的. 毕竟, 对于构成可重用组件接口的成员来说, 它的类型签名应该是明确的, 因为它们是组件与客户之间契约的重要组成部分。

Chapter 2 First Steps in Scala

Packages in Scala are similar to packages in Java: They partition the global namespace and provide a mechanism for information hiding. all of Java's primitive types have corresponding classes in the scala package.And when you compile your Scala code to Java bytecodes, the Scala compiler will use Java's primitive types where possible to give you the performance benefits of the primitive types.

scala 有两种类型的变量:

  • val 声明的变量是不可变的 A val is similar to a final variable in Java. Once initialized, a val can never be reassigned.
  • var 声明的变量是可变的 A var can be reassigned throughout its lifetime.

To enter something into the interpreter that spans multiple lines, just keep typing after the first line. If the code you typed so far is not complete, the interpreter will respond with a vertical bar("|") on the next line. If you realize you have typed something wrong, but the interpreter is still waiting for more input, you can escape by pressing enter twice.

定义函数时, 函数的参数必须带有类型信息, 因为 scala 不能推断函数的参数类型.

def max(x: Int, y: Int): Int = {
  if (x > y) x
  else y
}

basic_form_of_a_function_definition

Figure 2.1 - The basic form of a function definition in Scala.

如果函数是递归的, 必须显式的指定函数返回值的类型. 如果函数语句只有一行, 函数定义时可以省略大括号. Unit 类型的返回值表示函数没有返回值, 类似于 Java 的 void 类型.因此, 如果函数返回值的类型是 Unit, 那么意味着执行该函数的目的是为了它的 side effects. scala 中注释用 // 或者 /* .... */ scala 中不支持 ++i 和 i++ scala 中用 ";" 来分隔语句, 而且 ";" 是可选的 如果函数 body 只有一行, 而且只有一个参数, 那么你在定义函数时可以不用显式的定义参数

args.foreach((arg: String) => println(arg))
args.foreach(println)

syntax_of_a_function_literal Figure 2.2 - The syntax of a function literal in Scala.

for (arg <- args)
  println(arg)

"<-" 号在这里是 in 的意思, arg 是 val, for (arg <- args) 的意思是 "for arg in args."

Chapter 3 Next Steps in Scala

PARAMETERIZE ARRAYS WITH TYPES

Parameterization means "configuring" an instance when you create it. You parameterize an instance with values by passing objects to a constructor in parentheses.

val greetStrings = new Array[String](3)
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!\n"

for (i <- 0 to 2)
  print(greetStrings(i))

Listing 3.1 - Parameterizing an array with a type.

When you define a variable with val, the variable can't be reassigned, but the object to which it refers could potentially still be changed.

for (i <- 0 to 2)
  print(greetStrings(i))

上面代码中的第一行说明了 Scala 的另一个通用规则: 如果一个方法只有一个参数, 调用它的时候可以不加圆点或圆括号. 在这个例子中, 实际上是调用了一个参数类型为 Int 的方法. 代码 "0 to 2" 被转换成方法调用 "(0).to(2)". 请注意, 只有在显式的指明方法调用者的情况下, 此语法才有效. 你不能这样写 "println 10", 但是可以写成 "Console println 10".

Scala 并没有实现运算符重载技术, 因为它实际上并没有传统意义上的运算符. 作为替代, scala 中可以使用 +, - , * 和 / 等字符作为方法名称. 因此, 当你将 "1 + 2" 键入到Scala解释器中时, 实际上是在 Int object 1 上调用名为 "+" 的方法, 传入 2 作为方法参数。 "1 + 2" 与 "(1).+(2)" 是等价的.

Figure 3.1 - All operations are method calls in Scala.

这个例子另一个重要的方面是让你了解为什么要用括号访问数组中的元素. 与Java相比,Scala的特例更少. 数组不过是类的实例, 就像 Scala 中的其他类一样. 当你将包含一个或多个值的括号应用于变量时, Scala 会将代码转换成在该变量上调用 apply 方法. 所以 greetStrings(i) 会被转换为 greetStrings.apply(i). 因此在 scala 中访问数组中的元素也是通过调用方法. 这个原则不仅限于数组: 任何对象后面如果跟着括号括起来的参数(作为右值), 都将被转换为 apply 方法调用. 当然这个只有当该对象定义了 apply 方法时才会通过编译. 所以这不是特例. 这是一条通用规则.

类似的,对象后面如果跟着括号括起来的参数, 当赋值给它时(作为左值), 编译器会把它转换成一个 update 方法调用, 调用参数就是括号中的值和等号右边的值.

greetStrings(0) = "Hello"
greetStrings.update(0, "Hello")

Scala 通过将从数组到表达式的所有一切都视为带有方法的对象, 来实现概念上的简化.

val numNames = Array("zero", "one", "two")
val numNames2 = Array.apply("zero", "one", "two")

USE LISTS

函数式编程的一个重要思想就是方法调用不应该有副作用(side effects). 一个方法的唯一行为应该是计算并返回一个值. 采取这种编程范式获得的好处是方法变得更独立(方法之间依赖变少), 逻辑变得更清晰, 因此更可靠和更容易重用. 另一个好处(对于静态类型语言来说)是方法的入参和返回值都由类型检查器进行检查, 这样的话逻辑错误可以以类型错误的形式被检测出来. 将函数式编程的哲学应用到对象的世界(world of objects), 意味着使对象不可变(没有状态了, 实际上不是没有状态, 命令式编程中的状态变化转换为函数式编程中的新状态替换旧 状态).

正如你所看到的, 一个 Scala 数组是一个可变的对象序列, 它们都是相同的类型. 例如, 数组一旦初始化后, 就不能更改数组的长度, 但是你可以更改数组元素的值. 因此, 我们认为数组是可变对象.

对于由一组类型相同的对象组成的不可变序列, 可以使用 Scala 的 List 类. 与 Array[String] 一样, List[String] 只包含字符串. Scala 的 List 类 scala.List, 与 Java 的 java.util.List 类是不同的, Scala 中的 list 类是不可变的(而Java 的 list 类是可变的). 换句话说, Scala 的 list 类生来就是为了支持函数式编程的.

val oneTwoThree = List(1, 2, 3)

List 中的 ":::" 方法用于连接两个 List, 该方法返回一个新的 List.

val oneTwo = List(1, 2)
val threeFour = List(3, 4)
val oneTwoThreeFour = oneTwo ::: threeFour

List 中的 "::" 方法用于在 List 头部加入一个新的元素, 该方法也返回一个新的 List.

val twoThree = List(2, 3)
val oneTwoThree = 1 :: twoThree

如果方法名以冒号结尾, 方法在右操作数上调用, 如果方法名不是以冒号结尾, 方法在左操作数上调用.

Nil 用来指定一个空 List, 初始化新 List 的一种方法是用 "::" 方法连接各个元素, Nil 作为最后一个元素. 下面的两个表达式是等价的:

val oneTwoThree = List(1, 2, 3)
val oneTwoThree = 1 :: 2 :: 3 :: Nil
What it is What it does
List() or Nil The empty List
List("Cool", "tools", "rule") Creates a new List[String] with the three values"Cool", "tools", and "rule"
val thrill = "Will" :: "fill" :: "until" :: Nil Creates a new List[String] with the three values"Will", "fill", and "until"
List("a", "b") ::: List("c", "d") Concatenates two lists (returns a new List[String] with values "a", "b", "c", and "d")
thrill(2) Returns the element at index 2 (zero based) of thethrill list (returns "until")
thrill.count(s => s.length == 4) Counts the number of string elements in thrill that have length 4 (returns 2)
thrill.drop(2) Returns the thrill list without its first 2 elements (returns List("until"))
thrill.dropRight(2) Returns the thrill list without its rightmost 2 elements (returns List("Will"))
thrill.exists(s => s == "until") Determines whether a string element exists in thrillthat has the value "until" (returns true)
thrill.filter(s => s.length == 4) Returns a list of all elements, in order, of the thrilllist that have length 4 (returns List("Will", "fill"))
thrill.forall(s => s.endsWith("l")) Indicates whether all elements in the thrill list end with the letter "l" (returns true)
thrill.foreach(s => print(s)) Executes the print statement on each of the strings in the thrill list (prints "Willfilluntil")
thrill.foreach(print) Same as the previous, but more concise (also prints"Willfilluntil")
thrill.head Returns the first element in the thrill list (returns"Will")
thrill.init Returns a list of all but the last element in the thrilllist (returns List("Will", "fill"))
thrill.isEmpty Indicates whether the thrill list is empty (returnsfalse)
thrill.last Returns the last element in the thrill list (returns"until")
thrill.length Returns the number of elements in the thrill list (returns 3)
thrill.map(s => s + "y") Returns a list resulting from adding a "y" to each string element in the thrill list (returnsList("Willy", "filly", "untily"))
thrill.mkString(", ") Makes a string with the elements of the list (returns"Will, fill, until")
thrill.filterNot(s => s.length == 4) Returns a list of all elements, in order, of the thrilllist except those that have length 4 (returnsList("until"))
thrill.reverse Returns a list containing all elements of the thrilllist in reverse order(returnsList("until", "fill", "Will"))
thrill.sort((s, t) => s.charAt(0).toLower < t.charAt(0).toLower) Returns a list containing all elements of the thrilllist in alphabetical order of the first character lowercased (returns List("fill", "until", "Will"))
thrill.tail Returns the thrill list minus its first element (returns List("fill", "until"))

USE TUPLES

另一个有用的容器对象是元组(tuple). 像 List 一样, 元组是不可改变的, 但是与 List 不同, 元组可以包含不同类型的元素. List 可以是一个 List[Int] 或者 List [String], 而元组可以同时包含整数和字符串. 例如, 如果你需要在方法中返回多个对象, 那么元组将非常有用. Java 通常会创建一个类似 JavaBean 的类来保存多个返回值, 而在 Scala 中你可以直接返回一个元组. 要实例化一个新的元组来保存一些对象, 只需将对象放在括号中, 对象之间用逗号隔开. 一旦元组实例化成功, 你可以点, 下划线和索引(索引从1开始)组成的标记来访问元组中的元素.

val pair = (99, "Luftballons")
println(pair._1)
println(pair._2)

USE SETS AND MAPS

因为 Scala 旨在帮助你同时利用函数式和命令式编程风格, 所以集合相关的库特别注重区分可变集合和不可变集合. 例如, arrays 总是可变的; lists 总是不变的. Scala 还为 sets 和 maps 同时提供了可变和不可变的备选方案, 但是两个版本都使用相同的简单名称. 对于 sets 和 maps, Scala 通过类的层次结构对可变/不可变进行了建模.

例如, Scala API 将 sets 作为一个基础 trait(trait 类似于 Java 中的接口). 然后 Scala 又提供两个 subtraits, 一个用于可变集, 另一个用于不可变集.

Figure 3.2 - Class hierarchy for Scala sets.

  • immutable sets
var jetSet = Set("Boeing", "Airbus")
jetSet += "Lear"
println(jetSet.contains("Cessna"))
  • mutable set ("+=" 是方法名)
import scala.collection.mutable

val movieSet = mutable.Set("Hitch", "Poltergeist")
movieSet += "Shrek"
println(movieSet)

Figure 3.2 - Class hierarchy for Scala maps.

  • immutable map (没有指定导入的包, 默认的 map 就是 immutable map, map 中的元素是元组)
val romanNumeral = Map(
  1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"
)
println(romanNumeral(4))
  • mutable map ("+=" 是方法名) 1 -> "Go to island." 和 (1).->("Go to island.")) 等价
import scala.collection.mutable

val treasureMap = mutable.Map[Int, String]()
treasureMap += (1 -> "Go to island.")
treasureMap += (2 -> "Find big X on ground.")
treasureMap += (3 -> "Dig.")
println(treasureMap(2))

LEARN TO RECOGNIZE THE FUNCTIONAL STYLE

正如第1章所提到的, Scala 允许程序员以命令式的风格进行编程, 但是也鼓励你采用函数式的风格. 如果你是 Java 程序员, 那么在学习 Scala 时可能会面临的主要挑战之一是如何进行函数式编程.

首先是识别两种编程风格之间的区别. 如果代码包含任何变量, 它可能是命令式编程. 如果代码根本不包含变量, 比如说只包含 vals, 那么则可能是功能性风格. 走向函数式编程的一种方式就是在编程时不使用变量.

  • 命令式风格
def printArgs(args: Array[String]): Unit = {
  var i = 0
  while (i < args.length) {
    println(args(i))
    i += 1
  }
}
  • 函数式风格
def printArgs(args: Array[String]): Unit = {
  for (arg <- args)
    println(arg)
}

printArgs 并不是纯函数, 因为它依然有 side effects -- 打印到标准输出流, 另外一个 side effects 就是返回值是 Unit, 如果一个函数什么值都不返回, 那么它肯定在世界上做了一些其他的事情(暗示). 更偏向于函数化的编码是仅仅格式化参数,然后返回格式化后的字符串

def formatArgs(args: Array[String]) = args.mkString("\n")

每个有用的程序都可能有某种形式的副作用(side effects); 否则, 它将无法为使用者提供价值. 编写没有副作用的方法, 鼓励你设计副作用最小的程序. 这种方法的好处之一是它可以让你的程序更容易测试.

要记住, 无论是变量还是副作用都不是生来就是邪恶的. scala 不是一个纯粹的函数式的语言. scala 是函数式/命令式的混合体. 你可能会发现, 在某些情况下, 命令式编程风格更适合解决手头的问题, 在这种情况下, 你应该毫不犹豫地使用它(命令式编程).

Chapter 4 Classes and Objects

4.1 CLASSES, FIELDS, AND METHODS

类是对象的蓝图. 定义类后, 可以使用关键字 new 从蓝图创建对象.

在类定义中会放置字段和方法, 这些字段和方法统称为 members.Fields. 那些使用 val 或 var 修饰的字段用来引用对象. 而使用 def 定义的方法包含可执行代码. 变量保存对象的状态或数据, 而方法使用该数据来执行对象的计算工作. 当你实例化某个类时, 运行环境分配一些内存来保存对象的状态(即变量).

  • 追求对象健壮性的一个重要方法是确保对象的状态在其整个生命周期内保持可用, 解决方法是将字段设为 private, 使其只能被同一个类中定义的方法访问, 所有可以更新状态的代码都将在字段所属的类中.
  • Public 是 Scala 的默认访问级别.
  • Scala 中方法参数的一个重要特征是它们是 vals, 而不是 vars, 如果你尝试在方法内部修改参数, 将会导致编译失败.(呃, 函数式)
  • 如果方法中没有显式的返回语句, 那么 scala 方法将返回该方法计算的最后一个值. scala 建议编码时不要显式的指定返回值, 相反, 将每个方法视为一个表达式, 该表达式返回一个值. 这种理念将鼓励您将方法设计得非常小.
  • 如果方法只有一行表达式, 那么可以将方法的大括号省略. 如果表达式很短, 它甚至可以与 def 放在同一行. 为了将简洁做到极致, 您甚至可以省略函数返回类型, Scala 会自行推断(不建议, 容易出错).
  • 方法的 side effect 主要指改变了外部变量的状态或做了 I/O 操作. (A side effect is generally defined as mutating state somewhere external to the method or performing an I/O action.)
  • 一个返回值为 Unit 的仅为了其 side effect (通常指改变外部变量状态)而存在, 我们将这种方法称为过程(procedure).
  • ";" 号用来分隔语句, 如果一行只有一个语句, 则可以省略 ";" 号.
  • 如果你链接多行包含类似 "+" 符合的语句, 通常会将操作符放在行尾而不是行首.

偶尔 Scala 会违背你的意愿将声明分为两部分:(所以省略 ";" 号这功能没啥用)

x
+ y

scala 会将上面的代码解析为两行语句, 而不是你预期的 x + y, 因此, 无论何时链接诸如 + 的中缀操作, 常见的Scala样式是将操作符放在行的末尾而不是开头:

x +
y +
z

scala 会将上面的代码解析为一行语句.

关于语句分离的准确规则很简单. 简而言之, 除非满足下列条件之一, 否则将行结尾视为语句的结束:

  1. 行尾的符号作为语句的结尾来说不合法, 例如句号或中缀运算符.
  2. 下一行作为一个语句的开头来说无法解析.
  3. 该行在括号 (...) 或括号 [...] 内结束.(因为 (...) 或 [...] 无论如何都不能包含多个语句)

4.3 SINGLETON OBJECTS

  • scala 中的类不能含有静态成员(声称比 java 更面向对象), 单例(singleton)对象则是一个替换方案. 单例对象用 object 定义.

  • 当一个单例对象与类同名时, 我们就把它称为类的 "伴生对象"(companion object), 而这个类被称为单例对象的 "伴生类"(companion class), 类和它的伴生对象必须在同一个文件中定义. 类和伴生对象的私有成员对于对方来说都是可见的(类似于 C++ 中的友元, 还是一个对象拆成了两个部分).没有同名伴生类的单例对象称为孤立对象(standalone object).

// In file ChecksumAccumulator.scala
import scala.collection.mutable

object ChecksumAccumulator {
  // 对象引用不变, 而不是对象不变
  private val cache = mutable.Map.empty[String, Int]

  def calculate(s: String): Int =
    if (cache.contains(s))
      cache(s)
    else {
      val acc = new ChecksumAccumulator

      for (c <- s)
        acc.add(c.toByte)

      val cs = acc.checksum()
      cache += (s -> cs)
      cs
  }
}

单例对象不仅仅是静态方法的持有者, 它还是头等对象(irst-class object). 因此, 您可以将单例对象的名称视为附加到对象的 "名称标记".

定义单例对象并不是定义类型(Scala 抽象级别). 仅给出 ChecksumAccumulator object 的定义, 并不能创建 ChecksumAccumulator 类型的变量. 相反, 名为 ChecksumAccumulator 的类型由 singleton object 的伴随类定义. 但是, 单例对象扩展了超类, 并且可以混合 traits(通过 extends 关键字). 每个单例对象都是其超类和混入的 traits 的实例,你可以通过这些类型调用其方法, 从这些类型的变量引用它, 并将其传递给期望这些类型的方法. 我们将在 13 章中展示一些继承自类(classes)和特征(traits)的单例对象的例子.

类和单例对象之间的一个区别是单例对象不能接受参数, 而类可以. 因为你无法使用 new 关键字实例化单例对象, 所以无法将参数传递给它. 每个单例对象都实现为从静态变量引用的合成类(synthetic class 合成类的名称是对象名称加上美元符号)的实例, 因此它们具有与 Java 中 static 相同的初始化语义. 特别是, 单例对象在第一次访问时才初始化.

与伴随类不共享相同名称的单例对象称为 "独立对象"(standalone object). 独立对象有很多用途, 包括作为通用方法的集合或定义 Scala 应用程序的入口点.

4.4 A SCALA APPLICATION

要运行 Scala 程序, 必须提供一个独立对象的名称, 该独立对象存在 main 方法的,并且 main 方法接受一个参数 Array [String], 返回类型为 Unit. 任何一个拥有 main 方法的独立对象都可以用作应用程序的入口点.

// In file Summer.scala
import ChecksumAccumulator.calculate

object Summer {
  def main(args: Array[String]) = {
    for (arg <- args)
      println(arg + ": " + calculate(arg))
  }
}

Scala 隐式地将 java.lang 包和 scala 包的成员, 以及名为 Predef 的单例对象的成员导入到每个 Scala 源文件中. Predef 驻留在 scala 包中, 包含许多有用的方法. 例如, 当你在 Scala 源文件中说 println 时, 实际上是在 Predef 上调用 println. (Predef.println 转身调用Console.println, 它完成真正的工作). 当你说 assert 时, 你正在调用 Predef.assert.

Scala 和 Java之间的一个区别是, Java 要求你将类放在以类命名的文件中 - 例如, 你将类 SpeedRacer 放在文件 SpeedRacer.java 中, 然而在 scala 中你可以将 .scala 源码文件命名为任何你想要的东西, 无论你在文件中放入了什么 Scala 类或代码. 但是, 通常在非脚本(non-scripts)的情况下, 建议使用 java 的样式命名源文件, 这样程序员可以通过查看文件名来更轻松地找到类.

ChecksumAccumulator.scala 和 Summer.scala 都不是脚本, 因为它们以定义结尾. 与之形成对比的是, 脚本必须以结果表达式结束. 因此, 如果您尝试将 Summer.scala 作为脚本运行, Scala 解释器会抱怨 Summer.scala 没有以结果表达式结束. 所以, 您需要使用 Scala 编译器编译这些文件, 然后运行生成的类文件. 一种方法是使用 scalac, 它是基本的 Scala 编译器, 如下所示:

$ scalac ChecksumAccumulator.scala Summer.scala

这会编译您的源文件, 但在编译完成可能要花一点时间. 原因是每次编译器启动时, 它都会花时间扫描 jar 文件的内容并进行其他初始工作. 出于这个原因, Scala 发行版还包括一个名为 fsc 的 Scala compilerdaemon(用于快速 Scala 编译器). 你这样使用它:

$ fsc ChecksumAccumulator.scala Summer.scala

第一次运行 fsc 时, 它将在本地创建一个连接到计算机端口的服务器守护进程. 然后它通过端口将要编译的文件发送到守护程序, 由守护程序来进行编译. 下次运行 fsc 时, 守护程序仍然在运行, 因此 fsc 只用将文件列表发送到守护程序, 守护程序立即开始编译文件. 使用 fsc, 您只需要在 Java 运行环境第一次启动时等待. 任何时候你想停止 fsc 守护程序, 都可以使用 fsc-shutdown 来执行此操作.

运行 scalac 或 fsc 命令将生成 Java 类文件, 然后可以通过 scala 命令运行这些文件, 这与您在前面示例中用于调用解释器的命令相同. 但是, 这次并不是给它一个带有 .scala 扩展名的文件来解释, 在这种情况下, 你会给它一个包含正确方法的独立对象的名称. 因此, 您可以通过键入以下命令来运行 Summer 应用程序:

$ scala Summer of love

4.5 THE APP TRAIT

Scala 提供了一个 trait, scala.App, 可以节省一些输入时间.

import ChecksumAccumulator.calculate

object FallWinterSpringSummer extends App {
  for (season <- List("fall", "winter", "spring"))
    println(season + ": " + calculate(season))
}

要使用 trait, 首先要在单例对象的名称后面加上 "extends App". 然后, 不用编写 main 方法, 而是将你打算在 main 方法中放入的代码直接放在单例对象的花括号之间. 您可以通过名为 args 的字符串数组访问命令行参数, 这样就够了, 你可以编译和运行它.

Chapter 5 Basic Types and Operations


5.1 SOME BASIC TYPES

  • Byte 8-bit signed two's complement integer (-27 to 27 - 1, inclusive)
  • Short 16-bit signed two's complement integer (-215 to 215 - 1, inclusive)
  • Int 32-bit signed two's complement integer (-231 to 231 - 1, inclusive)
  • Long 64-bit signed two's complement integer (-263 to 263 - 1, inclusive)
  • Char 16-bit unsigned Unicode character (0 to 216 - 1, inclusive)
  • String a sequence of Chars
  • Float 32-bit IEEE 754 single-precision float
  • Double 64-bit IEEE 754 double-precision float
  • Boolean true or false

除了 string 类型在 java.lang 包中定义, 其他类型都是在 scala 中定义. scala 包和java.lang 包的所有成员都自动导入到每个 Scala 源文件中, 您可以在任何地方使用简称(即 Boolean, Char 或 String 等名称).

scala> val tower = 35L tower: Long = 35

scala> val of = 31l of: Long = 31

scala> val little = 1.2345F little: Float = 1.2345

scala> val bigger = 1.2345e1 bigger: Double = 12.345

scala> val a = 'A' a: Char = A

scala> val f = '\u0044' f: Char = D

scala> val hello = "hello" hello: String = hello

scala> val bool = true bool: Boolean = true

转义符号:

  • \n line feed (\u000A)
  • \b backspace (\u0008)
  • \t tab (\u0009)
  • \f form feed (\u000C)
  • \r carriage return (\u000D)
  • " double quote (\u0022)
  • ' single quote (\u0027)
  • \ backslash (\u005C)

你使用连续三个双引号(""")开始和结束一个 raw string. raw string 的内部可能包含任何字符, 包括换行符, 引号和特殊字符.

println("""Welcome to Ultamix 3000.
          Type "HELP" for help.""")

上面的语句返回的结果是:

Welcome to Ultamix 3000. Type "HELP" for help.

现在的问题是第二行之前的前导空格也包含在了字符串中! 要帮助解决这种问题, 可以在字符串上调用 stripMargin. 要使用此方法, 请在每行的前面放置一个管道符(|), 然后在整个字符串上调用 stripMargin:

println("""|Welcome to Ultamix 3000.
           |Type "HELP" for help.""".stripMargin)

上面的语句返回的结果是:

Welcome to Ultamix 3000. Type "HELP" for help.

symbol 在字面上写作 'ident, 其中 ident 可以是任何字母数字标识符. 此类文字映射到预定义类 scala.Symbol 的实例. 具体来说, literal'cymbal 将由编译器扩展为工厂方法调用: Symbol("cymbal"). symbol 通常在动态类型语言用作标识符. 例如, 您可能希望定义更新数据库中记录的方法:

scala> def updateRecordByName(r: Symbol, value: Any) = { // code goes here } updateRecordByName: (Symbol,Any)Unit

该方法将指示记录字段名称的 symbol 和用于更新记录中字段的值作为参数. 在动态类型语言中,您可以将未声明的字段标识符传递给方法, 但在 Scala 中, 这将无法通过编译:

scala> updateRecordByName(favoriteAlbum, "OK Computer") :6: error: not found: value favoriteAlbum updateRecordByName(favoriteAlbum, "OK Computer")

相反, 几乎同样简洁, 您可以传递 symbol:

scala> updateRecordByName('favoriteAlbum, "OK Computer")

除了找出它的名字之外, 用符号做的事情并不多:

scala> val s = 'aSymbol s: Symbol = 'aSymbol

scala> val nm = s.name nm: String = aSymbol

另一件值得注意的事情是如果两次写入相同的 Symbol, 则两个表达式都将引用完全相同的 Symbol 对象.

symbol 这东西不过是为了规避函数参数必须为 val 的限制

5.3 STRING INTERPOLATION

Scala 包含一个灵活的字符串插值机制, 允许您在字符串中嵌入表达式. 它最常见的用例是为字符串连接提供简洁易读的替代方法. 这是一个例子:

val name = "reader"
println(s"Hello, $name!")

可以在字符串中的美元符号后面的大括号内放置表达式, 对于单变量表达式, 可以将变量名称放在美元符号后面(不需要大括号)

scala> s"The answer is ${6 * 7}." res0: String = The answer is 42.

除了 s 以外, Scala 默认提供另外两个字符串插值器: raw 和 f.

raw 字符串插值器的行为类似于 s, 除了它不识别转义字符.

println(raw"No\\escape!") // prints: No\\escape!

f 字符串插值器允许您将 printf 样式的格式化指令附加到嵌入式表达式.

scala> f"${math.Pi}%.5f" res1: String = 3.14159

如果没有为嵌入式表达式提供格式化指令, 则 f 字符串插值器将默认为 %s.

scala> val pi = "Pi" pi: String = Pi scala> f"$pi is approximately ${math.Pi}%.8f." res2: String = Pi is approximately 3.14159265.

在 Scala 中, 字符串插值是通过在编译时重写代码来实现的. 库和用户可以为其他目的自定义字符串插值器.

5.4 OPERATORS ARE METHODS

Scala 为其基本类型提供了丰富的运算符. 正如前面章节中提到的, 这些运算符实际上只是普通方法调用. 例如, 1 + 2 实际上与1.+(2) 相同. 换句话说, 类 Int 包含一个名为 + 的方法, 它接受一个 Int 并返回一个 Int 结果. 添加两个 Ints 时会调用 + 方法: scala> val sum = 1 + 2 // Scala invokes 1.+(2) sum: Int = 3

实际上, Int 包含几个带有不同参数类型的重载 + 方法. 例如, Int 有另一个方法, 也叫做 +, 它接受 Long 类型的参数并返回一个 Long. 如果添加 Long 和 Int, 将调用此 + 方法, 如下所示:

scala> val longSum = 1 + 2L // Scala invokes 1.+(2L) longSum: Long = 3

注: 算术运算都是方法调用.

符号 "+" 是一个运算符 - 一个特定的中缀运算符. scala 中的运算符不同于其他语言中的运算符, 您可以像使用运算符一样调用任何方法.

注: infix notation: 中缀表示法, 意味着调用的方法位于对象和您希望传递给方法的参数之间.

scala> val s = "Hello, world!" s: String = Hello, world! scala> s indexOf 'o' // Scala invokes s.indexOf('o') res0: Int = 4

但是如果方法接受多个参数, 必须将这些参数放在括号中.

scala> s indexOf ('o', 5) // Scala invokes s.indexOf('o', 5) res1: Int = 8

ANY METHOD CAN BE AN OPERATOR

除了中缀表示法外, Scala 还有另外两种运算符表示法: 前缀和后缀. 在前缀(prefix)表示法中, 将方法名称放在要调用方法的对象之前(例如, -7 中的 "-"). 在后缀(postfix)表示法中, 将方法放在对象之后(例如, "7 toLong" 中的 "toLong"). 前缀和后缀运算符是一元的: 它们只需要一个操作数. 中缀运算符需要两个操作数.

可用作前缀运算符的标识符是 "+", "-", "!", 和 "~". 因此, 如果定义名为 unary_! 的方法, 则可以使用前缀运算符表示法(例如 !p)对相应类型的值或变量调用该方法. 但是, 如果定义一个名为 unary_* 的方法, 则无法使用前缀运算符表示法, 因为 "" 不是可用作前缀运算符的四个标识符之一. 您可以直接调用该方法, 如 inp.unary_, 但如果您尝试通过 *p 调用它, Scala将解析为 *.p 一样, 这可不是您想要的!

后缀运算符是在没有点或括号的情况下调用不带参数的方法. 在 Scala 中, 您可以在方法调用上不写空括号(没有参数时). 如果方法有副作用, 你可以包括括号, 例如 println(), 但如果方法没有副作用, 你可以不写, 例如在 String 上调用 toLowerCase:

scala> val s = "Hello, world!" s: String = Hello, world!

scala> s toLowerCase res4: String = hello, world!

5.5 ARITHMETIC OPERATIONS

当左右操作数都是整数类型(Int, Long, Byte, Short 或 Char)时, "/" 运算符将告诉您商的整数部分, 不包括任何余数. "%"运算符表示整数除法的余数.

使用 "%" 获得的浮点余数不是 IEEE 754 标准定义的值. IEEE 754 余数在计算余数时使用舍入除法, 而不是截断除法, 因此它与整数余数运算完全不同. 如果您真的需要 IEEE 754 余数, 可以在 scala.math 上调用 IEEEremainder, 如下所示:

scala> math.IEEEremainder(11.0,4.0)  res14:Double = -1.0

数字类型还提供一元前缀运算符 "+"(对应方法 unary_+)和 "-"(对应方法 unary_-), 它们允许您标识数字是正数还是负数, 如 -3 或 +4.0. 如果未指定一元运算符 "+" 或 "-", 则将数字默认为正数. 一元 - 也可用于反转变量. 这里有些例子:

scala> val neg = 1 + -3  neg:Int = -2

 scala> val y = +3  y:Int = 3

 scala> -neg  res15:Int = 2

5.6 RELATIONAL AND LOGICAL OPERATIONS

5.7 BITWISE OPERATIONS

5.8 OBJECT EQUALITY

如果要比较两个对象是否相等,可以使用 "==" 或其反向 "!=". 这些操作实际上适用于所有对象, 而不仅仅是基本类型.

如您所见, "==" 经过精心设计, 因此在大多数情况下能够如你所愿. 这是通过一个非常简单的规则来完成的: 首先检查左侧是否为空. 如果它不为 null, 则调用 equals 方法. 由于 equals 是一种方法, 因此您获得的比较结果取决于左侧操作数的类型. 由于存在自动空检查, 因此您无需亲自进行检查.

在 Java 中, 您可以使用 == 来比较原始类型和引用类型. 在原始类型上, Java "==" 比较的是它们的值, 就像在 Scala 中一样. 但是, 在引用类型上, Java 的 == 比较的是引用相等性, 这意味着两个变量是否指向 JVM 堆上的同一个对象. Scala 提供了一个用于比较引用相等性的工具, 名称为 eq. 但是, eq 及 ne 仅适用于直接映射到 Java 对象的对象. 有关 eq 和 ne 的完整详细信息, 请参见第 11.1 节和第 11.2 节. 另外, 请参阅第 30 章, 了解如何编写好的 equals 方法.

5.9 OPERATOR PRECEDENCE AND ASSOCIATIVITY

5.10 RICH WRAPPERS

Table 5.4 - Some rich operations

Code Result
0 max 5 5
0 min 5 0
-2.7 abs 2.7
-2.7 round -3L
1.5 isInfinity false
(1.0 / 0) isInfinity true
4 to 6 Range(4, 5, 6)
"bob" capitalize "Bob"
"robert" drop 2 "bert"

Table 5.5 - Rich wrapper classes

Basic type Rich wrapper
Byte scala.runtime.RichByte
Short scala.runtime.RichShort
Int scala.runtime.RichInt
Long scala.runtime.RichLong
Char scala.runtime.RichChar
Float scala.runtime.RichFloat
Double scala.runtime.RichDouble
Boolean scala.runtime.RichBoolean
String scala.collection.immutable.StringOps

Chapter 6 Functional Objects


6.1 A SPECIFICATION FOR CLASS RATIONAL

6.2 CONSTRUCTING A RATIONAL

IMMUTABLE OBJECT TRADE-OFFS

不可变对象相对于可变对象提供了若干优点, 但也有一个潜在的缺点. 首先, 不可变对象通常比可变对象更容易推理, 因为它们没有随时间变化的复杂状态空间. 其次, 你可以非常自由地传递不可变对象, 而在传递可变对象时, 你可能需要制作防御性副本. 第三, 一旦正确构造了不可变对象, 两个线程同时访问不可变对象时就没有办法破坏它的状态, 因为没有线程可以改变不可变对象的状态. 第四, 不可变对象制作安全的哈希表键. 例如, 如果可变对象在放入 HashSet 后发生变化, 则下次查看 HashSet 时就可能找不到该对象.

不可变对象的主要缺点在更新时它们有时需要复制大批对象. 在某些情况下, 这可能很难表达, 也可能导致性能瓶颈. 因此, 库为不可变类提供可变替代方案的情况并不少见. 例如, StringBuilder 类是对不可变类 String 的可变替代. 我们将在 18 章中为您提供有关在 Scala 中设计可变对象的更多信息.

在 Java 中, 类的构造函数可以使用参数; 而在 Scala 中, 类可以直接获取参数. Scala 表示法更简洁, 参数可以直接在类的主体中使用; 没有必要定义字段并编写构造函数将参数赋值给字段.

class Rational(n: Int, d: Int) {
  println("Created " + n + "/" + d)
}

6.3 REIMPLEMENTING THE TOSTRING METHOD

class Rational(n: Int, d: Int) {
  override def toString = n + "/" + d
}

6.4 CHECKING PRECONDITIONS

前提条件是对传递给方法或构造函数的值的约束, 这是调用者必须满足的要求.

class Rational(n: Int, d: Int) {
  require(d != 0)
  override def toString = n + "/" + d
}

6.5 ADDING FIELDS

为了保持 Rational 的不可变特性, add 方法不能将传递的 Rational 添加到自身. 相反, 它必须创建一个新的 Rational 来保存总和, 然会返回.

class Rational(n: Int, d: Int) { // This won't compile
  require(d != 0)
  override def toString = n + "/" + d
  def add(that: Rational): Rational = new Rational(n * that.d + that.n * d, d * that.d)
}

尽管类参数 n 和 d 在 add 方法的代码中可见, 但是只有对象内部的方法才能访问它们的值. 因此, 当您在 add 的实现中说 n 或 d 时, 编译器很乐意为您提供这些类参数的值. 但它不会让你说 that.n 或 that.d, 因为 that 并没有引用调用 add 的Rational 对象. 要访问其他类的 n 和 d, 您需要将它们放入类的定义中.

class Rational(n: Int, d: Int) {
  require(d != 0)
  val numer: Int = n
  val denom: Int = d
  override def toString = numer + "/" + denom
  def add(that: Rational): Rational =
    new Rational(
      numer * that.denom + that.numer * denom,
      denom * that.denom
    )
}

注: 类参数的可见性问题.

6.6 SELF REFERENCES

this

6.7 AUXILIARY CONSTRUCTORS

在 Scala 中, 除主要构造函数之外的构造函数称为辅助构造函数(auxiliary constructors). Scala 中的辅助构造函数以 def this(...) 开头.

class Rational(n: Int, d: Int) {

  require(d != 0)

  val numer: Int = n
  val denom: Int = d

  def this(n: Int) = this(n, 1) // auxiliary constructor

  override def toString = numer + "/" + denom

  def add(that: Rational): Rational =
    new Rational(
      numer * that.denom + that.numer * denom,
      denom * that.denom
    )
}

在 Scala 中, 每个辅助构造函数必须首先调用同一个类中的其他构造函数. 换句话说, 每个 Scala 类中每个辅助构造函数中的第一个语句都将具有 "this(...)" 形式. 调用的构造函数可以是主构造函数(如 Rational 示例中所示), 也可以是另一个辅助构造函数. 此规则的效果是 Scala 中的每个构造函数调用最终都会调用类的主构造函数. 因此, 主构造函数是构建类的唯一入口点.

在 Java 中, 构造函数必须调用同一个类的另一个构造函数, 或者直接调用超类的构造函数作为其第一个操作. 在 Scala 类中, 只有主构造函数可以调用超类构造函数.

6.8 PRIVATE FIELDS AND METHODS

6.9 DEFINING OPERATORS

6.10 IDENTIFIERS IN SCALA

字母数字标识符以字母或下划线开头, 后面可以跟其他字母, 数字或下划线. $ 字符也算作一个字母; 但是, 它被保留, 用于 Scala 编译器生成的标识符. 用户程序中的标识符不应包含 $ 字符, 即使它可以通过编译; 如果非要这样做, 这可能会导致与 Scala 编译器生成标识符的名称冲突.

Scala 遵循 Java 使用 camel-case 标识符的惯例, 例如 toString 和 HasSet. 虽然下划线在标识符中是合法的, 但它们通常不会在 Scala 程序中使用, 部分是为了与 Java 保持一致, 还因为下划线在 Scala 代码中有许多其他非标识符用法. 因此, 最好避免使用 to_string, init 或 name_ 等标识符. 字段, 方法参数, 局部变量和函数的 Camel-case 名称应以小写字母开头, 例如: length, flatMap 和 s. 类和 traits 的 Camel-case 名称应以大写字母开头, 例如: BigInt, List 和 UnbalancedTreeMap.

在 Scala 中, 常量不仅仅意味着 val. 即使 val 在初始化后就保持不变, 它仍然是一个变量. 例如, 方法参数是 val, 但每次调用该方法时, 这些 val 可以包含不同的值.

注: val 是初始化不可变. 常量是编译时就已经不可变了.

在 Java 中, 惯例是将常量名称全部大写, 下划线分隔单词, 例如 MAX_VALUE 或 PI. 在 Scala 中, 约定仅仅将第一个字符大写.

Scala 编译器将在内部 "修改" 运算符标识符, 将它们转换为嵌入了 $ 字符的合法Java标识符. 例如, 标识符 :-> 将在内部表示为 $colon$minus$greater. 如果您想从 Java 代码访问此标识符, 则需要使用此内部表示.

由于 Scala 中的运算符标识符可以是任意长度, 因此 Java 和 Scala 之间存在细微差别. 在 Java 中, 输入 x<-y 将被解析为四个词法符号, 因此它将等同于 x < - y. 在 Scala 中, <- 将被解析为单个标识符, 给出 x <- y. 如果你想要第一个解释, 你需要用空格分隔 < 和 - 字符. 这在实践中不太可能成为问题, 因为很少有人会在 Java 中编写 x <- y 而不在运算符之间插入空格或括号.

混合标识符由字母数字标识符组成, 后跟下划线和运算符标识符. 例如, 用作方法名称的 unary_+ 定义了一元 "+" 运算符. 或者, myvar_= 用作方法名称定义赋值运算符. 此外, 像 myvar_= 一样的混合标识符由 Scala 编译器生成以支持属性(更多内容见 18 章).

文字标识符是 `...` 中包含的任意字符串. 文字标识符的一些示例是: `x` `` `yield`

我们的想法是, 您可以将运行时接受的任何字符串作为反引号之间的标识符. 结果始终是 Scala 标识符. 即使反引号中包含的名称是 Scala 保留字也可以工作. 一个典型的用例是访问 Java 的 Thread 类中的静态 yield 方法. 你不能写 Thread.yield() 因为 yield 是 Scala 中的保留字. 但是, 您仍然可以在反引号中命名方法, 例如 Thread.`yield`().

6.11 METHOD OVERLOADING

6.12 IMPLICIT CONVERSIONS

您可以创建一个隐式转换, 在需要时自动将整数转换为有理数.

scala> implicit def intToRational(x: Int) = new Rational(x)

scala> val r = new Rational(2,3) r: Rational = 2/3

scala> 2 * r res15: Rational = 4/3

要使隐式转换起作用, 它必须在 scope 内. 如果将隐式方法定义放在 Rational 类中, 它将不在解释器的范围内. 现在, 您需要直接在解释器中定义它.

6.13 A WORD OF CAUTION

正如本章所述, 使用运算符名称和定义隐式转换创建方法可以帮助您设计客户端代码简洁易懂的库. Scala 为您提供了设计这些易于使用的库的强大功能. 但请记住, 能力越强, 责任越大.

如果使用不当, 操作符方法和隐式转换都会导致难以阅读和理解的客户端代码. 因为隐式转换是由编译器隐式应用的, 而不是在源代码中明确写出, 所以对于客户端程序员来说, 应用隐式转换是不感知的. 虽然运算符方法通常会使客户端代码更简洁, 但它们只会使客户端程序员能够识别并记住每个运算符的含义, 使其更具可读性.

在设计库时, 您应该牢记的目标不仅仅是启用简洁的客户端代码, 而是可读, 易懂的客户端代码. 简洁性通常是可读性的重要组成部分, 但是你可以把它简洁做到极致. 通过设计能够实现高雅简洁且同时可理解的客户端代码的库, 从而帮助客户端程序员高效地工作.

6.14 CONCLUSION

Chapter 7 Built-in Control Structures

您会注意到的一件事是, 几乎所有 Scala 的控制结构都会产生一些值. 这是函数式语言所采用的方法, 其中程序被视为值的计算, 因此程序的组件也应该计算值. 在命令式语言中, 函数调用可以返回一个值, 即使被调用函数更新作为参数传进来的输出变量也可以正常工作. 此外, 命令式语言通常具有三元运算符(ternary operator, 例如 C, C++ 和 Java 的 ?: 运算符), 其行为与 if 完全相同, 但会返回一个值. Scala 采用这种三元运算符模型, 只是将其称为 if. 换句话说, Scala 的 if 可以产生一个值. 除此以外, Scala 中的 for, try 和 match 也一样.

程序员可以使用这些结果值来简化代码, 就像它们使用函数的返回值一样. 如果没有此工具, 程序员必须创建临时变量, 以保存在控制结构内计算的结果. 删除这些临时变量会使代码变得更简单, 并且它还可以防止在一个分支中设置变量但又忘记在另一个分支中设置变量的许多错误.

7.1 IF EXPRESSIONS

命令式风格:

var filename = "default.txt"
if (!args.isEmpty)
  filename = args(0)

Scala 中 if 有返回值:

val filename =
  if (!args.isEmpty) args(0)
  else "default.txt"

上例中使用 val 是一种函数式样式, 它可以像 Java 中的 final 变量一样帮助您. 它告诉读者变量永远不会改变, 从而使他们不必扫描变量范围内的所有代码, 看它是否会发生变化.

使用 val 而不是 var 的第二个好处是它更好地支持等式推理(equational reasoning). 假设表达式没有副作用, 变量等于计算它的表达式. 因此, 无论何时你想用变量时, 您都可以用表达式来代替. 例如, 你可以写下这个, 而不是 println(filename):

println(if (!args.isEmpty) args(0) else "default.txt")

7.2 WHILE LOOPS

while 和 do-while 结构被称作 "循环", 而不是表达式, 因为它们不会产生有意义的值. "循环" 产生的结果类型是 Unit. 事实证明存在一个值(实际上只有一个值), 其类型为 Unit. 它被称为 unit value 并写做 (). () 的存在是 Scala Unit 与 Java void 的区别.

scala> def greet() = { println("hi") } greet: ()Unit

scala> () == greet() hi res0: Boolean = true

因为 greet 函数体中没有等号(没有求值操作), 所以 greet 被定义为返回类型为 Unit 的过程. 因此 greet 函数返回 unit value. 这在下一行中得到证实: 将 greet 函数的返回值与 () 进行比较, 结果为 true.

返回 Unit 值的另一个结构是重新给变量赋值. 例如, 如果您尝试使用以下 while 循环来读取 Scala 中的行, 您将遇到麻烦:

var line = ""
while ((line = readLine()) != "") // This doesn't work!
  println("Read: " + line)

编译此代码时, Scala 会给出一个警告, 即使用 != 比较 Unit 和 String 类型的值将始终为 true. 而在 Java 中, 赋值操作的结果总是得到所赋的值(在本例中是标准输入中的一行), 而在 Scala 赋值中总是得到 unit 值, (). 因此, 赋值 "line = readLine()" 的值将始终为 () 且永远不会为 "". 因此, 此 while 循环的条件永远不会为 false, 因此循环将永远不会终止.

注: scala 中, readLine() 返回的是 unit, 而不是 java 中的字符串. 感觉 scala 中所有 io 相关的函数返回的都是 unit.

因为 while 循环不返回任何值, 所以它常常被纯函数式语言排除在外. 这些语言只有表达式, 而不包括循环. 尽管如此, Scala 包含 while 循环, 因为有时候命令式解决方案可读性更强. 例如, 如果要编写一个重复某个过程直到某些条件发生变化的算法, while 循环可以直接表达它, 如果使用函数递归来代替, 对于某些读者来说可能是拗口的.

例如,代码清单 7.4 显示了另一种确定两个数字的最大公约数的方法. 给定 x 和 y 的两个相同值, 清单 7.4 中显示的 gcd 函数将返回与 gcdLoop 函数相同的结果, 如清单 7.2 所示. 这两种方法的区别在于 gcdLoop 是以命令式的方式编写的, 使用了变量和 while 循环.

而 gcd 是以更偏向函数式的风格编写的, 涉及递归(gcd 调用自身)并且不需要变量.

def gcdLoop(x: Long, y: Long): Long = {
  var a = x
  var b = y
  while (a != 0) {
    val temp = a
    a = b % a
    b = temp
  }
  b
}
def gcd(x: Long, y: Long): Long =
  if (y == 0) x else gcd(y, x % y)

通常情况下, 我们建议您以与对待变量相同的方式来对待代码中的 while 循环. 实际上, 虽然循环和变量通常是齐头并进的. 因为虽然 while 循环不会产生值, 但为了控制程序跳出循环, while 循环通常需要更新变量或执行 I/O. 您可以在前面显示的 gcdLoop 示例中看到此操作. while 循环会更新变量 a 和 b. 因此, 我们建议您对代码中的 while 循环持怀疑态度. 如果没有使用 while 或 do-while 循环的良好理由, 试着找到一种方法来代替它们.

7.3 FOR EXPRESSIONS

generator

val filesHere = (new java.io.File(".")).listFiles

for (file <- filesHere)         //generator
  println(file)

Filtering

val filesHere = (new java.io.File(".")).listFiles

for (file <- filesHere if file.getName.endsWith(".scala"))
  println(file)

Nested iteration

def fileLines(file: java.io.File) =
  scala.io.Source.fromFile(file).getLines().toList

def grep(pattern: String) =
  for (
    file <- filesHere
    if file.getName.endsWith(".scala");
    line <- fileLines(file)
    if line.trim.matches(pattern)
  ) println(file + ": " + line.trim)

grep(".*gcd.*")

如果您愿意, 可以使用花括号而不是括号来围绕生成器和过滤器. 使用花括号的一个优点是你可以省略使用括号时所需的一些分号, 因为如第 4.2 节所述, Scala 编译器在括号内不会推断出分号.

Mid-stream variable bindings

def grep(pattern: String) =
  for (
    file <- filesHere
    if file.getName.endsWith(".scala");
    line <- fileLines(file)
    trimmed = line.trim
    if trimmed.matches(pattern)
  ) println(file + ": " + trimmed)

grep(".*gcd.*")

Producing a new collection

def scalaFiles =
  for {
    file <- filesHere
    if file.getName.endsWith(".scala")
  } yield file

7.4 EXCEPTION HANDLING WITH TRY EXPRESSIONS

Catching exceptions

import java.io.FileReader
import java.io.FileNotFoundException
import java.io.IOException

try {
  val f = new FileReader("input.txt")
// Use and close file
} catch {
  case ex: FileNotFoundException => // Handle missing file
  case ex: IOException => // Handle other I/O error
}

与 Java 不同, Scala 不要求您在 catch 中列出待检查的异常或在 throws 子句中声明它们. 如果您希望使用 @throws 注释, 则可以声明 throws 子句, 但这不是必需的.

Yielding a value

与在 Java 中一样, 如果 finally 子句包含显式返回语句或抛出异常, 则该返回值或异常将 "覆盖" 源自 try 块或其 catch 子句之一的任何先前子句.

def f(): Int = try return 1 finally return 2

调用上面的函数将返回 2.

def g(): Int = try 1 finally 2

调用上面的函数将返回 1.

7.5 MATCH EXPRESSIONS

val firstArg = if (args.length > 0) args(0) else ""
firstArg match {
  case "salt" => println("pepper")
  case "chips" => println("salsa")
  case "eggs" => println("bacon")
  case _ => println("huh?")      // default case
}

与 Java 的 switch 语句有一些重要的区别. 一个是在 Scala 中可以使用任何类型的常量以及其他东西, 而不仅仅是 Java case 语句支持的整数类型, 枚举和字符串常量. 另一个区别是 case 的末尾都没有 break.

val firstArg = if (!args.isEmpty) args(0) else ""
val friend =
  firstArg match {
    case "salt" => "pepper"
    case "chips" => "salsa"
    case "eggs" => "bacon"
    case _ => "huh?"
  }

println(friend)

7.6 LIVING WITHOUT BREAK AND CONTINUE

最简单的方法是用布尔变量替换 break, 用 if 替换 continue.

int i = 0; // This is Java
boolean foundIt = false;
while (i < args.length) {
  if (args[i].startsWith("-")) {
    i = i + 1;
    continue;
  }
  if (args[i].endsWith(".scala")) {
    foundIt = true;
    break;
  }

  i = i + 1;
}
var i = 0
var foundIt = false

while (i < args.length && !foundIt) {
  if (!args(i).startsWith("-")) {
    if (args(i).endsWith(".scala"))
      foundIt = true
  }
  i = i + 1
}
def searchFrom(i: Int): Int =
  if (i >= args.length) -1
  else if (args(i).startsWith("-")) searchFrom(i + 1)
  else if (args(i).endsWith(".scala")) i
  else searchFrom(i + 1)

val i = searchFrom(0)

Scala 编译器实际上不会将上面的代码编译成递归调用. 因为所有递归调用都处于函数末尾, 所以编译器的输出类似于使用 while 循环的代码. 每个递归调用的实现都是跳回函数的开头. 尾递归优化在 8.9 节讨论.

import scala.util.control.Breaks._
import java.io._

val in = new BufferedReader(new InputStreamReader(System.in))

breakable {
  while (true) {
    println("? ")
    if (in.readLine() == "") break
  }
}

7.7 VARIABLE SCOPE

7.8 REFACTORING IMPERATIVE-STYLE CODE

// Returns a row as a sequence
def makeRowSeq(row: Int) =
  for (col <- 1 to 10) yield {
    val prod = (row * col).toString
    val padding = " " * (4 - prod.length)
    padding + prod
  }

// Returns a row as a string
def makeRow(row: Int) = makeRowSeq(row).mkString

// Returns table as a string with one row per line
def multiTable() = {

  val tableSeq = // a sequence of row strings
    for (row <- 1 to 10)
    yield makeRow(row)

  tableSeq.mkString("\n")
}

7.9 CONCLUSION

Chapter 8 Functions and Closures

8.1 METHODS

import scala.io.Source

object LongLines {

  def processFile(filename: String, width: Int) = {
    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(filename, width, line)
  }

  private def processLine(filename: String,
    width: Int, line: String) = {

    if (line.length > width)
      println(filename + ": " + line.trim)
  }
}

在方法中定义方法:

def processFile(filename: String, width: Int) = {

  def processLine(filename: String,
    width: Int, line: String) = {
  if (line.length > width)
    println(filename + ": " + line.trim)
  }

  val source = Source.fromFile(filename)
  for (line <- source.getLines()) {
    processLine(filename, width, line)
  }
}

8.3 FIRST-CLASS FUNCTIONS

scala> increase = (x: Int) => x + 9999 increase: Int => Int =

scala> increase(10) res1: Int = 10009

8.4 SHORT FORMS OF FUNCTION LITERALS

scala> someNumbers.filter((x) => x > 0) res5: List[Int] = List(5, 10)

8.5 PLACEHOLDER SYNTAX

scala> someNumbers.filter(_ > 0) res7: List[Int] = List(5, 10)

scala> val f = (: Int) + (: Int) f: (Int, Int) => Int = scala> f(5, 10) res9: Int = 15

8.6 PARTIALLY APPLIED FUNCTIONS

scala> sum(1, 2, 3) res10: Int = 6

scala> val a = sum _ a: (Int, Int, Int) => Int =

scala> a(1, 2, 3) res11: Int = 6

scala> val b = sum(1, _: Int, 3) b: Int => Int =

scala> b(2) res13: Int = 6

8.7 CLOSURES

8.8 SPECIAL FUNCTION CALL FORMS

Repeated parameters(可变参数)

"String*" 实际上是 Array[String]

scala> def echo(args: String*) = for (arg <- args) println(arg) echo: (args: String*)Unit

scala> val arr = Array("What's", "up", "doc?") arr: Array[String] = Array(What's, up, doc?)

scala> echo(arr) :10: error: type mismatch; found : Array[String]

scala> echo(arr: _*) What's up doc?

Named arguments

scala> def speed(distance: Float, time: Float): Float = distance / time speed: (distance: Float, time: Float)Float

scala> speed(100, 10) res27: Float = 10.0

scala> speed(distance = 100, time = 10) res28: Float = 10.0

scala> speed(time = 10, distance = 100) res29: Float = 10.0

也可以混合使用位置和命名参数. 在这种情况下, 位置参数首先出现. 命名参数最常与默认参数值结合使用.

Default parameter values

def printTime2(out: java.io.PrintStream = Console.out, divisor: Int = 1) =
  out.println("time = " + System.currentTimeMillis()/divisor)

8.9 TAIL RECURSION

尾调用优化仅限于方法或嵌套函数直接将其自身称为最后一个操作的情况, 而不通过函数值或其他中介.

### 8.10 CONCLUSION

foreach 方法定义在 Traversable trait 中

Chapter 9 Control Abstraction

9.1 REDUCING CODE DUPLICATION

高阶函数 - 将函数作为参数的函数 - 为您提供了压缩和简化代码的额外机会.

def filesMatching(query: String, matcher: (String, String) => Boolean) = {
  for (file <- filesHere; if matcher(file.getName, query))
    yield file
}
object FileMatcher {
  private def filesHere = (new java.io.File(".")).listFiles

  private def filesMatching(matcher: String => Boolean) =
    for (file <- filesHere; if matcher(file.getName))
      yield file

  def filesEnding(query: String) =
    filesMatching(_.endsWith(query))

  def filesContaining(query: String) =
    filesMatching(_.contains(query))

  def filesRegex(query: String) =
    filesMatching(_.matches(query))
}

9.2 SIMPLIFYING CLIENT CODE

def containsNeg(nums: List[Int]): Boolean = {
  var exists = false
  for (num <- nums)
    if (num < 0)
      exists = true
  exists
}
def containsNeg(nums: List[Int]): Boolean = {
  var exists = false
  for (num <- nums)
    if (num % 2 == 1)
      exists = true
  exists
}
def containsOdd(nums: List[Int]) = nums.exists(_ % 2 == 1)

9.3 CURRYING

scala> def plainOldSum(x: Int, y: Int) = x + y plainOldSum: (x: Int, y: Int)Int

scala> plainOldSum(1, 2) res4: Int = 3

scala> def curriedSum(x: Int)(y: Int) = x + y curriedSum: (x: Int)(y: Int)Int

scala> curriedSum(1)(2) res5: Int = 3

当你调用柯里化后的 sum (curriedSum)时, 你实际上得到了两个传统的函数调用. 第一个函数调用采用名为 x 的单个 Int 参数, 并返回一个单参数的函数. 第二个函数采用 Int 参数 y.

9.4 WRITING NEW CONTROL STRUCTURES

def withPrintWriter(file: File, op: PrintWriter => Unit) = {
  val writer = new PrintWriter(file)
  try {
    op(writer)
  } finally {
    writer.close()
  }
}

withPrintWriter(
  new File("date.txt"),
  writer => writer.println(new java.util.Date)
)

这种技术称为贷款模式, 因为控制抽象函数(如 withPrintWriter)打开资源并将其 "贷款" 给函数. 例如, 在上一个示例中, withPrintWriter 将一个 PrintWriter 借给函数 op. 当函数完成时, 它表示它不再需要 "借用" 资源. 然后在 finally 块中关闭资源, 无论函数是正常返回还是抛出异常.

def withPrintWriter(file: File)(op: PrintWriter => Unit) = {
  val writer = new PrintWriter(file)
  try {
    op(writer)
  } finally {
    writer.close()
  }
}

val file = new File("date.txt")

withPrintWriter(file) { writer =>
  writer.println(new java.util.Date)
}

在此示例中, 第一个参数列表(包含一个 File 参数)被括号括起来. 第二个参数列表包含一个函数参数, 由大括号括起来.

9.5 BY-NAME PARAMETERS(传名参数)

传值参数在函数调用之前表达式会被求值, 例如 Int, Long 等数值参数类型; 传名参数在函数调用前表达式不会被求值, 而是会被包裹成一个匿名函数作为函数参数传递下去, 例如参数类型为无参函数的参数就是传名参数.

在函数声明时, 参数类型中添加一个 =>, 参数的类型就变成了无参函数, 类型为 () => String, 按照 Scala 针对无参函数的简化规则, 可以省略 () 写作 => String. 因为参数的类型是无参函数, 所以此处是按名传递。

如果参数类型是无参函数, 则按名传递, 否则按值传递. 注意, 如果参数类型是函数类型, 但不是无参函数, 还是按值传递.

传值参数:

object Test {
  def invode(f: String => Int => Long) = {
    println("call invoke")
    f("1")(2)
  }
  def curry(s: String)(i: Int): Long = {
    s.toLong + i.toLong
  }
  def main(args: Array[String]) {
    invode{println("eval parameter expression");curry}
  }
}

传名参数:

object Test {
  def invode(f: => String => Int => Long) = {  // 参数 f 的类型有变化
    println("call invoke")
    f("1")(2)
  }
  def curry(s: String)(i: Int): Long = {
    s.toLong + i.toLong
  }
  def main(args: Array[String]) {
    invode{println("eval parameter expression");curry}
  }
}

Chapter 10 Composition and Inheritance

组合(Composition)意味着一个类引用另一个类, 使用引用的类来帮助它完成其任务. 继承是超类/子类关系.

10.1 A TWO-DIMENSIONAL LAYOUT LIBRARY

组合器(combinators)的参数都是一个函数, 这个函数的输入输出都是列表元素, 它们将某些域的元素组合成新元素.

10.2 ABSTRACT CLASSES

抽象类不能实例化, 抽象方法没有函数体, 不用 abstract 声明.

abstract class Element {
  def contents: Array[String]
}

10.3 DEFINING PARAMETERLESS METHODS

abstract class Element {
  def contents: Array[String]              // 因为函数没有参数, 所以省略 ()
  def height: Int = contents.length
  def width: Int = if (height == 0) 0 else contents(0).length
}
abstract class Element {
  def contents: Array[String]
  val height = contents.length
  val width = if (height == 0) 0 else contents(0).length
}

从客户的角度来看, 这两对定义完全相同. 唯一的区别是访问类的属性可能比调用类的方法稍快, 因为类的属性是在初始化类时算好的, 而不是在每次方法调用时都要重新计算. 另一方面, 属性在每个 Element 对象中需要额外的内存空间.

如果函数没有副作用, 则可以用其结果替换函数调用, 而不改变程序的行为. 这意味着没有参数或副作用的函数在语义上等同于保存该函数返回值的 val. 由于这个特性, 随着类的发展, 程序员可能会在使用 val 或调用函数之间来回切换, 因为方便或效率的要求.

由于您可以省略括号(parentheses), 这意味着调用类似 Element.height 的代码不需要知道也不关心 height 是函数还是 val. 因此, Element 类的实现者可以在两者之间自由地进行切换, 而无需更改任何调用代码(尽管需要重新编译). 它稳定了类的对外接口. 例如, 您可以通过调用 List 上的 size 方法来获取 size, 函数复杂度可能是 O(n), 然后出于效率的原因将 size 更改为 val.

用空括号定义的方法, 例如 def height():Int, 称为 empty-paren 方法. 如果函数调用存在副作用, 约定建议保留括号, 以明确该类成员肯定是函数调用, 因此可能不是引用透明的. 了解函数是否产生副作用非常重要, 因此可以避免重复调用产生副作用. 如果你不关心它是否是一个函数, 你最好把它当成不是.

总而言之, Scala 鼓励将不带参数且没有副作用的方法定义为无参数(parameterless)方法, 即省略空括号. 另一方面,您永远不应该定义一个没有括号的但有副作用的方法, 因为那个方法的调用看起来像一个字段选择.

10.4 EXTENDING CLASSES

class ArrayElement(conts: Array[String]) extends Element {
  def contents: Array[String] = conts
}

如果省略 extends 子句, Scala 编译器会隐式假设您的类继承自 scala.AnyRef, 它在 Java 平台上与类 java.lang.Object 相同.

10.5 OVERRIDING METHODS AND FIELDS

在 Scala 中, 字段和方法属于同一名称空间. 这使得字段可以覆盖无参数方法. 另一方面, 在 Scala 中, 禁止在同一个类中定义具有相同名称的字段和方法, 而在 Java 中允许这样做.

// This is Java
class CompilesFine {
  private int f = 0;

  public int f() {
    return 1;
  }
}
class WontCompile {
  private var f = 0 // Won't compile, because a field
  def f = 1 // and method have the same name
}

通常, Scala 只有两个用于定义的命名空间来, 而 Java 有四个. Java 的四个名称空间是字段, 方法, 类型和包. 相比之下, Scala 的两个名称空间是:

  • 值(字段, 方法, 包和单例对象)
  • 类型(class 和 trait 名称)

正是为了让您可以使用 val 重载无参数方法, Scala 将字段和方法放入同一名称空间, 这是 Java 无法做到的.

10.6 DEFINING PARAMETRIC FIELDS

class ArrayElement(conts: Array[String]) extends Element {
  def contents: Array[String] = conts
}
class ArrayElement(val contents: Array[String]) extends Element

请注意, 现在 contents 参数以 val 为前缀. 这是一种速记, 它同时定义了具有相同名称的参数和字段. 具体来说, 类 ArrayElement 现在具有(不可重新分配的)字段内容, 可以从类外部访问. 该字段使用参数值初始化.

您还可以使用 var 为 class 参数添加前缀, 在这种情况下, 相应的字段可以重新分配.

class Cat {
  val dangerous = false
}

class Tiger(
  override val dangerous: Boolean,
  private var age: Int
) extends Cat

10.7 INVOKING SUPERCLASS CONSTRUCTORS

10.8 USING OVERRIDE MODIFIERS

Scala 要求覆盖父类中具体成员的所有成员使用此类修饰符(override). 如果类成员实现具有相同名称的抽象类成员, 则修饰符 override 是可选的. 如果类成员没有覆盖或实现基类中的某个类成员, 则禁止使用 override 修饰符.

10.9 POLYMORPHISM AND DYNAMIC BINDING

polymorphism 多态

class UniformElement(
  ch: Char,
  override val width: Int,
  override val height: Int
) extends Element {
  private val line = ch.toString * width
  def contents = Array.fill(height)(line)
}
val e1: Element = new ArrayElement(Array("hello", "world"))
val ae: ArrayElement = new LineElement("hello")
val e2: Element = ae
val e3: Element = new UniformElement('x', 2, 3)

如果检查继承层次结构, 您会发现在上面代码中四个 val 定义语句中, 等号右侧的表达式类型在继承树中低于等号左侧初始化的 val 的类型.(父类可以转化为子类).

然而, 故事的另一半是基于变量或表达式的方法调用是动态绑定(dynamically bound)的. 这意味着调用的实际方法是在运行时根据对象的类确定的, 而不是根据变量或表达式的类型. 为了演示这种行为, 我们将暂时从 Element 类中删除所有现有成员, 并将一个名为 demo 的方法添加到 Element. 我们将在 ArrayElement 和 LineElement 类中覆盖 demo 方法, 但不会覆盖 UniformElement 类:

abstract class Element {
  def demo() = {
    println("Element's implementation invoked")
  }
}

class ArrayElement extends Element {
  override def demo() = {
    println("ArrayElement's implementation invoked")
  }
}

class LineElement extends ArrayElement {
  override def demo() = {
    println("LineElement's implementation invoked")
  }
}

// UniformElement inherits Element's demo
class UniformElement extends Element
def invokeDemo(e: Element) = {
  e.demo()
}

scala> invokeDemo(new ArrayElement) ArrayElement's implementation invoked

scala> invokeDemo(new LineElement) LineElement's implementation invoked

scala> invokeDemo(new UniformElement) Element's implementation invoked

10.10 DECLARING FINAL MEMBERS

final 确保子类不能覆盖成员函数 demo.

class ArrayElement extends Element {
  final override def demo() = {
    println("ArrayElement's implementation invoked")
  }
}

final 确保 ArrayElement 类不能被继承.

final class ArrayElement extends Element {
  override def demo() = {
    println("ArrayElement's implementation invoked")
  }
}

10.11 USING COMPOSITION AND INHERITANCE

组合(Composition)和继承(inheritance)是根据一个现有类定义新类的两种方法. 如果你所追求的主要是代码重用, 你通常应该更喜欢组合而不是继承. 只有继承会受到脆弱基类问题的影响, 这个问题是指更改超类会无意中破坏子类.

你可以问自己一个关于继承关系的问题 - 它是否模拟了一个 is-a 关系.例如, 可以合理地说 ArrayElement 是一个 Element. 另一个问题是客户端是否希望将子类类型视为超类类型. 对于 ArrayElement, 我们确实希望客户端希望将 ArrayElement 当成 Element 来使用.

实际上, 我们将 LineElement 定义为 ArrayElement 的子类主要是为了重用 ArrayElement 的内容定义. 因此, 将 LineElement 定义为 Element 的直接子类可能会更好, 如下所示:

改写成组合关系如下:

class LineElement(s: String) extends Element {
  val contents = Array(s)
  override def width = s.length
  override def height = 1
}

10.12 IMPLEMENTING ABOVE, BESIDE, AND TOSTRING

abstract class Element {
  def contents: Array[String]

  def width: Int =
    if (height == 0) 0 else contents(0).length

  def height: Int = contents.length

  def above(that: Element): Element =
    new ArrayElement(this.contents ++ that.contents)

  def beside(that: Element): Element =
    new ArrayElement(
      for (
        (line1, line2) <- this.contents zip that.contents
    ) yield line1 + line2
  )

  override def toString = contents mkString "\n"
}

10.13 DEFINING A FACTORY OBJECT

您可以选择通过工厂对象隐藏类的层次结构.

工厂对象包含构造其他对象的方法. 然后, 客户将使用这些工厂方法来构造对象, 而不是直接使用 new 来构造对象. 这种方法的一个优点是可以集中创建对象, 并且可以隐藏对象如何表示的细节. 这种隐藏将使您的库更易于被客户理解, 因为暴露的细节较, 并为您提供在不破坏客户端代码的情况下更改库的实现的机会.

构建布局元素工厂的第一个任务是选择工厂方法的位置. 它们应该是单例对象的成员还是类的成员? 一个简单的解决方案是创建类 Element 的伴随对象, 并使其成为布局元素的工厂对象. 这样, 您只需要向客户公开 Element 的类/对象组合, 同时可以隐藏三个实现类 ArrayElement, LineElement 和 UniformElement.

下面的代码是遵循此方案的 Element 对象的设计. Element 对象包含 elem 方法的三个重载变体. 每个都构造一种不同类型的布局对象.

object Element {
  def elem(contents: Array[String]): Element =
    new ArrayElement(contents)

  def elem(chr: Char, width: Int, height: Int): Element =
    new UniformElement(chr, width, height)

  def elem(line: String): Element =
    new LineElement(line)
}

随着这些工厂方法的出现, 更改类 Element 的实现以使其通过 elem 工厂方法而不是通过 new ArrayElement 显式创建实例是有意义的. 我们没有在类 Element 中直接使用 Element.elem 调用工厂方法, 而是导入 Element.elem, 然后通过简单名称 elem 调用工厂方法.

import Element.elem

abstract class Element {

  def contents: Array[String]

  def width: Int =
    if (height == 0) 0 else contents(0).length

  def height: Int = contents.length

  def above(that: Element): Element =
    elem(this.contents ++ that.contents)

  def beside(that: Element): Element =
    elem(
      for (
        (line1, line2) <- this.contents zip that.contents
    ) yield line1 + line2
  )

  override def toString = contents mkString "\n"
}

此外, 给定工厂方法, 子类, ArrayElement, LineElement 和 UniformElement 现在可以是私有的, 因为它们不再需要由客户端直接访问. 在 Scala 中, 您可以在其他类和单例对象内部定义类和单例对象. 使 Element 子类私有的一种方法是将它们放在 Element 单例对象中声明并指定它们是私有的.

object Element {

  private class ArrayElement(
    val contents: Array[String]
  ) extends Element

  private class LineElement(s: String) extends Element {
    val contents = Array(s)
    override def width = s.length
    override def height = 1
  }

  private class UniformElement(
    ch: Char,
    override val width: Int,
    override val height: Int
  ) extends Element {
    private val line = ch.toString * width
    def contents = Array.fill(height)(line)
  }

  def elem(contents: Array[String]): Element =
    new ArrayElement(contents)

  def elem(chr: Char, width: Int, height: Int): Element =
    new UniformElement(chr, width, height)

  def elem(line: String): Element =
    new LineElement(line)
}

注: 将 Element 的子类都定义在 Element 类内部的原因可能还是为了可见性, 客户代码只能通过工厂方法创建对象.

10.14 HEIGHTEN AND WIDEN

10.15 PUTTING IT ALL TOGETHER

10.16 CONCLUSION

Chapter 11 Scala's Hierarchy

11.1 SCALA'S CLASS HIERARCHY

scala 的根类 Any 中定义的方法如下:

  final def ==(that: Any): Boolean
  final def !=(that: Any): Boolean
  def equals(that: Any): Boolean
  def ##: Int
  def hashCode: Int
  def toString: String

== 方法与 equals 基本相同, 而且 != 始终是 equals 的否定. 因此, 各个类可以通过重写 equals 方法来定制 == 或 != 的意思.

Any 类有两个子类 AnyVal 和 AnyRef. AnyVal 是 Scala 中所有值类(value class)的父类. 虽然您可以定义自己的值类, 但 Scala 中内置了九个值类: Byte, Short, Char, Int, Long, Float, Double, Boolean 和 Unit. 前八个对应于 Java 中同名原始类型, 它们的值在运行时表示为 Java 的原始值.

Unit 大致对应于 Java 的 void 类型; Unit 有一个实例值, 写作().

请注意, 值类空间是扁平的; 所有值类都是 scala.AnyVal 的子类型, 但它们不是彼此子类. 相反, 不同的值类类型之间存在隐式转换. 例如, 类 scala.Int 的实例会在需要时自动扩展(通过隐式转换)为类 scala.Long 的实例.

注: 隐式转换并非一个好的特性, 容易导致出现不符合预期的结果, scala 应该提供机制将其关闭, 就像 C++ 那样.

根类 Any 的另一个子类是 AnyRef 类. 这是 Scala 中所有引用类的基类. 如前所述, 在 Java 平台看来, AnyRef 实际上只是类 java.lang.Object 的别名. 因此, 用 Java 编写的类以及用 Scala 编写的类都继承自 AnyRef. 可以将 java.lang.Object 视为 AnyRef 在 Java 平台上的实现. 虽然您可以在 Java 平台上的 Scala 程序中交替使用 Object 和 AnyRef, 但推荐的样式是在任何地方使用 AnyRef.

11.2 HOW PRIMITIVES ARE IMPLEMENTED

如果需要将整数视为(Java)对象, Scala 就会使用 "backup" 类 java.lang.Integer. 例如, 在整数上调用 toString 方法或将整数赋给 Any 类型的变量时会发生这种情况. Int 类型的整数将会透明地转换为 "boxed integers" 类型 java.lang.Integer.

注: int 与 Int 类的区别.

// This is Java
boolean isEqual(Integer x, Integer y) {
  return x == y;
}

System.out.println(isEqual(421, 421)); // 因为 == 表示引用类型的引用相等, 而Integer是引用类型, 所以返回 false, 因为比较的是两个不同的 Int 实例

scala> def isEqual(x: Any, y: Any) = x == y isEqual: (x: Any, y: Any)Boolean

scala> isEqual(421, 421) res11: Boolean = true

Scala 中的相等操作 "==" 被设计为对于类型透明的. 对于值类型, 它是自然(数字或布尔)相等. 对于引用类型, 除了 Java 的 boxed 数字类型以外, "==" 被视为从 Object 继承的 equals 方法的别名. 该方法最初被定义为引用相等, 但被许多子类覆盖以实现它们的自然平等概念. 这也意味着在 Scala 中你永远不会陷入 Java 有关字符串比较的陷阱(Java 字符串比较用 equals).

scala> val x = "abcd".substring(2) x: String = cd

scala> val y = "abcd".substring(2) y: String = cd

scala> x == y res12: Boolean = true

但是, 在某些情况下, 您需要的是引用相等而不是用户定义的相等性. 例如, 在某些效率优先的场景下, 您希望复用缓存中已经创建的实例, 可以通过比较实例的引用是否与之前保留的引用相等来确认是否是需要的实例. 对于这类场景, 类 AnyRef 定义了一个额外的 eq 方法, 该方法不能被覆盖并实现为比较引用是否相等(即它在 Java 中的行为类似于引用类型). 还有一个方法是对 eq 的否定, 称为 ne. 例如:

scala> val x = new String("abc") x: String = abc

scala> val y = new String("abc") y: String = abc

scala> x == y res13: Boolean = true

scala> x eq y res14: Boolean = false

scala> x ne y res15: Boolean = true

11.3 BOTTOM TYPES

scala.Null 和 scala.Nothing

Null 类是空引用的类型; 它是每个引用类的子类(每个引用类都继承自 AnyRef). Null 与值类型不兼容. 例如, 您不能将空值分配给整数变量:

scala> val i: Int = null :7: error: an expression of type Null is ineligible for implicit conversion val i: Int = null

Nothing 类是 Scala 类层次结构的最底层; 它是所有其他类型的子类型. 但是, 不存在任何此类型的值. 为什么没有值的类型有意义? 如第 7.4 节所述, Nothing 的一个用途是它表示异常终止.

例如, Scala 标准库的 Predef 对象中有 error 方法, 其定义如下:

def error(message: String): Nothing =
  throw new RuntimeException(message)

返回类型的错误是 Nothing, 它告诉调用者该方法将不会正常返回(它会抛出异常). 因为 Nothing 是所有其他类型的子类型, 所以您可以非常灵活的方式使用错误等方法. 例如:

def divide(x: Int, y: Int): Int =
  if (y != 0) x/y
  else error("can't divide by zero")

条件分支的返回值 x/y 是 Int 类型, 而 else 分支(调用 error 函数)返回的是 Nothing 类型. 因为 Nothing 是 Int 的子类型, 所以 divide 函数的返回值必然是 Int 类型的.

11.4 DEFINING YOUR OWN VALUE CLASSES

如第 11.1 节所述, 您可以定义自己的值类来扩充内置的值. 与 scala 自带的值类一样, 值类的实例通常会编译成不使用包装类的 Java 字节码. 在需要包装器的上下文中, 例如使用泛型的代码, 该值将自动装箱并拆箱.

只有某些类可以组成值类. 要使一个类成为值类, 它必须只有一个参数, 除了 defs 之外它必须没有任何内容. 此外, 其他类不能继承值类, 并且值类不能重新定义 equals 或 hashCode 方法.

要定义一个值类, 先让其成为 AnyVal 的子类, 并且将 val 放在唯一的一个参数之前. 这是一个值类的示例:

class Dollars(val amount: Int) extends AnyVal {
  override def toString() = "$" + amount
}

如第 10.6 节所述, val 前缀允许将 amount 参数作为字段进行访问. 例如, 以下代码创建值类的实例, 然后从中检索数量:

scala> val money = new Dollars(1000000) money: Dollars = $1000000 scala> money.amount res16: Int = 1000000

在此示例中, money 指的是值类的实例. 它是 Scala 源代码中的 Dollars 类型, 但编译的 Java 字节码将直接使用 Int 类型.

Avoiding a types monoculture

为了从 Scala 类层次结构中获得最大收益, 尝试为每个域概念(领域建模)定义一个新类, 即使可以重用相同的类来达成不同目的. 即使这样的类是没有方法或字段的所谓微类型, 多定义一些类也可以使编译器对您有所帮助.

例如, 假设您正在编写一些代码来生成 HTML. 在 HTML 中, 样式名称表示为字符串. 锚标识符也是如此. HTML 本身也是一个字符串, 所以如果你愿意, 你可以定义帮助函数来表示所有这些字符串, 如下所示:

def title(text: String, anchor: String, style: String): String =
  s"<a id='$anchor'><h1 class='$style'>$text</h1></a>"

类型签名(参数和返回值)有四个字符串! 这种全都是字符串类型的代码从技术上来看是强类型的, 但由于所看到的一切都是 String 类型, 编译器无法感知到您的实际意图. 例如, 它不会阻止文本和锚点参数的误用:

scala> title("chap:vcls", "bold", "Value Classes") res17: String =

chap:vcls

这个 HTML 是错位的. 预期的显示文本 "Value Classes" 被用作样式类, 正在显示的文本是"chap.vcls", 它应该是一个锚点. 最重要的是, 实际的锚标识符是 "粗体", 它应该是一个样式类. 尽管有这种错误非常明显, 但是编译器是无法感知的.

如果为每个域概念定义一个小类型, 编译器会更有帮助. 例如, 您可以为样式, 锚标识符, 显示文本和 HTML 定义一个小类. 由于这些类有一个参数而没有成员, 因此可以将它们定义为值类:

class Anchor(val value: String) extends AnyVal
class Style(val value: String) extends AnyVal
class Text(val value: String) extends AnyVal
class Html(val value: String) extends AnyVal

通过给定这些类, 可以编写具有更明确类型签名的 title 函数:

def title(text: Text, anchor: Anchor, style: Style): Html =
  new Html(
    s"<a id='${anchor.value}'>" +
      s"<h1 class='${style.value}'>" +
      text.value +
      "</h1></a>"
)

如果您在调用该函数时参数顺序错误, 编译器现在可以检测到错误. 例如:

scala> title(new Anchor("chap:vcls"), new Style("bold"), new Text("Value Classes")) :18: error: type mismatch; found : Anchor required: Text new Anchor("chap:vcls"), ^ :19: error: type mismatch; found : Style required: Anchor new Style("bold"), ^ :20: error: type mismatch; found : Text required: Style new Text("Value Classes")) ^

注: 说了半天就是用强类型来解决问题.

11.5 CONCLUSION

Chapter 12 Traits

Traits 是 Scala 中代码重用的基本单元. 特征封装了方法和字段定义, 然后可以通过将它们混合到类中来重用它们. 与类只能从一个超类继承不同, 类可以混合任意数量的特征. 本章向您展示了traits的工作原理,并展示了两种最常用的方法:将瘦接口扩展为富接口,以及定义可叠加的修改。 它还显示了如何使用排序 trait, 并将 traits 与其他语言的多重继承特性进行比较.

12.1 HOW TRAITS WORK

trait Philosophical {
  def philosophize() = {
    println("I consume memory, therefore I am!")
  }
}

trait 默认父类为 AnyRef. "extends" 和 "with" 关键字用来将 trait 混入类中.

class Frog extends Philosophical {
  override def toString = "green"
}

scala> val frog = new Frog frog: Frog = green

scala> frog.philosophize() I consume memory, therefore I am!

trait 也定义了一个类型:

scala> val phil: Philosophical = frog phil: Philosophical = green

scala> phil.philosophize() I consume memory, therefore I am!

phil 的类型是 Philosophical, 一个特征. 因此, 变量 phil 可以用任何混合了该接口的类的实例来初始化.

如果想同时继承父类并混合 trait, 请参考下面的代码:

class Animal
trait HasLegs

class Frog extends Animal with Philosophical with HasLegs {
  override def toString = "green"
}
class Animal

class Frog extends Animal with Philosophical {
  override def toString = "green"
  override def philosophize() = {
    println("It ain't easy being " + toString + "!")
  }
}

trait 的定义与类的定义有两个不同:

  • trait 定义时不能带参数.
  • 在类中, 父类函数的调用是静态绑定的, 而在 trait 中, 它们是动态绑定的. 如果在类中编写 "super.toString", 则确切地知道将调用哪个方法实现. 但是, 当您在 trait 中编写相同的内容时, 在定义 trait 时, 并不知道父类调用的具体实现. 相反, 每次将特征混合到具体类中时, 才能确定要调用的实现. 这种奇怪的父类调用机制是允许 traits 支持可堆叠修改的关键, 细节将在 12.5 节中描述. 解决父类调用的规则将在第 12.6 节中给出.

12.2 THIN VERSUS RICH INTERFACES

这里是说 trait 可以定义抽象方法, 也可以定义方法实现, 所以比 java 的 interface 更易用, 不用像 java 一样 -- 每个继承了接口的类都重新实现一次接口中的方法(貌似现在 java 的 interface 也支持具体方法实现了).

12.3 EXAMPLE: RECTANGULAR OBJECTS

abstract class Component {
  def topLeft: Point
  def bottomRight: Point
  
  def left = topLeft.x
  def right = bottomRight.x
  def width = right - left
  // and many more geometric methods...
}
trait Rectangular {
  def topLeft: Point
  def bottomRight: Point
  def left = topLeft.x
  def right = bottomRight.x
  def width = right - left
  // and many more geometric methods...
}

12.4 THE ORDERED TRAIT

class Rational(n: Int, d: Int) extends Ordered[Rational] {
  // ...
  def compare(that: Rational) =
    (this.numer * that.denom) - (that.numer * this.denom)
}

上面代码中的 Ordered trait 在混入类时需要指定一个类型参数(Type parameters, 19 章中讨论).

类型擦除 泛型是 Java 1.5 版本才引进的概念, 在这之前是没有泛型的概念的, 为了让泛型代码能够和之前版本的代码兼容. 泛型信息只存在于代码编译阶段, 在 JVM 执行阶段, 与泛型相关的信息会被擦除掉, 专业术语叫做类型擦除. 泛型类被类型擦除的时候, 之前泛型类中的类型参数部分如果没有指定上限, 如 则会被转译成普通的 Object 类型, 如果指定了上限如 则类型参数就被替换成类型上限. 利用类型擦除的原理,用反射 的手段就绕过了正常开发中编译器不允许的操作限制.

trait Ordered[T] {
  def compare(that: T): Int
  def <(that: T): Boolean = (this compare that) < 0
  def >(that: T): Boolean = (this compare that) > 0
  def <=(that: T): Boolean = (this compare that) <= 0
  def >=(that: T): Boolean = (this compare that) >= 0
}

请注意, Ordered trait 没有定义 equals 方法, 因为它无法执行此操作. 问题是实现 equals 需要预先检查传递对象的类型, 由于类型擦除, Ordered 本身不能进行此测试. 因此, 即使您继承了 Ordered, 也需要自己定义 equals 方法. 你将在第 30 章了解如何解决这个问题.

12.5 TRAITS AS STACKABLE MODIFICATIONS

Traits 允许您修改类的方法, 并且它们允许您相互堆叠这些修改.

abstract class IntQueue {
  def get(): Int
  def put(x: Int)
}
import scala.collection.mutable.ArrayBuffer

class BasicIntQueue extends IntQueue {
  private val buf = new ArrayBuffer[Int]   // Int 类型的 ArrayBuffer
  def get() = buf.remove(0)
  def put(x: Int) = { buf += x }
}
trait Doubling extends IntQueue {
  abstract override def put(x: Int) = { super.put(2 * x) }
}

上面的 extends 声明意味着这个 trait 只能混合到 IntQueue 的子类中. 另外注意上面的 super 是动态绑定的. 为了实现支持堆叠修改的 trait, 经常需要这种安排. 要告诉编译器您是故意这样做的, 您必须将这些方法标记为 abstract override. 这种修饰符的组合仅允许 trait 成员而不是类成员, 并且这意味着 trait 混入的类必须具有该方法的具体定义(这样才能覆盖).

trait Incrementing extends IntQueue {
  abstract override def put(x: Int) = { super.put(x + 1) }
}
trait Filtering extends IntQueue {
  abstract override def put(x: Int) = {
    if (x >= 0) super.put(x)
  }
}

scala> val queue = (new BasicIntQueue with Incrementing with Filtering) queue: BasicIntQueue with Incrementing with Filtering...

scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get() res16: Int = 1

scala> queue.get() res17: Int = 2

mixin 的顺序很重要. 准确的规则将在下一节中给出, 但简单来讲, 最右边的 trait 优先生效. 调用类的方法时, 首先调用最右边的 trait 中的方法. 如果该方法调用了 super, 它将调用左侧 trait 中的方法, 依此类推. 在前面的示例中, 首先调用 Filtering 的 put, 因此它会删除负整数. 第二次调用 Incrementing 的 put, 它会为剩余的整数加一.

scala> val queue = (new BasicIntQueue with Filtering with Incrementing) queue: BasicIntQueue with Filtering with Incrementing...

scala> queue.put(-1); queue.put(0); queue.put(1)

scala> queue.get() res19: Int = 0

scala> queue.get() res20: Int = 1

scala> queue.get() res21: Int = 2

12.6 WHY NOT MULTIPLE INHERITANCE?

trait 是继承多个类似类的构造的一种方式, 但它们与许多语言中存在的多重继承不同. 一个区别特别重要: super 的解释. 通过多重继承, 可以通过父调用中的函数名确认调用的是哪个父类的方法. 对于 trait, 调用的方法由类和混合到类中的 trait 来线性化(linearization)确定.

线性化的意思是当您使用 new 实例化一个类时, Scala 会获取该类及其父类和父类混入的 trait, 并将它们放在一个线性顺序中. 然后, 每当你在其中一个类中调用 super 时, 调用的方法就是链中的下一个方法. 在任何线性化中, 类总是在其所有超类及混入的特征前面线性化. 因此, 当您编写一个调用 super 的方法时, 该方法肯定会修改超类及混入特征的行为.

12.7 TO TRAIT OR NOT TO TRAIT?

每当您实现可重用的行为集合时, 您将必须决定是使用 trait 还是抽象类. 没有确定的规则, 但本节包含一些需要考虑的指导原则.

  • 如果不再重用该行为, 那么将其作为具体类. 毕竟这不是可重用的行为.
  • 如果它可以在多个不相关的类中重用, 那么将其作为 trait. 只有 trait 可以任意混入到类层次结构的不同部分.
  • 如果要在 Java 代码中继承它, 请使用抽象类. 由于具有方法具体实现的 trait 找不到类似的 Java 机制来模拟, 因此用 Java 类继承 trait 往往很尴尬. 同时, 继承 Scala 类就像从 Java 类继承一样. 作为一个例外, 只有抽象成员的 Scala 特征可以直接转换为 Java 接口, 因此如果您希望 Java 代码从 trait 中继承, 您应该这样定义 trait. 有关混合应用 Java 和 Scala 的更多信息, 请参见 31 章.
  • 如果您计划以编译形式分发它, 并且您希望外部组编写从其继承的类, 您可能倾向于使用抽象类. 问题是当 trait 获得或失去一个成员时, 任何从它继承的类都必须重新编译, 即使它们没有改变. 如果外部客户端只调用行为, 而不是继承它, 那么使用 trait 就可以了.
  • 如果您在排除上述情况后仍然不知道, 那么优先将其作为 trait. 您可以随时更改它, 通常使用 trait 会更灵活.

12.8 CONCLUSION

Chapter 13 Packages and Imports

在编写大型程序时, 重要的是最小化耦合(程序的各个部分依赖于其他部分的程度). 低耦合降低了程序的一部分中看似无害的小变化将在另一部分中产生破坏性后果的风险. 最小化耦合的一种方法是以模块化方式编写. 您将程序划分为多个较小的模块, 每个模块都有内部和外围. 在模块内部工作时, 您只需要与在同一个模块上工作的其他程序员协调. 只有当你必须改变模块的外部, 才有必要与在其他模块上工作的开发人员进行协调.

本章介绍了几种可帮助您以模块化方式进行编程的结构. 它显示了如何将东西放入包中, 通过导入使名称可见, 并通过访问修饰符控制定义的可见性.

13.1 PUTTING CODE IN PACKAGES

13.2 CONCISE ACCESS TO RELATED CODE

13.3 IMPORTS

13.4 IMPLICIT IMPORTS

13.5 ACCESS MODIFIERS

13.6 PACKAGE OBJECTS

13.7 CONCLUSION

Chapter 14 Assertions and Tests

14.1 ASSERTIONS

14.2 TESTING IN SCALA

14.3 INFORMATIVE FAILURE REPORTS

14.4 TESTS AS SPECIFICATIONS

14.5 PROPERTY-BASED TESTING

14.6 ORGANIZING AND RUNNING TESTS

14.7 CONCLUSION

Chapter 15 Case Classes and Pattern Matching

15.1 A SIMPLE EXAMPLE

15.2 KINDS OF PATTERNS

15.3 PATTERN GUARDS

15.4 PATTERN OVERLAPS

15.5 SEALED CLASSES

15.6 THE OPTION TYPE

15.7 PATTERNS EVERYWHERE

15.8 A LARGER EXAMPLE

15.9 CONCLUSION

Chapter 16 Working with Lists

16.1 LIST LITERALS

16.2 THE LIST TYPE

16.3 CONSTRUCTING LISTS

16.4 BASIC OPERATIONS ON LISTS

16.5 LIST PATTERNS

16.6 FIRST-ORDER METHODS ON CLASS LIST

16.7 HIGHER-ORDER METHODS ON CLASS LIST

16.8 METHODS OF THE LIST OBJECT

16.9 PROCESSING MULTIPLE LISTS TOGETHER

16.10 UNDERSTANDING SCALA'S TYPE INFERENCE ALGORITHM

16.11 CONCLUSION

Chapter 17 Working with Other Collections

17.1 SEQUENCES

17.2 SETS AND MAPS

17.3 SELECTING MUTABLE VERSUS IMMUTABLE COLLECTIONS

17.4 INITIALIZING COLLECTIONS

17.5 TUPLES

17.6 CONCLUSION

Chapter 18 Mutable Objects

18.1 WHAT MAKES AN OBJECT MUTABLE?

18.2 REASSIGNABLE VARIABLES AND PROPERTIES

18.3 CASE STUDY: DISCRETE EVENT SIMULATION

18.4 A LANGUAGE FOR DIGITAL CIRCUITS

18.5 THE SIMULATION API

18.6 CIRCUIT SIMULATION

18.7 CONCLUSION

Chapter 19 Type Parameterization

19.1 FUNCTIONAL QUEUES

19.2 INFORMATION HIDING

19.3 VARIANCE ANNOTATIONS

19.4 CHECKING VARIANCE ANNOTATIONS

19.5 LOWER BOUNDS

19.6 CONTRAVARIANCE

19.7 OBJECT PRIVATE DATA

19.8 UPPER BOUNDS

19.9 CONCLUSION

Chapter 20 Abstract Members

20.1 A QUICK TOUR OF ABSTRACT MEMBERS

20.2 TYPE MEMBERS

20.3 ABSTRACT VALS

20.4 ABSTRACT VARS

20.5 INITIALIZING ABSTRACT VALS

20.6 ABSTRACT TYPES

20.7 PATH-DEPENDENT TYPES

20.8 REFINEMENT TYPES

20.9 ENUMERATIONS

20.10 CASE STUDY: CURRENCIES

20.11 CONCLUSION

Chapter 21 Implicit Conversions and Parameters

21.1 IMPLICIT CONVERSIONS

21.2 RULES FOR IMPLICITS

21.3 IMPLICIT CONVERSION TO AN EXPECTED TYPE

21.4 CONVERTING THE RECEIVER

21.5 IMPLICIT PARAMETERS

21.6 CONTEXT BOUNDS

21.7 WHEN MULTIPLE CONVERSIONS APPLY

21.8 DEBUGGING IMPLICITS

21.9 CONCLUSION

Chapter 22 Implementing Lists

22.1 THE LIST CLASS IN PRINCIPLE

22.2 THE LISTBUFFER CLASS

22.3 THE LIST CLASS IN PRACTICE

22.4 FUNCTIONAL ON THE OUTSIDE

22.5 CONCLUSION

Chapter 23 For Expressions Revisited

23.1 FOR EXPRESSIONS

23.2 THE N-QUEENS PROBLEM

23.3 QUERYING WITH FOR EXPRESSIONS

23.4 TRANSLATION OF FOR EXPRESSIONS

23.5 GOING THE OTHER WAY

23.6 GENERALIZING FOR

23.7 CONCLUSION

Chapter 24 Collections in Depth

Chapter 25 The Architecture of Scala Collections

Chapter 26 Extractors

Chapter 27 Annotations

Chapter 28 Working with XML

Chapter 29 Modular Programming Using Objects

Chapter 30 Object Equality

Chapter 31 Combining Scala and Java

Chapter 32 Futures and Concurrency

Chapter 33 Combinator Parsing

Chapter 34 GUI Programming

Chapter 35 The SCells Spreadsheet