Scala学习(一)

来源:互联网 发布:windows with jre 编辑:程序博客网 时间:2024/06/05 02:31


一、Scala论断

Scala可以通过让你提升你设计和使用的接口的抽象级别来帮助你管理复杂性。例如,假设你有一个String变量name,你想弄清楚是否String包含一个大写字符。

val nameHasUpperCase = name.exists(_.isUpperCase)

Java代码把字串看作循环中逐字符步进的低层级实体。Scala代码把同样的字串当作能用论断:predicate查询的字符高层级序列。明显Scala代码更短并且——对训练有素的眼睛来说——比Java代码更容易懂。因此Scala代码在通盘复杂度预算上能极度地变轻。它也更少给你机会犯错。

论断,_.isUpperCase,是一个Scala里面函数式文本的例子。[1]它描述了带一个字符参量(用下划线字符代表)的函数,并测试其是否为大写字母。[2]


[1] 返回类型为Boolean的函数式文本被称作论断。

[2] 这种使用下划线作为参数占位符的做法会在8.5节中描述。

二、Scala变量

Scala有两种变量,val和var。

val类似于Java里的final变量。一旦初始化了,val就不能再赋值了。

与之对应的,var如同Java里面的非final变量。var可以在它生命周期中被多次赋值。

三、函数

现在你已经用过了Scala的变量,或许想写点儿函数。下面是在Scala里面的做法:

scala> def max(x: Int, y: Int): Int = {

if (x > y) x

else y

}

max: (Int,Int)Int

函数的定义用def开始。函数名,本例中是max,跟着是括号里带有冒号分隔的参数列表。每个函数参数后面必须带前缀冒号的类型标注,因为Scala编译器(还有解释器,但之后我们将只说编译器)没办法推断函数参数类型。本例中,名叫max的函数带有两个参数,x和y,都是Int类型。在max参数列表的括号之后你会看到另一个“: Int”类型标注。这个东西定义了max函数的结果类型:result type。[1]跟在函数结果类型之后的是一个等号和一对包含了函数体的大括号。本例中,函数体里仅有一个if表达式,选择x或者y,哪个较大,就当作max函数的结果。就像这里演示的,Scala的if表达式可以像Java的三元操作符那样产生一个值。举例来说,Scala表达式“if (x > y) x else y”与Java里的“(x > y) ? x : y”表现得很像。在函数体前的等号提示我们函数式编程的世界观里,函数定义一个能产生值的表达式。函数的基本结构在图2.1里面演示。

clip_image001

图释2.1 Scala函数的基本构成

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

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


[1] 在Java里,从方法里返回的值的类型被称为返回类型。在Scala里,同样的概念被叫做结果类型

[2] 如果一个方法调用自身,就称为递归。

[3] 尽管如此,就算编译器不需要,显式说明函数结果类型也经常是个好主意,这种类型标注可以使代码便于阅读,因为读者不用研究了函数体之后再去猜结果类型。

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

scala> def greet() = println("Hello, world!")

greet: ()Unit

当你定义了greet()函数,解释器会回应一个greet: ()Unit。“greet”当然是函数名。空白的括号说明函数不带参数。Unit是greet的结果类型。Unit的结果类型指的是函数没有返回有用的值。Scala的Unit类型比较接近Java的void类型,而且实际上Java里每一个返回void的方法都被映射为Scala里返回Unit的方法。因此结果类型为Unit的方法,仅仅是为了它们的副作用而运行。在greet()的例子里,副作用是在标准输出上打印一句客气的助词。

下一步,你将把Scala代码放在一个文件中并作为脚本执行它。如果你想离开解释器,输入:quit或者:q。

scala> :quit

$

四、函数文本

var i = 0while (i < args.length) {  println(args(i))  i += 1}

注意Scala和Java一样,必须把while或if的布尔表达式放在括号里。(换句话说,就是不能像在Ruby里面那样在Scala里这么写:if i < 10。在Scala里必须写成if (i < 10)。)另外一点与Java类似的,是如果代码块仅有一个句子,大括号就是可选的。

函数式语言的一个主要特征是,函数是第一类结构,这在Scala里千真万确。举例来说,另一种(简洁得多)打印每一个命令行参数的方法是:

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

这行代码中,你在args上调用foreach方法,并把它传入函数。此例中,你传入了带有一个叫做arg参数的函数文本:function literal。函数体是println(arg)。如果你把上述代码输入到新文件pa.scala,并使用命令执行:

$ scala pa.scala Concise is nice

你会看到:

Concise

is

nice

