《Scala 程序设计》学习笔记 Chapter 2:更简洁 更
分号
- 在 scala REPL 中,使用
:paste
模式输入多行代码,然后用 Ctrl-D 结束。[P29]
变量声明
-
val
和var
关键字只标识引用本身是否可以指向另一个不同的对象,它们并未表明其所引用的对象是否可变。[P30] - 为了减少可变性引起的 bug ,应当*尽可能地使用不可变变量。[p30]
Range [P31]
-
1 to 10
: 1, 2, .., 10 -
1 until 10
: 1, 2, 3, ..., 9 -
1 to 10 by 3
: 1, 4, 7, 10 -
1 to 10 by -3
: 10, 7, 4, 1 1L to 10L by 3L
1f to 10.3f by 0.3f
'a' to 'g' by 3
偏函数
-
”偏“:偏函数不处理所有可能的输入,只处理能与至少一个
case
语句匹配的输入。[P32] -
在偏函数中只能用
case
语句,而整个函数必须用花括号包围。[P32] -
如果偏函数被调用,而函数的输入却与所有语句都不匹配,系统会抛出一个
MatchError
运行时错误。[P32] -
使用
isDefinedAt
方法测试特定输入是否与偏函数匹配:f.isDefinedAt(x)
返回true / false
。[P32] -
可以使用
orElse
语法连接偏函数:[P32]val pf1 = PartialFunction[Any, String] = {case s:String => "YES"} val pf2 = PartialFunction[Any, String] = {case d:Double => "YES"} val pf = pf1 orElse pf2
方法声明
-
copy
方法也是 case 类自动创建的。它允许你在创建 case 类的新实例时,只给出与原始对象不同部分的参数。[P33]
方法具有多个参数列表
-
Scala 允许我们把参数列表两边的圆括号替换为花括号。[P34]
def draw(offset: Point = Point(0.0, 0.0))(f: String => Unit): Unit = f(s"draw(offset = $offset), ${this.toString}")
s.draw(Point(1.0, 2.0))(str => println(s"ShapesDrawingActor: $str")) // 等价于 s.draw(Point(1.0, 2.0)){ str => println(s"ShapesDrawingActor: $str")) }
-
使用具有多个参数列表的方法有助于 Scala 进行参数推断。[�P35]
-
使用具有多个参数列表的方法中,可以使用最后一个参数列表推断隐含参数。�[P35]
-
隐含参数是用
implicit
关键字声明的参数,当相应方法被调用时,我们可以显式指定这个参数,或者不指定,让编译器在当前作用域找到一个合适的值作为参数。[P35] -
隐含参数可以代替参数默认值,而且更加灵活。[P35]
Future 简介
-
scala.concurrent.Future
� 是 Scala 提供的一个并发工具,其 API 使用隐含参数减少代码冗余。[P35] -
Future 与隐含参数[P36]
apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]
import scala.concurrent.ExecutionContext.Implicits.global val future = { ... }
-
只有用
implicit
关键字声明,在当前作用域可见的对象才能用作隐含值;只有被声明为implicit
的函数参数才允许调用时不给出实参,而采用隐含的值。��[P37]
嵌套方法的定义与递归
-
内部变量会屏蔽外部相同名称的变量。[P38]
-
Scala 采用局部作用域类型推断,所以必须为递归方法显式声明返回值类型。[P39]
-
尾递归:调用递归函数是该函数的最后一个表达式,该表达式的返回值就是所调用的递归函数的返回值。[P39]
-
尾递归检查
@tailrec
[P39]import scala.annotation.tailrec def factorial(i: Int): Long = { @tailrec def fact(i: Int): Long = { if (i <= 1) accumulator else fact(i - 1, i * accumulator) } fact(i, 1) }
- 如果函数不是尾递归,编译器会报错。
-
外层方法中的一切在嵌套方法中都是可见的,包括传递给外层方法的参数。
推断类型信息
-
什么时候需要显式类型注解:[P41]
- 声明了可变的
var
变量或不可变的val
变量,没有进行初始化。 - 所有的方法参数(如
def deposit(amount: Money) = {...}
)。 - 方法的返回值类型,在以下情况中必须显式声明其类型。
- 在方法中明显地使用了
return
(即使在方法末尾也是如此)。 - 递归方法。
- 两个或多个方法重载(拥有相同的函数名),其中一个方法调用了另一个重载方法,调用者需要显式类型注解。
- Scala 推断出的类型比你期望的类型更为宽泛,如
Any
。(少见)
- 在方法中明显地使用了
- 声明了可变的
-
Scala 支持可变方法,但是:[P42]
- 方法可以用有其他参数,但必须位于可变参数之前;
- 方法只能有一个可变参数。
-
加入返回类型注解可以在编译阶段发现推断类型错误,也有助于提升代码可读性。[P43]
-
Scala 将方法体前的声明和等号当做函数定义,而在函数式编程中,函数总要有返回值。当 Scala 发现函数体之前没有等号时,就认定程序员希望该方法是一个 procedure ,意味着它只返回
Unit
。[P44] -
Unit
类型拥有一个名为()
的值。[P44]
保留字 [P44 - 45]
-
forSome
:用在已经存在的类型声明中,限制其能够使用的具体类型。 -
implicit
:使得方法或变量值可以被�������用于隐含转换;将方法参数标记为可选的,只要在调用该方法时,作用域内有类型匹配的候选对象,就会使用该对象作为参数。 -
lazy
:推迟val
变量的赋值。 -
requires
:[已停用] -
sealed
:用于父类型,要求所有派生的子类必须在同一个源文件中声明。 -
trait
:这是一个混入模块,对类的实例添加额外的状态和行为;也可以用于声明而不实现方法,类似 Java 的interface
。 -
type
:声明类型。 -
yield
:在for
循环中返回元素,这些元素会构成一个序列。 -
还有一些字符,具体看文档。
-
Scala 没有
break
和continue
。 -
在 Scala 中引用保留的 Java 方法时,要为其加上
``
。java.util.Scanner.`match`
字面量
整数字面量
- 整数字面量如果超过规定的范围,会引发一个编译错误。[P46]
字符字面量
- 不可打印的 Unicode 字符(如:\u0009 水平制表符)在 Scala 中是不允许的。[P48]
字符串字面量
- 字符串字面量是被双引号或者三重引号包围的字符串序列,如
"""..."""
。[P48] - 用三重双引号包含的字符串字面量被称为多行字符串字面量,这些字符串可以跨越多行, 换行符是字符串的一部分。可以包含任意字符,但不能出现三个连续的双引号。三重双引号包含的字符串不转义。[P49]
- 在多行字符串中,可以使用
String.stripMargin
移除每行字符串开头的空格和第一个遇到的垂直分割符|
。 [P49]- 如果希望用别的字符替代
|
,可以使用stripMargin
的重载版本,该函数可以指定一个Char
参数替代|
。如果想要移除整个字符串(而不是字符串的各个行)的前缀和后缀,有相应的stripPrefix
和stripSuffix
方法可以完成。
- 如果希望用别的字符替代
符号字面量
- Scala 支持符号。符号是一些规定的字符串。两个同名符号会指向内存中的同一对象。[P50]
- 符号是单引号( ' )后边跟上一个或多个数字、字幕或下划线,但第一个字符不能是数字。[P50]
- 符号字面量
'id
是表达式scala.Symbol("id")
的简写形式,如果要创建一个包含空格的符号,可以使用Symbol.apply
,比如Symbol(" programming Scala")
。[P50]
函数字面量
-
(i: Int, s: String) => s + i
是一个类型为Function2[Int, String, String]
的函数字面量。以下声明等价:[P50]val f1: (Int, String) => String = (i, s) => s + i val f2: Function2[Int, String, String] = (i, s) => s + i
元组字面量
-
Scala 库中包含
TupleN
类,用于组建 N 元素组,它以小括号加上逗号分隔的元素序列的形式来创建元素组。TupleN
表示的多个类各自独立,N 的取值从 1 到 22 ,包括 22 。[P50] -
用字面量语法声明
Tuple
类型的变量:[P50]val t1: (Int, String) = (1, "two") val t2: Tuple2[Int, String] = (1, "two")
-
使用元组:[P51]
val t = ("Hello", 1, 2.3) println("Print the whole tuple: " + t) println("Print the first item: " + t._1) println("Print the second item: " + t._2) println("Print the third item: " + t._3)
-
表达式
t._n
提取元组 t 中的第 n 个元素。元组从 1 开始计数。[P51] -
一个量元素的元组,有时被简称为 pair ,有很多定义 pair 的方法,除了在圆括号中列出元素值以外,还可以”箭头操作符“放在两个值之间,也可以用相应类的工厂方法:[P51]
(1, "one") 1 -> "one Tuple2(1, "one")
箭头操作符只适用于两元素的元组。
Option
、 Some
和 None
:避免使用 null
-
抽象类
Option
具有两个两个具体的子类Some
和None
。Some
用于表示有值,None
用于表示没有值。[P52] -
举个例子:[P52]
val stateCapitals = Map( "Alabama" -> "Montgomery", "Alaska" -> "Juneau" ) stateCapitals.get("Alabama") // Some(Montgomery) stateCapitals.get("Alabama").get // "Montgomery" stateCapitals.get("Unknown") // None stateCapitals.get("Unknown").getOrElse("Oops") // "Oops"
Map.get
方法返回Option[T]
,本例子中T
为String
。
封闭类的继承
-
关键字
sealed
用于实现封闭类的继承。[P53]sealed abstract class Option[+A] ... { ... }
关键字
sealed
告诉编译器,所有的子类必须在同一个源文件中声明。 -
如果为了防止用户派生任何子类,也可以用
final
。
用文件和命名空间组织代码
-
Scala 支持嵌套
package
语法:[P54 - 55]package org { ... package scala { ... package demo { ... } } } package com { package example { ... } } package com1.example2 { ... }
-
使用连续包声明时,必须使用单独的
package
语句:[P55]package com.example // 导入 example 中所有包级别的声明 package mypkg // 导入 mypkg 中所有包级别的声明 class MyPkgClass { ... }
-
Scala 不允许在脚本中定义包,脚本被隐含包装在一个对象中。在对象中声明包是不允许的。[P55]
导入类型及其成员
-
在 Scala 中,使用
_
作为通配符。[P56] -
Scala 的
import
语句几乎可以放在任何位置上,所以你可以将其可见性限制在需要的作用域中。[P56] -
可以在导入时对类型做重命名以解决冲突问题:[P56]
import java.math.BigInteger.{ ONE => _, // 重命名为下划线,使该常量不可见。 TEN, ZERO => JAVAZERO }
导入是相对的
import collection.immutable._ // 由于 scala 已经默认导入,所以不需要给出全路径
import _root_.scala.collection.parallel._ // 从 ”根“ 开始的全路径
使用第二种导入方法时,要保证库的所在路径被包含在了 CLASSPATH 中。[P57]
包对象
-
Scala 支持包对象一种特殊类型的、作用域为包层次的对象。它像普通的对象一样声明,但与普通对象有着一些不同点:[P57]
- 文件名必须是 package.scala 。 - 1
- 标记上层包的作用域。 - 2
- 使用
package
关键字给包名之后的对象命名。 - 3 - 适合暴露给客户端的成员。 - 4
// src/com/example/json/package.scala // 1 package com.example // 2 package object json { // 3 class JSONObject { ... } // 4 def fromString(string: String): JSONObject = { ... } } // 客户端可以使用 import com.example.json._ 导入所有定义,或用通常方法单独导入元素。
抽象类型与参数化类型
-
Scala 的参数化类型与 Java 的泛型很像。举一个参数化类型的例子:[P58]
- 在 Scaladoc 中,
List
的声明为sealed abstract class List[+A]
。 -
A
之前的+
表示:如果B
是A
的子类型,则List[B]
也是List[A]
的子类型,这被称为协类型。 - 如果类型参数前有
-
,则表示另一种关系:如果B
是A
的子类型,且Foo[A]
被声明为Foo[-A]
,则Foo[B]
是Foo[A]
的父类型(称为逆类型)。
- 在 Scaladoc 中,
-
Scala 还支持一种被称为”抽象类型“ 的抽象机制,可以用在许多参数化类型中。[P58]
-
参数化类型和抽象类型都被声明为其他类型的成员,就像是该类型的方法与属性一样。[P58 - 59]
// 类型成员( type ) abstract class BulkReader { type In val source: In def read: String // 不同的 In ,read 的方式不同。 } class StringBulkReader extends BulkReader { type In = String }
// 参数化类型 abstract class BulkReader[In] { val source: In ... } class StringBulkReader extends BulkReader[String] = { ... }
-
类型成员比参数化类型的优势:[P59]
- 当类型参数与参数化类型无关时,参数化类型更适用。
- 举个例子:
List[A]
,A
可能是Int
、String
等。
- 举个例子:
- 当类型成员与所封装的类型同步变化时,类型成员更适用。有时这种特点被称为家族多态,或者协特化。
- 当类型参数与参数化类型无关时,参数化类型更适用。