Scala 编程(一)Scala 编程总览

时间:2021-10-16 03:34:18

Scala 简介

  Scala 属于“可伸展语言”,源于它可以随使用者的需求而改变和成长。Scala 可以应用在很大范围的编程任务上,小到脚本大到建立系统均可以。 Scala 跑在标准 Java 平台上,可以与所有 Java 库实现无缝交互。它把面向对象和函数式编程理念加入到静态类型语言中。Scala可以培育新的类型和新的控制结构,使它用起来更像是内建的类型和控制结构一样,它没有提供所有你在一种“完美齐全”语言中可能需要的东西,而是把制作这些东西的工具放在了你的手中。

Scala 编程总览

Scala 解释器

  Scala 解释器是一个编写 Scala 表达式和程序的交互式 “ shell ”,在解释器里输入一个表达式,它将计算这个表达式并打印结果。在终端输入 scala 即可打开解释器:

➜  ~  scala
Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_65).
Type in expressions to have them evaluated.
Type :help for more information.

在输入表达式并回车之后:

scala> 1 + 2
res0: Int = 3

解释器计算表达式后打印的结果包括:

  ·一个自动产生的或用户定义的名称说明计算的值(res0,表示结果0)

  ·一个冒号(:),跟着表达式的类型(Int),

  ·一个等号(=),

  ·计算表达式所得到的结果(3)。

resX识别符还将用在后续的代码行中。例如,既然res0已在之前设为3,res0 * 3就是9:

scala> res0 * 3
res1: Int = 9

变量

  Scala有两种变量,val和var。val一旦初始化了再赋值了, var则可以在它生命周期中被多次赋值。下面是一个val的定义:

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

  这个语句引入了msg当作字串"hello world"的名字。类型是 String ,因为Scala的字串是由Java的String类实现的。Scala能自动理解省略的类型。在这个例子里,用一个字串文本初始化了msg,Scala推断msg的类型是 String 。不过,也可以显式地定义类型,显式的类型标注不但可以确保 Scala 编译器推断你倾向的类型,还可以作为将来代码读者有用的文档。 Scala 里变量的类型在其名称之后,用冒号分隔。如:

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

  由于msg是val不是var,不能再给它赋值,否则会报错:

scala> msg = "hello"
<console>:11: error: reassignment to val
msg = "hello"
^

  如果需要可重赋值,应该使用var定义变量:

scala> var msg3 = "hello world"
msg3: String = hello world scala> msg3 = "hello"
msg3: String = hello

  要输入一些能跨越多行的东西,只要一行行输进去就行。如果输到行尾还没结束,解释器将在下一行回应一个竖线:

scala> var multLine =
| "next line"
multLine: String = next line

  如果输入了一些错误的东西,而解释器仍在等着更多的输入,可以通过按两次回车取消掉:

scala> val oops =
|
|
You typed two blank lines. Starting a new command.

函数

  Scala 里函数定义写法:

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

  函数的基本结构如图所示:

Scala 编程(一)Scala 编程总览

  有时候Scala编译器会需要定义函数的结果类型。比方说,如果函数是递归的就必须显式地定义函数结果类型。然而在max的例子里,你可以不用写结果类型,编译器也能够推断它。如果函数仅由一个句子组成,可以可选地不写大括号:

scala> def max2(x: Int, y: Int) = if (x > y) x else y
max2: (x: Int, y: Int)Int

  一旦定义了函数,就可以用它的名字调用它,如:

scala> max(1,2)
res3: Int = 2

  还有既不带参数也不返回有用结果的函数定义:

scala> def hello() = println("hello world")
hello: ()Unit

  当定义hello()函数,解释器会回应一个 hello: ()Unit 。“hello”是函数名。空白的括号说明函数不带参数。 Unit 是 hello 的结果类型。 Unit 的结果类型指的是函数没有返回有用的值。因此结果类型为 Unit 的方法,仅仅是为了它们的副作用而运行。在 hello() 的例子里,副作用是在标准输出上打印一句客气的助词。

脚本

  运行脚本的方法为 scala [file.scala],通过 Scala 的名为 args 的数组可以获得传递给 Scala 脚本的命令行参数。 Scala 里数组从 0 开始,通过括号里指定索引访问一个元素。

while 循环;if 判断;foreach 和 for枚举

  用如下方式写 while 和 if :