前例中,Scala解释器推断arg的类型是String,因为String是你调用foreach的那个数组的元素类型。如果你喜欢更显式的,你可以加上类型名,不过如此的话你要把参数部分包裹在括号里(总之这是语法的普通形式):

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

运行这个脚本的结果与前一个相同。

如果你更喜欢简洁的而不是显式的风格,就可以充分体会到Scala特别简洁的优越性。如果函数文本由带一个参数的一句话组成,你都不需要显式命名和指定参数。[1]这样,下面的代码同样有效:

args.foreach(println)

总而言之,函数文本的语法就是,括号里的命名参数列表,右箭头,然后是函数体。语法演示在图2.2中。

clip_image001[5]

图释2.2 Scala函数文本的语法

现在,到这里你或许想知道那些你在指令式语言如Java或C里那么信任的for循环到哪里去了呢。为了努力引导你向函数式的方向,Scala里只有一个指令式for(称为for表达式:expression)的函数式近似。目前你还看不到他们全部的力量和表达方式,直到你读到了(或者先瞄一眼)第7.3节,我们仅仅带您在这里领略一下。创建一个新文件forargs.scala,输入以下代码:

for (arg <- args)

println(arg)

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

如果执行forargs.scala脚本:

$ scala forargs.scala for arg in args

可以看到:

for

arg

in

args


[1] 这种简写被称为偏应用函数:partially applied function,将在8.6节里描述。

[2] 你可以认为<-符号代表“其中”。如果要读for(arg<-args),就读做“对于args中的arg”。

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

当对带有括号并包括一到若干参数的变量赋值时,编译器将把它转化为对带有括号里参数和等号右边的对象的update方法的调用。例如,

greetStrings(0) = "Hello"

将被转化为

greetStrings.update(0, "Hello")

因此,下列Scala代码与你在代码3.1里的代码语义一致:

val greetStrings = new Array[String](3)

greetStrings.update(0, "Hello")

greetStrings.update(1, ", ")

greetStrings.update(2, "world!\n")

for (i <- 0.to(2))

print(greetStrings.apply(i))

五、使用Tuple,Set或Map

另一种有用的容器对象是元组:tuple。与列表一样,元组也是不可变的,但与列表不同,元组可以包含不同类型的元素。而列表应该是List[Int]或List[String]的样子,元组可以同时拥有Int和String。元组很有用,比方说,如果你需要在方法里返回多个对象。Java里你将经常创建一个JavaBean样子的类去装多个返回值,Scala里你可以简单地返回一个元组。而且这么做的确简单:实例化一个装有一些对象的新元组,只要把这些对象放在括号里,并用逗号分隔即可。一旦你已经实例化了一个元组,你可以用点号,下划线和一个基于1的元素索引访问它。代码3.4展示了一个例子:

val pair = (99, "Luftballons")

println(pair._1)

println(pair._2)

代码 3.4 创造和使用元组

代码3.4的第一行,你创建了元组,它的第一个元素是以99为值的Int,第二个是"luftballons"为值的String。Scala推断元组类型为Tuple2[Int, String],并把它赋给变量pair。第二行,你访问_1字段,从而输出第一个元素,99。第二行的这个“.”与你用来访问字段或调用方法的点没有区别。本例中你正用来访问名叫_1的字段。如果执行这个脚本,你能看到:

99

Luftballons

元组的实际类型取决于它含有的元素数量和这些元素的类型。因此,(99, "Luftballons")的类型是Tuple2[Int, String]。('u', 'r', 'the', 1, 4, "me")是Tuple6[Char, Char, String, Int, Int, String]。[1]

访问元组的元素

你或许想知道为什么你不能像访问List里的元素那样访问元组的,就像pair(0)。那是因为List的apply方法始终返回同样的类型,但是元组里的或许类型不同。_1可以有一个结果类型,_2是另外一个,诸如此类。这些_N数字是基于1的,而不是基于0的,因为对于拥有静态类型元组的其他语言,如Haskell和ML,从1开始是传统的设定。


[1] 尽管理论上你可以创建任意长度的元组,然而当前Scala库仅支持到Tupe22。

val romanNumeral = Map(

1 -> "I", 2 -> "II", 3 -> "III", 4 -> "IV", 5 -> "V"

)

println(romanNumeral(4))

 

六、学习识别函数式风格

第1章里提到过,Scala允许你用指令式风格编程,但是鼓励你采用一种更函数式的风格。如果你是从指令式的背景转到Scala来的——例如,如果你是Java程序员——那么学习Scala是你有可能面对的主要挑战就是理解怎样用函数式的风格编程。我们明白这种转变会很困难,在本书中我们将竭尽所能把你向这方面引导。不过这也需要你这方面的一些工作,我们鼓励你付出努力。如果你来自于指令式的背景,我们相信学习用函数式风格编程将不仅让你变成更好的Scala程序员,而且还能拓展你的视野并使你变成通常意义上好的程序员。

