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实现流行的"FizzBuzz"的编程挑战
本章将带你快速浏览Scala语言。从现在开始,我们将专注于Scala的基础,你也许会发现这些基础和任何一门主流编程语言都类似。
本章的目标就是让你能够熟练得像你惯用的其他主流编程语言一样,毫无障碍地编写一些Scala代码。本章并不会涵盖任何Scala特定的编程风格或语言特性:这些会放到第五章《耳目一新的Scala特性》中去。
本章的示例代码我们将使用Ammonite Scala REPL来编写:
$ amm
Loading...
Welcome to the Ammonite Repl 2.0.4 (Scala 2.13.1 Java 11.0.2)
@
|
|
这些类型和Java的基本数据类型是等价的,并且对于像C#,C++或其他静态类型的编程语言来说都是类似的。
你可以在控制台输入这些类型得到相应的输出结果。每一种类型都支持经典的操作,例如布尔类型支持逻辑运算符||
和&&
,数值类型支持四则运算符 + - * /
,位运算支持 |
和&
等等。所有的值都支持用==
来判断是否相等,用!=
来判断不相等。
@ true || false
res0: Boolean = true
@ true && false
res1: Boolean = false
默认使用的是32位的Int类型:
@ 1 + 2
res2: Int = 3
算术运算符的优先级和其他其他编程语言一样:*
和/
比+
或-
优先级更高,并且括号可以用来分组。
@ 1 + 2 * 3
res3: Int = 7
@ (1 + 2) * 3
res4: Int = 9
Int类型是带符号的,而且当溢出时,值又会绕回从最小值开始:
@ 2147483647
res5: Int = 2147483647
@ 2147483647 + 1
res6: Int = -2147483648
你可以通过使用大写的L
语法来声明数字为64位的长整型,那么就不这么容易出现溢出了:
@ 2147483647L
res7: Long = 2147483647L
@ 2147483647L + 1L
res8: Long = 2147483648L
除了基本操作外,java.lang.Integer
和java.lang.Long
类还拥有很多有用的方法:
@ java.lang.Integer.<tab>
BYTES decode numberOfTrailingZeros
signum MAX_VALUE divideUnsigned
parseInt sum MIN_VALUE
getInteger parseUnsignedInt toBinaryString
...
@ java.lang.Integer.toBinaryString(123)
res9: String = "1111011"
@ java.lang.Integer.numberOfTrailingZeros(24)
res10: Int = 3
在整数后面使用.0的语法,指定使用64位的浮点型,并且它也拥有(和上面整形类型)类似的算术运算操作:
@ 1.0 / 3.0
res12: Double = 0.3333333333333333
你也可以在整数后面使用.0F的语法,要求使用32位的浮点型:
@ 1.0F / 3.0F
res13: Float = 0.33333334F
在Scala中,字符串是由每个16位的字符组成的数组:
@ "hello world"
res14: String = "hello world"
字符串可以由.substring
方法进行切片:
@ "hello world".substring(0, 5)
res15: String = "hello"
@ "hello world".substring(5, 10)
res16: String = " worl"
构造字符串时可以使用+
来拼接,或者通过使用字母前缀s"..."
的字符串插值来实现,此时可以使用$
或${...}
来替换要插入的变量。
@ val s = "hello" + 1 + " " + "world" + 2
s: String = "hello1 world2"
@ val x = 1; val y = 2
@ val s = s"Hello $x World $y"
s: String = "Hello 1 World 2"
@ val s = s"Hello ${x + y} World ${x - y}"
s: String = "Hello 3 World -1"
你可以用val
关键字来命名一个局部的常量:
@ val x = 1
@ x + 2
res25: Int = 3
注意val
标记的值是不可变的:当赋值后,你无法再对该x
赋上另外一个值:
@ x = 3
cmd41.sc:1: reassignment to val
val res26 = x = 3
^
Compilation Failed
如果想要一个局部变量重新赋值,你必须使用var
关键字。
@ var y = 1
@ y + 2
res27: Int = 3
@ y = 3
@ y + 2
res29: Int = 5
通常来说,你应该尽可能的使用val
:因为在程序中大多值是不需要被再次赋值的,使用val
能防止一些因偶然再次赋值而造成的错误。当且仅当你确定需要再次赋值时,才使用var
。
val
和var
关键字都可以显式地声明类型:
@ val x: Int = 1
@ var s: String = "Hello"
s: String = "Hello"
@ s = "World"
这样做可使代码文档化,以便能帮助人们更好理解你的代码。同样,当你不小心给一个变量赋值错误的类型时,这样的写法能让编译器帮助你捕捉到错误。
@ val z: Int = "Hello"
cmd33.sc:1: type mismatch;
found : String("Hello")
required: Int
val z: Int = "Hello"
^
Compilation Failed
Tuple 是一种有着定长的值的集合,其中每个值可以有不同的类型:
@ val t = (1, true, "hello")
t: (Int, Boolean, String) = (1, true, "hello")
@ t._1
res34: Int = 1
@ t._2
res35: Boolean = true
@ t._3
res36: String = "hello"
上述示例我们用(a, b, c)
的语法将一个tuple
保存到了局部变量t
中,接着使用._1
,._2
,._3
从中将值依次取出。tuple
中的字段是不可变的。
@ val t: (Int, Boolean, String) = (1, true, "hello")
你也可以使用val (a, b, c) = t
的语法,一次性将所有的值都提取出来,并给他们赋予一个有意义的名称:
@ val (a, b, c) = t
a: Int = 1
b: Boolean = true
c: String = "hello"
@ a
res39: Int = 1
@ b
res40: Boolean = true
@ c
res41: String = "hello"
Tuple的字段数最多不能超过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
)
多数Tuple相对比较小。大的Tuple容易让人困惑:当至少操作._1
,._2
和._3
也行还行,
但是当用到._11
和._13
这样的大Tuple时,就很容易把字段搞混了。
如果你正好要处理大的Tuple,考虑定义一个Class
(3.4.1节)或者我们将会在第五章的《耳目一新的Scala特性》介绍的Case Class
。
我们可以使用Array[T](a, b, c)
语法来定义一个数组,并且使用a(n)
来访问数组每个元素。
@ val a = Array[Int](1, 2, 3, 4)
@ a(0) // 取数组的第一个元素(数组下标是从0开始)
res44: Int = 1
@ a(3) // 去最后一个元素
res45: Int = 4
@ val a2 = Array[String]("one", "two", "three", "four")
a2: Array[String] = Array("one", "two", "three", "four")
@ a2(1) // 去第二个元素
res47: String = "two"
方括号内的类型参数[Int]
或者[String]
决定了数组的类型,与此同时圆括号内的(1, 2, 3, 4)
决定了数组初始化的内容。需要注意的是,通过下标索引访问数组中的元素是通过圆括号a(3)
而不是方括号a[3]
,这一点与其他编程语言的约定有所不同。
你也可以忽略掉显式的类型参数,由编译器来推断数组的类型:
@ val a = Array(1, 2, 3, 4)
a: Array[Int] = Array(1, 2, 3, 4)
@ val a2 = Array("one", "two", "three", "four")
a2: Array[String] = Array("one", "two", "three", "four")
或使用new Array(T)[length]
来定义一个(固定长度的)空的数组,随后在通过下标索引进行赋值:
@ val a = new Array[Int](4)
a: Array[Int] = Array(0, 0, 0, 0)
@ a(0) = 1
@ a(2) = 100
@ a
res53: Array[Int] = Array(1, 0, 100, 0)
数组是一直可变但长度固定的数据结构:你可以改变每个下标所应的元素,但是不能通过增加或者删除里面的元素来改变数组的大小。 稍后我们将在第4章《Scala集合》中看到如何创建可变长度的集合类型。
使用new Array
创建出来的数组,对于数值类型来说,所有的元素默认值都是0,对于布尔类型来说,所有元素的默认值是false,对于字符串或其他类型来说,所有元素的默认值都是null。
Scala同样支持多维数组(也称数组的数组):
@ val multi = Array(Array(1, 2), Array(3, 4))
multi: Array[Array[Int]] = Array(Array(1, 2), Array(3, 4))
@ multi(0)(0)
res55: Int = 1
@ multi(0)(1)
res56: Int = 2
@ multi(1)(0)
res57: Int = 3
@ multi(1)(1)
res58: Int = 4
多维数组非常适合于表示网格,矩阵等相似类型的数据结构。
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 Mr. $lastName")
case None => println(s"Hello $firstName")
}
}
@ hello("Mr", "Haoyi", None)
Hello Haoyi
@ hello("Mr", "Haoyi", Some("Li"))
Hello Mr. Li
上面的例子向你展示了如果使用Some
和None
去构建一个Option,同时也演示了如何使用模式匹配来处理Option。Scala的许多API依靠的就是Option而不是null来判断一个值是否存在。一般说来,Option会强迫你去处理存在或不存在的情况,而使用null的时候却恰恰容易忘记这个值究竟是不是非空的,结果在运行期导致了令人困惑的NullPointerException
异常发生。
我们会在第5章《耳目一新的Scala特性》深入模式匹配。
Option包含了一些有用的方法,这些方法使它能够轻松地和可选值配合使用,比如getOrElse
方法就允许在Option是None的时候,用一个可选值来代替:
@ Some("Li").getOrElse("<unknown>")
res0: String = "Li"
@ None.getOrElse("<unknown>")
res1: String = "<unknown>"
Option和一个大小为0或者1的集合非常像,你可以像遍历集合那样的对它进行操作:
@ def hello2(nameOpt: Option[String]) = {
for (name <- nameOpt) println(s"Hello $name")
}
@ hello2(None) // 什么都不做
@ hello2(Some("Haoyi"))
Hello Haoyi
或者使用标准的集合操作方法来对其进行转换,比如使用.map
:
@ def lengthOfName(nameOpt: Option[String]) = nameOpt.map(_.length).getOrElse(-1)
@ lengthOfName(Some("Haoyi"))
res6: Int = 5
@ lengthOfName(None)
res7: Int = -1
上面的示例中,我们将.map
和.getOrElse
组合在一起使用,如果输入的字符串存在,则输出字符串的长度,否则输出-1。
在第四章《Scala集合》中我们将学习更多关于Scala集合的操作。
Scala中的For循环和其他语言的foreach
类似: 不需要显式的维护一个自增的下标索引,而是直接依次遍历集合中的每个元素:
@ var total = 0; val items = Array(1, 10, 100, 1000)
@ for (item <- items) total += item
@ total
res79: Int = 1111
如果你想要循环一个索引范围,可以想这样使用Range(0, 5)
,从0遍历到5:
@ var total = 0
@ for (i <- Range(0, 5)) {
println("Looping " + i)
total = total + i
}
Looping 0
Looping 1
Looping 2
Looping 3
Looping 4
@ total
res64: Int = 10
你可以在循环头部使用multiple <-s
,来循环一个内嵌数组:
@ val multi = Array(Array(1, 2, 3), Array(4, 5, 6))
@ for (arr <- multi; i <- arr) println(i)
1
2
3
4
5
6
循环内还能使用if
语句进行过滤:
@ for (arr <- multi; i <- arr; if i % 2 == 0) println(i)
2
4
6
if-else条件判断和其他编程语言是类似的:
@ var total = 0
@ for (i <- Range(0, 10)) {
if (i % 2 == 0) {
total += i
} else {
total += 2
}
}
@ total
res93: Int = 30
在Scala中,if-else也可以实现类似其他语言的三目运算。因此,你也可以按照下面的写法来实现上面的代码:
@ var total = 0
@ for (i <- Range(0, 10)) {
total += (if (i % 2 == 0) i else 2)
}
@ total
res97: Int = 30
现在我们知道了Scala的语法基础,那么来挑战一下这个经典的Fizzbuzz
的算法题吧:
写一个小程序,从1至100每行输出当前的数字编号。 如果是3的倍数,则用
Fizz
代替数字输出。 如果是5的倍数,则用Buzz
代替数字输出。 如果既是3的倍数也是5的倍数,则用FizzBuzz
代替数字输出。
我们可以用以下程序达成:
@ 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
...
由于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
)
}
除了使用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")
你可以在for循环的括号内使用if
表达式过滤元素,再得到最终的集合:
@ val a4 = for (i <- a if i % 2 == 0) yield "hello " + i
a4: Array[String] = Array("hello 2", "hello 4")
或者对两个输入数组(如果下所述的a
和b
)使用解析,使之扁平化变成一个输出数组,这和之前提到的内嵌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")
为了更好的可读性,如果你想让子循环跨多行编写,你还可以用花括号{}
来代替圆括号()
。注意<-s
的顺序是影响解析结果的,下面的示例就演示了子循环顺序不同而产生的结果也不同的情况:
@ 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")
我们可以使用解析来编写一个新的FizzBuzz
版本,它并不会立即把结果输出到控制台,而是将结果返回给一个Seq
(序列的简称)对象:
@ 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",
...
我们接下来可以对这个fizzbuzz
集合进行想要的操作:比如保存到一个变量里,将其传给一个方法,或者用其他方式进行传递。我们将会在第四章《Scala的集合》中再详细地向你介绍这些集合的用法。
你可以使用关键字def
来定义方法:
@ def printHello(times: Int) = {
println("hello " + times)
}
@ printHello(1)
hello 1
@ printHello(2)
hello 2
@ printHello(times = 3) // 显式指定参数名称
hello 3
传递一个错误类型的参数或者缺少必要参数会引起编译错误:
@ printHello2("1") // 错误类型的参数
cmd128.sc:1: type mismatch;
found : String("1")
required: Int
val res128 = printHello2("1")
^
Compilation Failed
@ printHello() // 缺少参数
cmd121.sc:1: not enough arguments for method printHello: (times: Int)Unit.
Unspecified value parameter times.
val res121 = printHello()
^
Compilation Failed
参数是可以有默认值的,这样的参数就不是必须要传的:
@ def printHello2(times: Int = 0) = {
println("hello " + times)
}
@ printHello2(1)
hello 1
@ printHello2()
hello 0
方法除了打印外,还能返回执行结果:
@ def hello(i: Int = 0) = {
"hello " + i
}
@ hello(1)
res125: String = "hello 1"
@ hello()
res126: String = "hello 0"
通常来说在Scala的方法中,花括号{}
里最后一行的表达式被当做是返回值。你可以调用方法将结果输出或者返回结果,再由其他程序计算:
@ println(hello())
hello 0
@ val helloHello = hello(123) + " " + hello(456)
helloHello: String = "hello 123 hello 456"
@ helloHello.reverse
res17: String = "654 olleh 321 olleh"
你可以使用=>
语法来顶一个函数值:
@ val f: Int => Int = i => i + 1
@ f(1)
res19: Int = 2
@ f(2)
res20: Int = 3
从调用来看函数值和方法类似,通过传参调用执行一些操作或返回某个结果。与方法不同的是,函数本身就是值:你可以将其任意传递或暂存到变量中,稍后用得到的时候在执行。
@ var g: Int => Int = i => i + 1
@ g(10)
res25: Int = 11
@ g = i => i * 2
@ g(10)
res27: Int = 20
需要注意的和方法不同是,函数值不能拥有可选的参数(比如说默认参数)并且也不能接受类型参数。当一个方法被转成函数值时,任何的可选参数都必须显式的包含或排除(比如说,使用了占位符_
语法)
而且类型参数要能固定成实际的类型。函数值是匿名的,和方法执行相比,函数使得堆栈追踪信息变得不是那么的容易阅读。
一般来说,你应当多使用方法,除非真的是有需求要将函数当做参数或保存在变量中。但是如果你需要的是灵活度,那函数值是值得拥有的强大工具。
一种比较常见地用法是将函数值当成参数传递到接收这个函数类型的方法。比如说下面示例代码定义的update
方法:
@ class Box(var x: Int) {
def update(f: Int => Int) = x = f(x)
def printMsg(msg: String) = println(msg + x)
}
我们定义了一个Box类,它有一个printMsg
方法将其内容输出(一个整数数字),同时还有一个接收参数类型是Int => Int
的方法update
用来更新内部变量x
的值。
你可以通过传递一个函数的实现到update
方法中,从而对变量x
进行修改:
@ val b = new Box(1)
@ b.printMsg("Hello")
Hello1
@ b.update(i => i + 5)
@ b.printMsg("Hello")
Hello6
其中.update
调用可以被简写成如下代码:
@ b.update(_ + 5)
@ b.printMsg("Hello")
Hello11
最后一个例子展示了即使是简单得形如i => i + 5
这样的函数,也可以简写成_ + 5
,其中下划线_
在这代表的是函数的参数。在多个参数的函数中也是可以的,比如(x ,y) => x + y
可以写成_ + _
。
任何一个接收函数参数的方法也可以传递给另外一个方法,只要这个方法的签名和函数类型相匹配,像下面的Int => Int
:
@ def increment(i: Int) = i + 1
@ val b = new Box(123)
@ b.update(increment)
@ b.update(x => increment(x)) //显式的用lambda来调用
@ b.update(increment(_)) // 你也可以使用下划线`_`的占位符语法
@ b.printMsg("result: ")
result: 126
这个可以将函数当成参数传递给方法的能力被广泛的运用于标准库中,例如实现了简洁的集合转换操作。我们会在第四章《Scala集合》中看到更多用法。
你可以使用关键字class
来定义类,然后使用关键字new
来实例化它们:
@ class Foo(x: Int) {
def printMsg(msg: String) = println(msg + x)
}
@ val f = new Foo(1)
@ f.printMsg("hello")
hello1
@ f.printMsg("world")
world1
默认情况下,类的构造函数定义的变量的作用域对该类的所有方法都是可用的:这个上面代码的(x: Int)
既定义了私有属性,同时也定义了构造函数。属性x
于是就可以在函数printMsg
被访问,但是不能再类的外部被访问:
@ f.x
cmd132.sc:1: value x is not a member of ammonite.$sess.cmd128.Foo
val res132 = f.x
^
Compilation Failed
当你加上val
来定义它时,x
就可以通过外部访问了:
@ class Bar(val x: Int) {
def printMsg(msg: String) = println(msg + x)
}
@ val b = new Bar(1)
@ b.x
res134: Int = 1
你还可以用var
来定义它,这样还能对他进行修改:
@ class Qux(var x: Int) {
def printMsg(msg: String) = {
x += 1
println(msg + x)
}
}
@ val q = new Qux(1)
@ q.printMsg("hello")
hello2
@ q.printMsg("hello")
hello3
当然你也可以在类的结构体内用val
或var
来定义属性来保存数据。这些都会在对象实例化的时候,会进行计算:
@ class Foo(x: Int) {
val bangs = "!" * x
def printMsg(msg: String) = println(msg + bangs)
}
@ val f = new Foo(3)
@ f.printMsg("hello")
hello!!!
@ f.printMsg("world")
world!!!
特质类似于传统面向对象语言的接口:它拥有一组方法,多个类继承它后,就能做到类的实例之间相互替换。
@ 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
以上,我们定义了一个带有单个方法def hypotenuse: Double
特性Point
。两个子类Point2D
和Point3D
各自的构造函数参数不同,但是都实现了方法hypotenuse
,接下来我们把Point2Ds
和Point3Ds
实例化的对象
放入到数组points: Array[Point]
中,此时不管他们实际的类型是什么,他们都当成是拥有hypotenuse
方法的对象。
本章我们快速浏览了Scala语言的核心部分。尽管有些语法也许对你来说比较陌生,但是概念应该都是类似的:基本上每个编程语言都有基本数据类型,字符串,循环,条件语句,方法,函数,类和接口。 到现在我们已经涵盖了Scala语言的基础部分,接下来我们要看看Scala的标准类库的核心部分:Scala集合。