var i = 0
while (i < args.length) {
if (i != 0)
print (" ")
print (args(i))
i += 1
}

  像这样一次性发出一个指令式的命令,用循环去枚举,并经常改变共享在不同函数之间的状态称为指令式风格编程, Scala 允许指令式地编程,但函数式风格编程更舒适:

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

  这行代码中,在 args 上调用 foreach 方法,并把它传入函数,传入了带有一个叫做 arg 参数的函数文本,函数体是 println(arg) 。

  Scala 解释器推断 arg 的类型是 String ,因为 String 是调用 foreach 的那个数组的元素类型。显式的需要加上类型名,要把参数部分包裹在括号里:

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

  如果函数文本由带一个参数的一句话组成,甚至都不需要显式命名和指定参数

args.foreach(println)

  总而言之,函数文本的语法就是,括号里的命名参数列表,右箭头,然后是函数体:

Scala 编程(一)Scala 编程总览

  Scala 里指令式 for 使用方法:

for (arg <- args)
println(arg)

  这个表达式里“for”之后的括号包含 arg<-args 。<- 右侧的是熟悉的 args 数组。<-左侧的是“arg”, val 的名称(不是 var )。尽管 arg 可能感觉像 var ,因为他在每次枚举都会得到新的值,但它的确是 val :  arg 不能在 for 表达式的函数体中重新赋值。取而代之,对每个 args 数组的元素,一个新的 arg val 将被创建并初始化为元素值,然后 for 的函数体将被执行。

带类型的参数化数组

  Scala里可以使用new实例化对象或类实例。在实例化对象时可以使用值和类型把它参数化,即在创建实例的时候“设置”它,通过把加在括号里的对象传递给实例的构造器的方式来用值参数化实例:

scala> val big = new java.math.BigInteger("123456")
big: java.math.BigInteger = 123456

  通过在方括号里设定一个或更多类型来参数化实例:

val greetStrings = new Array[String](3)
greetStrings(0) = "Hello"
greetStrings(1) = ", "
greetStrings(2) = "world!"
for (i <- 0 to 2)
print(greetString(i))

  代码里展示的例子中,greetStrings是类型Array[String](字串数组)的值,并被第一行代码里的值 3 参数化,使它的初始长度为 3 。如果想用一种更显式的方式,可以显式定义greetStrings的类型:

val greetStrings: Array[String] = new Array[String](3)

  当用val定义一个变量,那么这个变量就不能重新赋值,但它指向的对象却仍可以暗自改变。所以在本例中,不能把 greetStrings 重新赋值成不同的数组; greetStrings 将永远指向那个它被初始化时候指向的同一个 Array[String] 实例。但是能一遍遍修改那个 Array[String] 的元素,因此数组本身是可变的。

  最后两行for表达式的第一行代码演示了Scala的另一个通用规则:如果方法仅带一个参数,可以不带点或括号的调用它。本例中 to 实际上是带一个 Int 参数的方法。代码 0 to 2 被转换成方法调用 (0).to(2) 。注意这个语法仅在指定方法调用的接受者时才起作用。从技术上讲, Scala 没有操作符重载,因为它根本没有传统意义上的操作符。取而代之的是,诸如 +,-,* 和 / 这样的字符可以用来做方法名。因此,当第一步在 Scala 解释器里输入 1 + 2 ,实际上正在 Int 对象 1 上调用一个名为 + 的方法,并把 2 当作参数传给它,也可以使用传统的方法调用语法把 1 + 2 替代写成 (1).+(2) 。

  这里演示的可以看到为什么数组在 Scala 里是用括号访问的。数组和 Scala 里其他的类一样只是类的实现。当一个或多个值或变量外使用括号时, Scala 会把它转换成对名为 apply 的方法调用。于是 greetStrings(i) 转换成 greetStrings.apply(i) 所以 Scala 里访问数组的元素也只不过是跟其它的一样的方法调用。这个原则不仅仅局限于数组:任何对某些在括号中的参数的对象的应用将都被转换为对 apply 方法的调用。当然前提是这个类型实际定义过 apply 方法。所以这不是一个特例,而是一个通则。

  与之相似的是,当对带有括号并包括一到若干参数的变量赋值时,编译器将把它转化为对带有括号里参数和等号右边的对象的 update 方法的调用。例如, greetStrings(0) = "Hello"

将被转化为 greetStrings.update(0, "Hello") 。

  Scala 提供了更简洁的方法创造和初始化数组:

scala> val numNames = Array("zero", "one", "two")
numNames: Array[String] = Array(zero, one, two)

  这行代码创建了长度为 3 的新数组,用传入的字串 "zero","one" 和 "two" 初始化。编译器推断数组的类型是 Array[String] 。代码里实际做的就是调用了一个叫做 apply 的方法,从而创造并返回了新的数组。 apply 方法带可变数量个参数,被定义在 Array 的伴生对象: companion object 上。可以认为这个就像在 Array 类上调用一个叫做 apply 的静态方法:

scala> val numNames2 = Array.apply("zero", "one", "two")
numNames2: Array[String] = Array(zero, one, two)

使用 List

  Scala 数组是一个所有对象都共享相同类型的可变序列。比方说 Array[String] 仅包含 String 。尽管实例化之后无法改变 Array 的长度,它的元素值却是可变的。因此,Array是可变的对象。和数组一样, List[String] 包含的仅仅是 String 。 scala.List 总是不可变的(而 Java 的 List 可变),它是设计给函数式风格的编程用的。创建一个 List 很简单:

scala> val oneTwoThree = List(1, 2, 3)
oneTwoThree: List[Int] = List(1, 2, 3)

  代码完成了一个新的叫做 oneTwoThree 的 val ,并已经用带有整数元素值 1,2 和 3 的新 List[Int] 初始化。在一个 List 上调用方法时,似乎这个名字指代的 List 看上去被改变了,而实际上它只是用新的值创建了一个 List 并返回。比方说,List有个叫“:::”的方法实现叠加功能:

scala> val oneTwo = List(1, 2)
oneTwo: List[Int] = List(1, 2) scala> val threeFour = List(3, 4)
threeFour: List[Int] = List(3, 4) scala> val oneTwoThreeFour = oneTwo ::: threeFour
oneTwoThreeFour: List[Int] = List(1, 2, 3, 4)

  上述代码执行后 oneTwo 和 threeFour 并没有发生变化, oneTwoThreeFour 是用一个新的 List。 List 最常用的操作符是发音为 “cons” 的 ‘::’ 。 Cons 把一个新元素组合到已有 List 的最前端,然后返回结果List:

scala> val twoThree = List(2, 3)
twoThree: List[Int] = List(2, 3) scala> val oneTwoThree = 1 :: twoThree
oneTwoThree: List[Int] = List(1, 2, 3)

  表达式 “1 :: twoThree” 中, :: 是它右操作数列表 twoThree ,的方法。这是一个简单的需记住的规则:如果一个方法被用作操作符标注,如 a * b ,那么方法被左操作数调用,就像a.*(b)——除非方法名以冒号结尾。这种情况下,方法被右操作数调用。因此, 1 :: twoThree 里, :: 方法被 twoThree 调用,传入1,像这样:twoThree.::(1) 。

  由于定义空类的捷径是 Nil ,所以一种初始化新 List 的方法是把所有元素用 cons 操作符串起来, Nil 作为最后一个元素:

scala> val oneTwoThree = 1 :: 2 :: 3 :: Nil
oneTwoThree: List[Int] = List(1, 2, 3)

  要在最后用到 Nil 的理由是 :: 是定义在 List 类上的方法。如果只是写成1 :: 2 :: 3,由于3是Int类型,没有::方法,因此会导致编译失败。

  Scala 的 List 包装了很多有用的方法:

List() 或 Nil

空List