通向更函数式风格路上的第一步是识别这两种风格在代码上的差异。其中的一点蛛丝马迹就是,如果代码包含了任何var变量,那它大概就是指令式的风格如果代码根本就没有var——就是说仅仅包含val——那它大概是函数式的风格。因此向函数式风格推进的一个方式,就是尝试不用任何var编程

如果你来自于指令式的背景,如Java,C++,或者C#,你或许认为var是很正统的变量而val是一种特殊类型的变量。相反,如果你来自于函数式背景,如Haskell,OCamel,或Erlang,你或许认为val是一种正统的变量而var有亵渎神灵的血统。然而在Scala看来,val和var只不过是你工具箱里两种不同的工具。它们都很有用,没有一个天生是魔鬼。Scala鼓励你学习val,但也不会责怪你对给定的工作选择最有效的工具。尽管或许你同意这种平衡的哲学,你或许仍然发现第一次理解如何从你的代码中去掉var是很挑战的事情。

考虑下面这个改自于第2章的while循环例子,它使用了var并因此属于指令式风格:

def printArgs(args: Array[String]): Unit = {var i = 0while (i < args.length) {println(args(i))i += 1}}

你可以通过去掉var的办法把这个代码变得更函数式风格,例如,像这样:

def printArgs(args: Array[String]): Unit = {for (arg <- args)println(arg)}或这样:def printArgs(args: Array[String]): Unit = {args.foreach(println)}

这个例子演示了减少使用var的一个好处。重构后(更函数式)的代码比原来(更指令式)的代码更简洁,明白,也更少机会犯错。Scala鼓励函数式风格的原因,实际上也就是因为函数式风格可以帮助你写出更易读懂,更不容易犯错的代码。

当然,你可以走得更远。重构后的printArgs方法并不是函数式的,因为它有副作用——本例中,其副作用是打印到标准输出流。函数有副作用的马脚就是结果类型为Unit。如果某个函数不返回任何有用的值,就是说其结果类型为Unit,那么那个函数唯一能让世界有点儿变化的办法就是通过某种副作用。更函数式的方式应该是定义对需打印的arg进行格式化的方法,但是仅返回格式化之后的字串,如代码3.9所示:

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

代码 3.9 没有副作用或var的函数

现在才是真正函数式风格的了:满眼看不到副作用或者var。能在任何可枚举的集合类型(包括数组,列表,集和映射)上调用的mkString方法,返回由每个数组元素调用toString产生结果组成的字串,以传入字串间隔。因此如果args包含了三个元素,"zero","one"和"two",formatArgs将返回"zero\none\ntwo"。当然,这个函数并不像printArgs方法那样实际打印输出,但可以简单地把它的结果传递给println来实现:

println(formatArgs(args))

每个有用的程序都可能有某种形式的副作用,因为否则就不可能对外部世界提供什么值。偏好于无副作用的方法可以鼓励你设计副作用代码最少化了的程序。这种方式的好处之一是可以有助于使你的程序更容易测试。举例来说,要测试本节之前给出三段printArgs方法的任一个,你将需要重定义println,捕获传递给它的输出,并确信这是你希望的。相反,你可以通过检查结果来测试formatArgs:

val res = formatArgs(Array("zero", "one", "two"))

assert(res == "zero\none\ntwo")

Scala的assert方法检查传入的Boolean并且如果是假,抛出AssertionError。如果传入的Boolean是真,assert只是静静地返回。你将在第十四章学习更多关于断言和测试的东西。

虽如此说,不过请牢记在心:不管是var还是副作用都不是天生邪恶的。Scala不是强迫你用函数式风格编任何东西的纯函数式语言。它是一种指令式/函数式混合的语言。你或许发现在某些情况下指令式风格更符合你手中的问题,在这时候你不应该对使用它犹豫不决。然而,为了帮助你学习如何不使用var编程,在第7章中我们会给你看许多有var的特殊代码例子和如何把这些var转换为val。

Scala里方法参数的一个重要特征是它们都是val,不是var。[1]如果你想在方法里面给参数重新赋值,结果是编译失败:

def add(b: Byte): Unit = {

b += 1 // 编译不过,因为b是val

sum += b

}


[1] 参数是val的理由是val更容易讲清楚。你不需要多看代码以确定是否val被重新赋值,而var则不然。

