3.1 数据结构 | 39 |
3.2 循环,条件,Comprehensions | 45 |
3.3 方法与函数 | 50 |
3.4 类和Trait | 53 |
for
(
i <-
Range.
inclusive(
1
,
100
)
)
{
println(
if
(
i %
3
==
0
&&
i %
5
==
0
)
"FizzBuzz"
else
if
(
i %
3
==
0
)
"Fizz"
else
if
(
i %
5
==
0
)
"Buzz"
else
i
)
}
</> 3.1.scala
Snippet 3.1: 用Scala实现的“FizzBuzz”编程挑战
本章快速浏览Scala语言。我们现在聚焦Scala基本知识,你可能发现这部分和其他主流编程语言是相似的。
本章的目标是让你熟悉Scala,能够像你已经熟悉的其他语言一样毫不费力地编写相同功能的代码。本章并不会涉及很多Scala编程风格和语言特点:这些会留待 Chapter 5: Scala特性 介绍。
本章我们将在Ammonite Scala REPL中编写代码:
$ amm
Loading...
Welcome to the Ammonite Repl 2.2.0 (Scala 2.13.2 Java 11.0.7)
@
</> 3.2.bash
Scala有下列原始类型:
|
|
这些类型和Java原始类型一致,和其它静态类型编程语言C#、C++等也相似。每种类型支持典型的操作,比如booleans支持布尔逻辑 ||
,numbers支持算术运算 &&
+
-
*
和位运算 /
|
等等。所有数据结构支持 &
判断相等,==
判断不等。!=
Numbers默认32位
。算术运算优先级同其它编程语言一样:Int
和 *
优先于 /
或 +
,括号里的表达式优先计算。-
@ 1
+
2
*
3
res0:
Int
=
7
@ (
1
+
2
)
*
3
res1:
Int
=
9
</> 3.3.scala
是有符号的,可能溢出。带 Int
L
后缀的是64位
,支持更大数值范围,不会轻易溢出:Long
|
|
java
和 .
lang.
Integerjava
除了支持基本的操作符,还有很多有用的方法:.
lang.
Long
@ java.
lang.
Integer.
<
tab>
BYTES decode numberOfTrailingZeros
signum MAX_VALUE divideUnsigned
getInteger parseUnsignedInt toBinaryString
.
.
.
@ java.
lang.
Integer.
toBinaryString(
123
)
res6:
String
=
"1111011"
@ java.
lang.
Integer.
numberOfTrailingZeros(
24
)
res7:
Int
=
3
</> 3.6.scala
是64位 1.0
,支持类似的算术运算。你可以用 Double
声明这是一个32位 1.0F
数字:Float
@ 1.0
/
3.0
res8:
Double
=
0.3333333333333333
@ 1.0F
/
3.0F
res9:
Float
=
0.33333334F
</> 3.7.scala
32位
类型只占64位 Float
类型一半存储,但是在算数运算中更容易发生四舍五入错误。在 Double
和 Float
上可以执行类似像 Double
java
和 .
lang.
Float
java
所支持的操作。.
lang.
Double
在Scala中是16位 String
数组:Char
@ "hello world"
res10:
String
=
"hello world"
</> 3.8.scala
用于对 .
substring
进行切片,String
用于拼接 +
。前缀 String
s"..."
可做字符串插值,在其中用 $
或者 $
来引用变量或表达式完成插值:{
.
.
.
}
|
|
你可以用
关键字命名局部变量:val
@ val
x =
1
@ x +
2
res18:
Int
=
3
</> 3.11.scala
变量的地址不可变:你不可以重新分配 val
来引用其它值。你只能使用 val
x
关键字重新分配局部变量。var
|
|
通常来讲,你应该尽可能使用
:你的程序中大多数有名变量不需要重新分配,使用 val
预防你意外地重分配它们。当你确信会重分配它们时,才使用 val
。var
和 val
都可以直接注解类型,这有利于阅读你的代码,而且当你意外地分配了不匹配的类型时,编译器能够捕获该错误。var
|
|
元组是固定长度的变量集合,这些变量可以是不同类型:
@ val
t =
(
1
,
true
,
"hello"
)
t:
(
Int
,
Boolean
,
String
)
=
(
1
,
true
,
"hello"
)
@ t.
_1
res27:
Int
=
1
@ t.
_2
res28:
Boolean
=
true
@ t.
_3
res29:
String
=
"hello"
</> 3.16.scala
上面,我们用语法
将一个元组存储到局部变量 (
a,
b,
c)
t
,用 .
_1.
_2
抽取其中的字段值。元组中的字段是不可变的。.
_3
局部变量 t
的类型可以注解为一个元组类型:
@ val
t:
(
Int
,
Boolean
,
String
)
=
(
1
,
true
,
"hello"
)
你同样可以使用
一次性抽取所有字段,并赋予名称:val
(
a,
b,
c)
=
t
|
|
元组长度从1支持到22:
@ val
t =
(
1
,
true
,
"hello"
,
'c'
,
0.2
,
0.5f
,
12345678912345L
)
t:
(
Int
,
Boolean
,
String
,
Char
,
Double
,
Float
,
Long
)
=
(
1
,
true
,
"hello"
,
'c'
,
0.2
,
0.5F
,
12345678912345L
)
</> 3.19.scala
大元组变量让人迷惑:使用 .
_1.
_2
时可能还好,但你使用 .
_3.
_11
时很容易混淆不同字段。如果你发现自己使用大元组,就该考虑定义一个 类 (3.4) ,或者使用在 Chapter 5: Scala特性 中介绍的Case Class。.
_13
数组用 Array
来实例化,数组中第n个元素用 [
T]
(
a,
b,
c)
a
检索:(
n)
@ val
a =
Array[
Int
]
(
1
,
2
,
3
,
4
)
@ a(
0
)
// first entry, array indices start from 0
res36:
Int
=
1
@ a(
3
)
// last entry
res37:
Int
=
4
@ val
a2 =
Array[
String
]
(
"one"
,
"two"
,
"three"
,
"four"
)
a2:
Array[
String
]
=
Array(
"one"
,
"two"
,
"three"
,
"four"
)
@ a2(
1
)
// second entry
res39:
String
=
"two"
</> 3.20.scala
方括号 [
Int
]
是类型参数,决定了数组类型。括号 [
String
]
中的参数决定了数组的初始内容。通过索引查询数组应使用括号 (
1
,
2
,
3
,
4
)
a
,而不是像其它常见语言那样使用方括号 (
3
)
a
。[
3
]
你可以省略类型参数,让编译器推断数组的类型。或者使用
创建一个指明类型的空数组,然后为每个索引位置分配值:new
Array[
T]
(
length)
|
|
用
创建的数组,所有元素使用默认值初始化,数值型数组用 new
Array
填充,0
数组用 Boolean
填充,false
和其它类型用 String
填充。null
Array
长度固定但其元素可变,你可以修改每个元素的值,但是不能添加或者删除元素。我们会在 Chapter 4: Scala集合 看到如何创建长度可变的集合。
Scala也支持多维数组,即数组的数组:
@ val
multi =
Array(
Array(
1
,
2
)
,
Array(
3
,
4
)
)
multi:
Array[
Array[
Int
]
]
=
Array(
Array(
1
,
2
)
,
Array(
3
,
4
)
)
@ multi(
0
)
(
0
)
res47:
Int
=
1
@ multi(
0
)
(
1
)
res48:
Int
=
2
@ multi(
1
)
(
0
)
res49:
Int
=
3
@ multi(
1
)
(
1
)
res50:
Int
=
4
</> 3.23.scala
多维数组用于代表网格,矩阵,以及其它类似数据结构。
Scala Option
代表一种可能存在,也可能不存在的值。[
T]
Option
子类型 [
T]
Some
指示其值存在,(
v:
T)
None
指示其值不存在:
@ def
hello(
title:
String
,
firstName:
String
,
lastNameOpt:
Option[
String
]
)
=
{
lastNameOpt match
{
case
Some(
lastName)
=>
println(
s"Hello $title. $lastName")
case
None =>
println(
s"Hello $firstName")
}
}
@ hello(
"Mr"
,
"Haoyi"
,
None)
Hello Haoyi
@ hello(
"Mr"
,
"Haoyi"
,
Some(
"Li"
)
)
Hello Mr.
Li
</> 3.24.scala
上述例子展示了如何用 Some
和 None
构造 Option
对象,以及如何匹配它们。Scala的许多API依赖 Option
而不是
来表达可有可无的值。通常,null
Option
强迫你同时处理“出现”和“缺失”两种场景,而使用
容易让你忘记该值是否可能为null,在运行时碰到令人迷惑的 null
NullPointerException
错误。我们会在 Chapter 5: Scala特性 深入介绍模式匹配。
Option
包含一些有用的方法,可以轻松处理空值,比如 getOrElse
,如果 Option
是 None
,就返回你提供的值:
@ Some(
"Li"
)
.
getOrElse(
"<unknown>"
)
res54:
String
=
"Li"
@ None.
getOrElse(
"<unknown>"
)
res55:
String
=
"<unknown>"
</> 3.25.scala
Option
很像大小是
或者 0
的集合。你可以像普通集合一样循环遍历它们,或者使用 1
这样的标准集合操作来处理它们。.
map
|
|
上面,我们结合
和 .
map
,当值存在时打印name的长度,否则打印 .
getOrElse
。我们会在 Chapter 4: Scala集合 学习更多关于集合的操作。-
1
Scala的For循环类似其他语言中的“foreach”循环:它们直接遍历集合元素,而不用维护自增索引。如果你想遍历索引范围,可以使用 Range
,比如 Range
:(
0
,
5
)
|
|
在循环头部放置多个
,你可以遍历嵌套的 <-
Array
:
@ val
multi =
Array(
Array(
1
,
2
,
3
)
,
Array(
4
,
5
,
6
)
)
@ for
(
arr <-
multi;
i <-
arr)
println(
i)
1
2
3
4
5
6
</> 3.30.scala
循环可以使用
语法添加守卫从句作为过滤条件:if
@ for
(
arr <-
multi;
i <-
arr;
if
i %
2
==
0
)
println(
i)
2
4
6
</> 3.31.scala
-if
条件语句和其它编程语言的类似。有一点需要注意,Scala else
-if
可以作为表达式使用,这有点像其它语言中的 else
a
三目运算符。Scala 没有单独的三目运算符语法,但 ?
b :
c
-if
可以直接写在如下所示 else
total
的右侧。+=
|
|
现在我们了解了Scala基本语法,让我们看看常见的“Fizzbuzz”编程挑战:
Write a short program that prints each number from 1 to 100 on a new line.
For each multiple of 3, print "Fizz" instead of the number.
For each multiple of 5, print "Buzz" instead of the number.
For numbers which are multiples of both 3 and 5, print "FizzBuzz" instead of the number.
我们可以如下实现:
@ for
(
i <-
Range.
inclusive(
1
,
100
)
)
{
if
(
i %
3
==
0
&&
i %
5
==
0
)
println(
"FizzBuzz"
)
else
if
(
i %
3
==
0
)
println(
"Fizz"
)
else
if
(
i %
5
==
0
)
println(
"Buzz"
)
else
println(
i)
}
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
.
.
.
</> 3.34.scala
既然
-if
是一个表达式,我们也可以这样实现:else
@ for
(
i <-
Range.
inclusive(
1
,
100
)
)
{
println(
if
(
i %
3
==
0
&&
i %
5
==
0
)
"FizzBuzz"
else
if
(
i %
3
==
0
)
"Fizz"
else
if
(
i %
5
==
0
)
"Buzz"
else
i
)
}
</> 3.35.scala
除了使用
定义循环来执行操作,你也可以结合 for
转换一个集合,得到一个新集合:yield
@ val
a =
Array(
1
,
2
,
3
,
4
)
@ val
a2 =
for
(
i <-
a)
yield
i *
i
a2:
Array[
Int
]
=
Array(
1
,
4
,
9
,
16
)
@ val
a3 =
for
(
i <-
a)
yield
"hello "
+
i
a3:
Array[
String
]
=
Array(
"hello 1"
,
"hello 2"
,
"hello 3"
,
"hello 4"
)
</> 3.36.scala
和循环类似,你可以在括号中使用
守卫从句,过滤出最终集合:if
@ val
a4 =
for
(
i <-
a if
i %
2
==
0
)
yield
"hello "
+
i
a4:
Array[
String
]
=
Array(
"hello 2"
,
"hello 4"
)
</> 3.37.scala
Comprehensions支持输入多个数组,如下 a
和 b
,得到一个打平的 Array
,类似于使用嵌套for循环:
@ val
a =
Array(
1
,
2
)
;
val
b =
Array(
"hello"
,
"world"
)
@ val
flattened =
for
(
i <-
a;
s <-
b)
yield
s +
i
flattened:
Array[
String
]
=
Array(
"hello1"
,
"world1"
,
"hello2"
,
"world2"
)
</> 3.38.scala
如果你想用多行展开嵌套循环,以便于阅读代码,你可以使用花括号
替换括号 {
}
。注意嵌套comprehension的 (
)
顺序很重要,就像嵌套循环的顺序如何影响循环动作发生顺序一样:<-
@ val
flattened =
for
{
i <-
a
s <-
b
}
yield
s +
i
flattened:
Array[
String
]
=
Array(
"hello1"
,
"world1"
,
"hello2"
,
"world2"
)
@ val
flattened2 =
for
{
s <-
b
i <-
a
}
yield
s +
i
flattened2:
Array[
String
]
=
Array(
"hello1"
,
"hello2"
,
"world1"
,
"world2"
)
</> 3.39.scala
我们可以使用comprehension编写一个不立即打印结果到控制台的FizzBuzz版本,而是返回一个 Seq
(“sequence”的简写):
@ val
fizzbuzz =
for
(
i <-
Range.
inclusive(
1
,
100
)
)
yield
{
if
(
i %
3
==
0
&&
i %
5
==
0
)
"FizzBuzz"
else
if
(
i %
3
==
0
)
"Fizz"
else
if
(
i %
5
==
0
)
"Buzz"
else
i.
toString
}
fizzbuzz:
IndexedSeq[
String
]
=
Vector(
"1"
,
"2"
,
"Fizz"
,
"4"
,
"Buzz"
,
.
.
.
</> 3.40.scala
然后我们可以任意使用这个 fizzbuzz
集合:把它保存在一个变量中,或者传递到方法中,又或者用其他方式处理。我们会在 Chapter 4: Scala集合 介绍怎样操作这个集合。
你可以用
关键字定义方法:def
@ def
printHello(
times:
Int
)
=
{
println(
"hello "
+
times)
}
@ printHello(
1
)
hello 1
@ printHello(
times =
2
)
// argument name provided explicitly
hello 2
</> 3.41.scala
传递错误类型的参数,或者缺失必要参数时,会得到一个编译错误。如果参数有默认值,可以不传递它。
|
|
方法执行动作除了打印,还可以返回值。花括号
代码块里的最后一个表达式的估值结果就是方法的返回值。{
}
@ def
hello(
i:
Int
=
0
)
=
{
"hello "
+
i
}
@ hello(
1
)
res96:
String
=
"hello 1"
</> 3.44.scala
你可以调用方法打印其返回值,或者执行其它计算:
@ println(
hello(
)
)
hello 0
@ val
helloHello =
hello(
123
)
+
" "
+
hello(
456
)
helloHello:
String
=
"hello 123 hello 456"
@ helloHello.
reverse
res99:
String
=
"654 olleh 321 olleh"
</> 3.45.scala
你可以使用
语法定义函数。函数与方法类似,你可以带参调用它们,执行一个动作,或者返回一个值。不同于方法,函数本身是一个value:你可以当参数传递它们;或者用变量存储起来,稍后再调用它们。=>
@ var
g:
Int
=>
Int
=
i =>
i +
1
@ g(
10
)
res101:
Int
=
11
@ g =
i =>
i *
2
@ g(
10
)
res103:
Int
=
20
</> 3.46.scala
注意不同于方法,函数不能有可选参数(即带默认值的参数),也不支持语法
定义类型参数。当方法转为函数时,可选参数必须被传递,类型参数也要固定为具体类型。函数是匿名的,这使得涉及它们的栈踪迹相较于方法而言,阅读起来不太方便。[
T]
通常,你应该优先使用方法而非函数,除非你需要灵活地将其作为参数传递,或者存储在变量中。
一个常见用例就是把函数作为参数传递到方法中。这样的方法称为“高阶方法”。下文中,我们有一个类 Box
,它有一个方法 printMsg
,打印的内容是一个
变量,另外还有一个独立方法 Int
update
,输入一个类型是
的函数,用来更新 Int
=>
Int
x
。你可以传递一个函数字面量给 update
以改变 x
的值:
|
|
简单的函数字面量,比如 i
可以简写为 =>
i +
5
_
,下划线 +
5
_
代表函数参数。
@ b.
update(
_ +
5
)
@ b.
printMsg(
"Hello"
)
Hello11
</> 3.49.scala
函数字面量中的占位符语法对多参数函数同样适用,比如
可以写作 (
x,
y)
=>
x +
y_
。+
_
把函数作为参数的方法,可以接收方法引用,只要其签名符合函数类型要求,本例中其类型是
:Int
=>
Int
@ def
increment(
i:
Int
)
=
i +
1
@ val
b =
new
Box(
123
)
@ b.
update(
increment)
// Providing a method reference
@ b.
update(
x =>
increment(
x)
)
// Explicitly writing out the function literal
@ b.
update{
x =>
increment(
x)
}
// Methods taking a single function can be called with {}s
@ b.
update(
increment(
_)
)
// You can also use the `_` placeholder syntax
@ b.
printMsg(
"result: "
)
result:
127
</> 3.50.scala
方法可以接收多个参数列表。这对于编写控制结构类的高阶方法很有用,比如下列方法 myLoop
:
|
|
把函数字面量传递到方法的能力在标准库中得到了很好的应用,实现了简洁地在集合上执行变换,我们将在 Chapter 4: Scala集合 看到更多例子。
你可以使用
关键字定义类,并用 class
实例化。默认情况下,所有传递给类构造器的参数在类的所有方法中都是可访问的:下面的new
定义了类构造器的私有字段,因此 (
x:
Int
)
x
可在 方法 printMsg
中被访问,但是你不能在类外访问它:
|
|
使用
修饰它,可以让它在类外也可访问;使用 val
不仅让它可以公开访问,也可修改:var
|
|
|
|
你可以在类里使用
或者 val
来存储数据。这些变量在类实例化时会计算一次:var
|
|
类似传统面向对象语言中的 trait
interface
:一系列方法的封装,通过类来继承,这些类实例之间可以交换使用。
@ trait
Point{
def
hypotenuse:
Double
}
@ class
Point2D(
x:
Double
,
y:
Double
)
extends
Point{
def
hypotenuse =
math.
sqrt(
x *
x +
y *
y)
}
@ class
Point3D(
x:
Double
,
y:
Double
,
z:
Double
)
extends
Point{
def
hypotenuse =
math.
sqrt(
x *
x +
y *
y +
z *
z)
}
@ val
points:
Array[
Point]
=
Array(
new
Point2D(
1
,
2
)
,
new
Point3D(
4
,
5
,
6
)
)
@ for
(
p <-
points)
println(
p.
hypotenuse)
2.23606797749979
8.774964387392123
</> 3.61.scala
上面,我们定义了 Point
trait,其拥有一个方法
。子类 def
hypotenuse:
Double
Point2D
和 Point3D
有各自不同的参数集。因此我们可以把 Point2D
和 Point3D
都放进 points
来一视同仁,这些对象都包括一个方法 :
Array[
Point]
,而不用管对象的实际类是什么。def
hypotenuse
本章,我们快速浏览了Scala语言核心。尽管特定的语法对你可能是新鲜的,但是它们的概念你应该是熟悉的:原始类型,数组,循环,条件,方法,以及类,这几乎是每种编程语言必有的一部分。接下来,我们会接触Scala核心标准库:Scala集合。
Exercise: Define a
method that takes a def
flexibleFizzBuzz
callback
function as its argument, and allows the caller to decide what they want to do
with the output. The caller can choose to ignore the output, String
=>
Unit
println
the
output directly, or store the output in a previously-allocated array they
already have handy.
|
|
Exercise: Write a recursive method printMessages
that can receive an array of Msg
class instances, each with an optional parent
ID, and use it to print out a
threaded fashion. That means that child messages are print out indented
underneath their parents, and the nesting can be arbitrarily deep.
class
Msg(
val
id:
Int
,
val
parent:
Option[
Int
]
,
val
txt:
String
)
def
printMessages(
messages:
Array[
Msg]
)
:
Unit
=
.
.
.
</> 3.64.scala
|
|
Exercise: Define a pair of methods withFileWriter
and withFileReader
that can be
called as shown below. Each method should take the name of a file, and a
function value that is called with a java
or
.
io.
BufferedReaderjava
that it can use to read or write data. Opening and
closing of the reader/writer should be automatic, such that a caller cannot
forget to close the file. This is similar to Python "context managers" or Java
"try-with-resource" syntax..
io.
BufferedWriter
TestContextManagers.sc
withFileWriter
</> 3.67.scala(
"File.txt"
)
{
writer=>
writer.
write(
"Hello\n"
)
;
writer.
write(
"World!"
)
}
val
result=
withFileReader(
"File.txt"
)
{
reader=>
reader.
readLine(
)
+
"\n"
+
reader.
readLine(
)
}
assert(
result==
"Hello\nWorld!"
)
You can use the Java standard library APIs
java
and .
nio.
file.
Files.
newBufferedWriternewBufferedReader
for working
with file readers and writers. We will get more familiar with working with
files and the filesystem in Chapter 7: Files and Subprocesses.