List("Cool", "tools", "rule)

创建带有三个值"Cool","tools"和"rule"的新List[String]

val thrill = "Will"::"fill"::"until"::Nil

创建带有三个值"Will","fill"和"until"的新List[String]

List("a", "b") ::: List("c", "d")

叠加两个列表(返回带"a","b","c"和"d"的新List[String])

thrill(2)

返回在thrill列表上索引为2(基于0)的元素(返回"until")

thrill.count(s => s.length == 4)

计算长度为4的String元素个数(返回2)

thrill.drop(2)

返回去掉前2个元素的thrill列表(返回List("until"))

thrill.dropRight(2)

返回去掉后2个元素的thrill列表(返回List("Will"))

thrill.exists(s => s == "until")

判断是否有值为"until"的字串元素在thrill里(返回true)

thrill.filter(s => s.length == 4)

依次返回所有长度为4的元素组成的列表(返回List("Will", "fill"))

thrill.forall(s => s.endsWith("1"))

辨别是否thrill列表里所有元素都以"l"结尾(返回true)

thrill.foreach(s => print(s))

对thrill列表每个字串执行print语句("Willfilluntil")

thrill.foreach(print)

与前相同,不过更简洁(同上)

thrill.head

返回thrill列表的第一个元素(返回"Will")

thrill.init

返回thrill列表除最后一个以外其他元素组成的列表(返回List("Will", "fill"))

thrill.isEmpty

说明thrill列表是否为空(返回false)

thrill.last

返回thrill列表的最后一个元素(返回"until")

thrill.length

返回thrill列表的元素数量(返回3)

thrill.map(s => s + "y")

返回由thrill列表里每一个String元素都加了"y"构成的列表(返回List("Willy", "filly", "untily"))

thrill.mkString(", ")

用列表的元素创建字串(返回"will, fill, until")

thrill.remove(s => s.length == 4)

返回去除了thrill列表中长度为4的元素后依次排列的元素列表(返回List("until"))

thrill.reverse

返回含有thrill列表的逆序元素的列表(返回List("until", "fill", "Will"))

thrill.sort((s, t) => s.charAt(0).toLowerCase < t.charAt(0).toLowerCase)

返回包括thrill列表所有元素,并且第一个字符小写按照字母顺序排列的列表(返回List("fill", "until", "Will"))

thrill.tail

返回除掉第一个元素的thrill列表(返回List("fill", "until"))

使用 Tuple

  另一种有用的容器对象是元组:tuple。与列表一样,元组也是不可变的,但与列表不同,元组可以包含不同类型的元素。而列表应该是 List[Int] 或 List[String] 的样子,元组可以同时拥有 Int 和 String 。实例化一个装有一些对象的新元组,只要把这些对象放在括号里,并用逗号分隔即可。一旦已经实例化了一个元组,可以用点号,下划线和一个基于1的元素索引访问它:

scala> val pair = (99, "Luftballons")
pair: (Int, String) = (99,Luftballons) scala> println(pair._1)
99 scala> println(pair._2)
Luftballons

  代码的第一行创建了元组,它的第一个元素是以99为值的 Int ,第二个是 "luftballons" 为值的 String 。 Scala 推断元组类型为 Tuple2[Int, String] ,并把它赋给变量 pair 。第二行访问 _1 字段,从而输出第一个元素 99 。第二行的“.”与用来访问字段或调用方法的点没有区别。元组的实际类型取决于它含有的元素数量和这些元素的类型。因此, (99, "Luftballons") 的类型是 Tuple2[Int, String] 。 ('u', 'r', 'the', 1, 4, "me") 是 Tuple6[Char, Char, String, Int, Int, String] 。

使用 Set 和 Map

  Scala 的集合类型库区分了集合类的可变和不可变。例如,数组始终是可变的,而列表始终不可变。Scala 对集和映射提供了可变和不可变的替代品,不过用了不同的办法。对于集和映射,Scala把可变性建模在类继承中。

  Scala 的 API 包含了集的一个基本特质: trait ,特质这个概念接近于 Java 的接口。 Scala 提供了两个子特质,一个是可变的集,另一个是不可变的集。如图所示:

Scala 编程(一)Scala 编程总览

  这三个特质都共享同样的简化名, Set 。然而它们的全称不一样,因为每个都放在不同的包里。 Scala 的 API 里具体的 Set 类,如 HashSet 类,扩展了要么是可变的,要么不可变的 Set 特质。因此,如果想要使用 HashSet ,可以根据需要选择可变的或不可变的变体。创造集的缺省方法如下:

scala> var jetSet = Set("Boeing", "Airbus")
jetSet: scala.collection.immutable.Set[String] = Set(Boeing, Airbus) scala> jetSet += "Lear"

  代码定义了名为 jetSet 的新 var ,并使用了包含 "Boeing" 和 "Airbus" 的不可变集完成了初始化。 Scala 中创建集的方法与创建列表和数组的类似:通过调用 Set 伴生对象的名为 apply 的方法,对 scala.collection.immutable.Set 的伴生对象调用了 apply 方法,返回了一个缺省的,不可变 Set 的实例。 Scala 编译器推断 jetSet 的类型为不可变 Set[String] 。要向集加入新的变量,可以在集上调用 + ,传入新的元素。可变的和不可变的集都提供了 + 方法,但它们的行为不同。可变集将把元素加入自身,不可变集将创建并返回一个包含了添加元素的新集。代码中使用的是不可变集,因此 + 调用将产生一个全新集。因此尽管可变集提供的实际上是 += 方法,不可变集却不是。 “jetSet += "Lear"” 实质上是下面写法的简写:

jetSet = jetSet + "Lear"

  因此代码用一个包含了"Boeing","Airbus"和"Lear"的新集重新赋值了 jetSet 这个 var 。如果需要可变集,就需要使用一个引用:import

scala> import scala.collection.mutable.Set
import scala.collection.mutable.Set scala> val movieSet = Set("Hitch", "Poltergeist")
movieSet: scala.collection.mutable.Set[String] = Set(Poltergeist, Hitch) scala> movieSet += "Shrek"
res7: movieSet.type = Set(Poltergeist, Shrek, Hitch) scala> println(movieSet)
Set(Poltergeist, Shrek, Hitch)

  代码引用了可变 Set 。就像 Java 那样,引用语句可以使用简单名,如 Set ,以替代更长的全标识名。结果,当在写 Set 的时候,编译器就知道是指 scala.collection.mutable.Set 。使用包含字串 "Hitch" 和   "Poltergeist" 的新可变集初始化了 movieSet 。下一行通过在集上调用 += 方法向集添加了 "Shrek" 。 += 是实际定义在可变集上的方法,可以替换掉 movieSet += "Shrek" 的写法,写成 movieSet.+=("Shrek") 。

  通过可变和不可变的 Set 方法制造的缺省的集实现能满足大多数情况,如果想要显式的集类,只要引用需要的类:

scala> import scala.collection.immutable.HashSet
import scala.collection.immutable.HashSet scala> val hashSet = HashSet("Tomatoes", "Chilies")
hashSet: scala.collection.immutable.HashSet[String] = Set(Chilies, Tomatoes) scala> println(hashSet + "Coriander")
Set(Chilies, Tomatoes, Coriander)

  Map 是 Scala 里另一种有用的集合类。和集一样, Scala 采用了类继承机制提供了可变的和不可变的两种版本的 Map , Map的类继承机制看上去和Set的很像:Scala 编程(一)Scala 编程总览

  scala.collection 包里面有一个基础 Map 特质和两个子特质 Map :可变的 Map 在 scala.collection.mutable 里,不可变的在 scala.collection.immutable 里。可以使用与那些用在数组,列表和集中的一样的方法去创造和初始化映射:

scala> import scala.collection.mutable.Map
import scala.collection.mutable.Map scala> val treasureMap = Map[Int, String]()
treasureMap: scala.collection.mutable.Map[Int,String] = Map() scala> treasureMap += (1 -> "Go to island.")
res20: treasureMap.type = Map(1 -> Go to island.) scala> treasureMap += (2 -> "Find big X on ground.")
res21: treasureMap.type = Map(2 -> Find big X on ground., 1 -> Go to island.) scala> treasureMap += (3 -> "Dig.")
res22: treasureMap.type = Map(2 -> Find big X on ground., 1 -> Go to island., 3 -> Dig.) scala> println(treasureMap(2))
Find big X on ground.

  代码引用了可变形式的 Map 。然后定义了一个叫做 treasureMap 的 val 并使用空的包含整数键和字串值的可变 Map 初始化它。映射为空是因为没有向方法传递任何值。 “[Int, String]” ,对代码来说是必须的,因为没有任何值被传递给方法,编译器无法推断映射的类型参数。相对的,如过使用:

scala> val romanNumeral = Map( 1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V" )
romanNumeral: scala.collection.mutable.Map[Int,String] = Map(2 -> II, 5 -> V, 4 -> IV, 1 -> I, 3 -> III) scala> println(romanNumeral(4))
IV

  译器可以从传递给映射的工厂方法的值推断参数类型,就不需要显式类型参数了。 -> 和 += 方法把键/值对添加到 Map 里。 Scala 编译器把如 1 -> "Go to island" 这样的二元操作符表达式转换为 (1).->("Go to island.") 。因此,当输入 1 -> "Go to island." ,实际上是在值为 1 的 Int 上调用 -> 方法,并传入值为 "Go to island." 的 String 。这个 -> 方法可以调用 Scala 程序里的任何对象,并返回一个包含键和值的二元元组。

  如果没有引用,在代码提及Map时,会得到缺省的映射: scala.collection.immutable.Map 。传给方法的键/值元组会返回包含这些传入的键/值对的不可变Map。