如果没有发现任何显式的返回语句,Scala方法将返回方法中最后一个计算得到的值

假如某个方法仅计算单个结果表达式,则可以去掉大括号。如果结果表达式很短,甚至可以把它放在def同一行里。这样改动之后,类ChecksumAccumulator看上去像这样:

class ChecksumAccumulator {private var sum = 0def add(b: Byte): Unit = sum += bdef checksum(): Int = ~(sum & 0xFF) + 1}

像ChecksumAccumulator的add方法那样的结果类型为Unit的方法,执行的目的就是它的副作用。通常我们定义副作用为在方法外部某处改变状态或者执行I/O活动。比方说,在add这个例子里,副作用就是sum被重新赋值了。表达这个方法的另一种方式是去掉结果类型和等号,把方法体放在大括号里。这种形式下,方法看上去很像过程:procedure,一种仅为了副作用而执行的方法。代码4.1的add方法里演示了这种风格:

// 文件ChecksumAccumulator.scala

class ChecksumAccumulator {

private var sum = 0

def add(b: Byte) { sum += b }

def checksum(): Int = ~(sum & 0xFF) + 1

}

代码 4.1 类ChecksumAccumulator的最终版

应该注意到令人困惑的地方是当你去掉方法体前面的等号时,它的结果类型将注定是Unit。不论方法体里面包含什么都不例外,因为Scala编译器可以把任何类型转换为Unit。例如,如果方法的最后结果是String,但方法的结果类型被声明为Unit,那么String将被转变为Unit并失去它的值。下面是这个例子:

scala> def f(): Unit = "this String gets lost"

f: ()Unit

例子里,String被转变为Unit因为Unit是函数f声明的结果类型。Scala编译器会把一个以过程风格定义的方法,就是说,带有大括号但没有等号的,在本质上当作是显式定义结果类型为Unit的方法。例如:

scala> def g() { "this String gets lost too" }

g: ()Unit

因此,如果你本想返回一个非Unit的值,却忘记了等号时,那么困惑就出现了。所以为了得到你想要的结果,你需要插入等号

scala> def h() = { "this String gets returned!" }

h: ()java.lang.String

scala> h

res0: java.lang.String = this String gets returned!

七、 分号推断

Scala程序里,语句末尾的分号通常是可选的。如果你愿意可以输入一个,但若一行里仅有一个语句也可不写。另一方面,如果一行里写多个语句那么分号是需要的:

val s = "hello"; println(s)

如果你想输入一个跨越多行的语句,多数时候你只需输入,Scala将在正确的位置分隔语句。例如,下面的代码被认为是一个跨四行的语句:

if (x < 2)

println("too small")

else

println("ok")

然而,偶尔Scala也许没有按照你的愿望把句子分割成两部分:

x

+ y

这会被分成两个语句x和+ y。如果你希望把它作为一个语句x + y,你可以把它包裹在括号里:

(x

+ y)

或者,你也可以把+放在行末。正是由于这个原因,当你在串接类似于+的中缀操作符,把操作符放在行尾而不是行头是普遍的Scala风格:

x +

y +

z

八、 Singleton对象

如第1章所提到的,Scala比Java更面向对象的一个方面是Scala没有静态成员。替代品是,Scala有单例对象:singleton object。除了用object关键字替换了class关键字以外,单例对象的定义看上去就像是类定义。代码4.2展示了一个例子:

// 文件ChecksumAccumulator.scalaimport scala.collection.mutable.Mapobject ChecksumAccumulator {private val cache = Map[String, Int]()def calculate(s: String): Int =if (cache.contains(s))cache(s)else {val acc = new ChecksumAccumulatorfor (c <- s)acc.add(c.toByte)val cs = acc.checksum()cache += (s -> cs)cs}}代码 4.2 类ChecksumAccumulator的伴生对象

表中的单例对象被叫做ChecksumAccumulator,与前一个例子里的类同名。当单例对象与某个类共享同一个名称时,他被称作是这个类的伴生对象:companion object。你必须在同一个源文件里定义类和它的伴生对象。类被称为是这个单例对象的伴生类:companion class。类和它的伴生对象可以互相访问其私有成员。

ChecksumAccumulator单例对象有一个方法,calculate,用来计算所带的String参数中字符的校验和。它还有一个私有字段,cache,一个缓存之前计算过的校验和的可变映射。[1]方法的第一行,“if (cache.contains(s))”,检查缓存,看看是否传递进来的字串已经作为键存在于映射当中。如果是,就仅仅返回映射的值,“cache(s)”。否则,执行else子句,计算校验和。else子句的第一行定义了一个叫acc的val并用新建的ChecksumAccumulator实例初始化它。[2]下一行是个for表达式,对传入字串的每个字符循环一次,并在其上调用toByte把字符转换成Byte,然后传递给acc所指的ChecksumAccumulator实例的add方法。完成了for表达式后,下一行的方法在acc上调用checksum,获得传入字串的校验和,并存入叫做cs的val。下一行,“cache += (s -> cs)”,传入的字串键映射到整数的校验和值,并把这个键-值对加入cache映射。方法的最后一个表达式,“cs”,保证了校验和为此方法的结果

如果你是Java程序员,考虑单例对象的一种方式是把它当作是或许你在Java中写过的任何静态方法之家。可以在单例对象上用类似的语法调用方法:单例对象名,点,方法名。例如,可以如下方式调用ChecksumAccumulator单例对象的calculate方法:

ChecksumAccumulator.calculate("Every value is an object.")

然而单例对象不只是静态方法的收容站。它同样是个第一类的对象。因此你可以把单例对象的名字看作是贴在对象上的“名签”:

clip_image001[7]

定义单例对象不是定义类型(在Scala的抽象层次上说)。如果只是ChecksumAccumulator对象的定义,你就建不了ChecksumAccumulator类型的变量。宁愿这么说,ChecksumAccumulator类型是由单例对象的伴生类定义的。然而,单例对象扩展了超类并可以混入特质。由于每个单例对象都是超类的实例并混入了特质,你可以通过这些类型调用它的方法,用这些类型的变量指代它,并把它传递给需要这些类型的方法。我们将在第十二章展示一些继承自类和特质的单例对象的例子。

类和单例对象间的一个差别是,单例对象不带参数,而类可以。因为你不能用new关键字实例化一个单例对象,你没机会传递给它参数。每个单例对象都被作为由一个静态变量指向的虚构类:synthetic class的一个实例来实现,因此它们与Java静态类有着相同的初始化语法。[3]特别要指出的是,单例对象会在第一次被访问的时候初始化。

不与伴生类共享名称的单例对象被称为孤立对象:standalone object。由于很多种原因你会用到它,包括把相关的功能方法收集在一起,或定义一个Scala应用的入口点。下一段会说明这个用例。


[1] 这里我们使用了缓存例子来说明带有域的单例对象。像这样的缓存是通过内存换计算时间的方式做到性能的优化。通常意义上说,只有遇到了缓存能解决的性能问题时,才可能用到这样的例子,而且应该使用弱映射(weak map),如scala.Collection.jcl的WeakHashMap,这样如果内存稀缺的话,缓存里的条目就会被垃圾回收机制回收掉。

[2] 因为关键字new只用来实例化类,所以这里创造的新对象是ChecksumAccumulator类的一个实例,而不是同名的单例对象。

[3] 虚构类的名字是对象名加上一个美元符号。因此单例对象ChecksumAccumulator的虚构类是ChecksumAccumulator$。

九、 Application特质

Scala提供了一个特质,scala.Application,可以节省你一些手指的输入工作。尽管我们还没有完全提供给你去搞明白它如何工作的所有需要知道的东西,不过我们还是认为你可能想要知道它。代码4.4展示了一个例子:

import ChecksumAccumulator.calculateobject FallWinterSpringSummer extends Application {for (season <- List("fall", "winter", "spring"))println(season +": "+ calculate(season))}

代码 4.4 使用Application特质

使用这个特质的方法是,首先在你的单例对象名后面写上“extends Application” 。然后代之以main方法,你可以把想要放在main方法里的代码直接放在单例对象的大括号之间。就这么简单。之后可以像对其它程序那样编译和运行。

这种方式之所以能奏效是因为特质Application声明了带有合适的签名的main方法,并由你的单例对象继承,使它可以像个Scala程序那样用。大括号之间的代码被收集进了单例对象的主构造器:primary constructor,并在类被初始化时被执行。如果你不明白所有这些指的是什么也不用着急。之后的章节会解释这些,目前可以暂时不求甚解。

继承自Application比写个显式的main方法要短,不过它也有些缺点。首先,如果想访问命令行参数的话就不能用它,因为args数组不可访问。比如,因为Summer程序使用了命令行参数,所以它必须带有显式的main方法,如代码4.3所示。第二,因为某些JVM线程模型里的局限,如果你的程序是多线程的就需要显式的main方法。最后,某些JVM的实现没有优化被Application特质执行的对象的初始化代码。因此只有当你的程序相对简单和单线程情况下你才可以继承Application特质。