第二章:开始学习Scala

本章内容将学习Scala的类型,函数,for-comprehensions,模式匹配,以及其它方面内容。

注意模式匹配和for-comprehensions,是函数式编程概念。本章主要目的是先熟悉Scala语言的环境和一些基本的语法。一开始,会先从REPL简单环境入手。

在作进一步学习之前,先确保已经安装好Scala。现在,让我们先了了解Scala的解析器REPL。

Scala解析器REPL

Scala 解析器,指的是一个可以编写Scala表达式和程序的交互shell。要进入到Scala的交互模式,在命令提示符中输入scala即可。你将进入到Scala的命令提示符:

1
2
3
4
Welcome to Scala version 2.10.0.final (Java ...).
Type in expressions to have them evaluated.
Type :help for more information.
scala>

这表明Scala安装成功。在Scala提示符输入42,按回车,将看到:

1
2
scala> 42
res0: Int = 42

res0为Scala解析器创建的变量名,持有值42。如果再输入变量名,将再次得到相似的输出:

1
2
scala> res0
res1: Int = 42

带有这些步骤的,称之为 REPL(read-evaluate-print loop)。即你可以在Scala解析器内部重复的循环 读-执行-打印 步骤。现在再看看:

1
2
scala> println("Hello world")
Hello world

这里调用了println函数,以及传递参数"Hello World",并输出结果。

printlnscala.Console中的一个函数,该函数使用System.out.println()将消息输出在控制台上,Scala库中的Predef会将
Console.println映射为println,因此你可以不使用包名调用。

1
2
scala > val myList = new java.util.ArrayList[String]()
myList: java.util.ArrayList[String] = []

在Scala中可以直接调用Java对象和方法。

下面列出REPL的一些比较有用的选项:

Command Description
:help This command prints the help message with all the commands available in the Scala interpreter.
:cp Use this command to add a JAR file to the classpath for the Scala interpreter. For example, :cp tools/junit.jar will try to find a JUnit JAR file relative to your current location and, if found, it will add the JAR file to your classpath so that you can refer to the classes inside the JAR file.
:load or :l Allows you to load Scala files into the interpreter. If you want to investigate existing Scala code, you could load the file into the Scala interpreter, and all the definitions will be accessible to you.
:replay or :r Resets the interpreter and replays all the previous commands.
:quit or :q Exits the interpreter.
:type Displays the type of an expression without evaluating it. For example, :type 1 + 2 will determine the type of the expression to Int without performing the add operation.

Scala基础

接下来将介绍Scala的基础类型,包括String和值类型ByteShortIntLongFloatDoubleBoolean以及Char。同时将介绍Scala的两种变量类型varval。你将学习Scala函数,如何定义,如何调用。

基础类型

Scala的所有基础类型都是对象,它们定义在scala包下。

2种变量类型,9种基本数据类型

Value type Description and range
Byte 8位二进制,-128~127
Short 16位二进制,-32768~32767
Int 32位二进制,–2,147,483,648~2,147,483,647
Long 64位二进制,-9,223,372,036,854,775,808~,223,372,036,854,775,807
Float A single-precision 32-bit IEEE 754 floating point.
Double A double-precision 64-bit IEEE 754 floating point.
Boolean True False
Char 16位的Unicode字符,‘、u0000’~‘\uffff’

在Scala中,所有的基本数据类型的第一个字母都是大写的。在Scala的早期版本中既可以使用小写也可以使用首字母大写,
但从2.8版本开始,将不再支持小写。

注意 在Scala中是默认导入了java.langScala.Predef,在.Net环境中对应是system包。对象Predef定义了函数和类型
别名,由于Predef是自动导入的,你可以使用Predef中的所有成员和方法。

说明 你可以在REPL编译环境中通过命令:import查看所有被自动导入的包。

1
2
3
4
scala> :imports
1) import java.lang._ (153 types, 158 terms)
2) import scala._ (798 types, 806 terms)
3) import scala.Predef._ (16 types, 167 terms, 96 are implicit)

字面常量

整形

Byte,Short,Int,Long和Char这些基本类型被称为 整形。字面常量可以表示十进制、十六进制和八进制的数,这取决于
这些字面常量的定义方式。如果是0或非零的数字,则它是十进制的:

1
2
Scala > val decimal = 11235
decimal: Int = 11235

和Java一样,如果要声明为Long长整型,只需要在后面加上L或l,如:

1
2
scala> val decimal = 11235L
decimal: Long = 11235

类似地,如果要声明为十六进制和八进制则在数据前面加上前缀0x(十六进制)和0(八进制):

1
2
3
4
scala> val hexa = 0x23
hexa: Int = 35
scala> val octa = 023
hexa: Int = 19

值得注意的是,不管是十六进制还是八进制,Scala总是返回一个十进制,如果你可以通过类型声明的方式指定,如:

1
2
3
4
scala> val i = 1
i: Int = 1
scala> val i2: Byte = 1
i2: Byte = 1
浮点型

浮点型字面量一般包含整数部分和小数部分,小数点和小数部分通常是可选的,浮点型(Float)以F或f作后缀标记,否则就是
Double型的,如:

1
2
3
4
scala> val d = 0.0
d: Double = 0.0
scala> val f = 0.0f
f: Float = 0.0

Double型可以通过科学记数法表示:

1
2
scala>val exponent = 1e30
exponent: Double = 1.0E30

特列:由于浮点型数据可以以 1. 表示,在使用toString方法时,需要用空格分开,即1. toString。对于非字母开头的方法
则可以自动识别,如1.+1将返回2.0,这是一种不规范的写法,应该尽量避免使用。

字符字面量

字符字面量有两种表示形式,一种是使用Unicode编号,一种是直接显示,即:

1
2
3
4
scala> val capB = '\102'
capB: Char = B
scala> val capB = 'B'
capB: Char = B

因此,一些比较特殊的字符,比如换行和回车也可以表示:

1
2
scala> val new_line = '\n'
new_line: Char =
字符串字面量

字符串字面量表示方式和字符字面量一样,表示形式和Java语言一样,基本类型也是String。

1
2
scala> val bookName = "Scala in \"Action\""
bookName: java.lang.String = Scala in "Action"

同时也具有如python的换行等特性,如用连续三个引号来表示换行。

1
2
3
4
5
6
scala> val multiLine = """This is a
| multi line
| string"""
multiLine: java.lang.String =
This is a
multi line

新特性
从Scala 2.10开始支持字符串内插(String interpolation)写法,因此你可以这样写:

1
2
3
4
scala> val name = "Nilanjan"
name: String = Nilanjan
scala> s"My name $name"
res0: String = My name Nilanjan

在Scala内部将调用一个类StringContext来对字符串进行处理,以$${...}开头的字符token将被替换,头处理f会将匹配的字符进行格式化操作,如:

1
2
3
4
5
6
scala> val height = 1.9d
height: Double = 1.9
scala> val name = "James"
name: String = James
scala> println(f"$name%s is $height%2.2f meters tall")
James is 1.90 meters tall

RichString vs. StringLike
如果你使用之前的版本,你将使用scala.RichString来为字符串对象提供额外的方法,但从Scala2.8开始被叫做scala.collection.immutable.StringLike,因为字符串是一个字符集合,它将把字符串视作一个不可变的容器(immutable collection)。相对地,Rich的封装基本类还有RichIntRichBooleanRichDouble等等。

XML字面量

传统地,使用XML意味着需要第三方解析器或者库,但在Scala中属于语言的一部分,Scala支持XML字面量,因此,可以直接输入:

1
2
3
4
5
6
7
8
9
val book = <book>
<title>Scala in Action</title>
<author>Nilanjan Raychaudhuri</author>
</book>
book: scala.xml.Elem =
<book>
<title>Scala in Action</title>
<author>Nilanjan Raychaudhuri</author>
</book>

Scala将XML字面量转换为scala.xml.Elem类型对象,不仅仅如此,你可以使用{}在XML中传递参数和变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
scala> val message = "I didn't know xml could be so much fun"
scala> val code = “1”
scala> val alert = <alert>
<message priority={code}>{message}</message>
<date>{new java.util.Date()}</date>
</alert>
alert: scala.xml.Elem =
<alert>
<message priority=”1”>
I didn't know xml could be so much fun
</message>
<date>Fri Feb 19 19:18:08 EST 2010</date>
</alert>

像这样代码定义在{}里面的,在Scala中称之为 Scala代码块(Scala code blocks)。顾名思义,我们可以在{}内写入程序代
码。值得注意的是,在{}外不要使用“”双引号,否则将解析成字符串。

变量

Scala中变量的声明有两种方式:valvar
val 是一个赋值变量,有时又叫 值(value),表示一旦初始化就不能被修改和指定到其他值(类似于Java的final)。
var 是可再分配的(reassignable),你可以在其初始化后任意更改变量的值。

1
2
3
4
5
6
7
8
9
10
scala> val constant = 87
constant: Int = 87
scala> constant = 88
<console>:5: error: reassignment to val
constant = 88
^
scala> var variable = 87
variable: Int = 87
scala> variable = 88
variable: Int = 88

尽管Scala解析器在变量类型上实现和很好的机制,但有时需要对变量声明指定的类型,我们可以通过 变量:【类型】的形式进行指定。当你声明一个变量而又不明确该变量的值时,你可以使用占位符(_)表示默认类型

1
2
scala> var willKnowLater:String = _
willKnowLater: String = null

因为String的默认类型为null,在该例子中willKnowLater的值就是null。特别声明,不管是var还是val都要为其指定值或_占位符(val不能通过占位符_表示值,因为val声明的是一个确切的不能被改变的值,本地方法或语句块内的var变量不能
使用占位符_),只有变量声明在类内部时才不用指定值。

Lazy 变量

由于变量在var或val声明时便被赋值,但你又不想在一些耗时的操作中声明该变量的值,为了改变这种行为,可以使用lazy val

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> lazy val forLater = someTimeConsumingOperation()
forLater: Unit = <lazy>

表示forLater不在绑定变量后立即求值,
而是在someTimeConsumingOperation()被调用时,才进行求值,如

scala> var a = 1
a: Int = 1
scala> lazy val b = a + 1
b: Int = <lazy>
scala> a = 5
a: Int = 5
scala> b
res1: Int = 6

变量b在最后一步被调用,但是原来的a已经由1变成了5,所有b最后计算的6

注意 lazy关键字只能跟随val,不能声明lazy var,因为只有val是不可变的,这意味着通过lazy val声明的变量将在第
一次被调用时产生一个确切的不可变的值。

模式匹配

我们还可以通过模式匹配来进行声明变量,如

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

List在Scala中是一个队列容器,模式匹配中左边的first将匹配到List中的第一个元素,最后一个rest匹配剩余的元素,符号::是一个定义在List中的方法,模式匹配将在本章着重讨论。

记住,声明变量时习惯上将其用val声明,而在实际需要是再将其更改为var,以避免出错。

函数

函数结构定义

函数在Scala中以语句块形式表示,在本小节我们将着重探讨。
在Scala中,函数的定义形式为:

def 方法名(方法参数) 可选返回类型 = 方法主体

在函数中,使用:从返回类型中分割参数列表;多个参数使用,隔开;使用=将方法声明部分和方法主体部分隔开。

1
2
scala> def myFirstMethod():String = { "exciting times ahead" }
myFirstMethod: ()String

在该部分,方法返回类型是可选的,因此上面也可以这样声明

1
2
3
4
scala> def myFirstMethod() = { "exciting times ahead" }
myFirstMethod: ()java.lang.String
scala> myFirstMethod()
res6: java.lang.String = exciting times ahead

在方法中=的作用不仅仅用于区分方法声明和方法主体,另一个作用是告诉Scala编译器,应该返回什么类型的数据。如果
省略了=号,Scala将不返回任何东西,如

1
2
3
scala> def myFirstMethod(){ "exciting times ahead" }
myFirstMethod: ()Unit
scala> myFirstMethod()

以上面为例,在REPL环境中,返回类型不再是java.lang.String,而已UnitUnit即相当于Java中的void,就是表示一个
方法不返回任何东西。

对于一般比较简单的方法,你可以直接去掉大括号

1
2
scala> def myFirstMethod() = "exciting times ahead"
myFirstMethod: ()java.lang.String

像这些不包含复杂结构的,或不进行参数传递的,可以直接省略掉{}大括号。甚至,如果不包含参数,可以直接将小括号去掉()

1
2
scala> def myFirstMethod = "exciting times ahead"
myFirstMethod: java.lang.String

输出结果形式更像是一个声明,因此这也意味着你可以用这种def的方法声明形式代替valvar

1
2
scala> myFirstMethod
res17: java.lang.String = exciting times ahead

明显,调用这种形式的方法和调用var和val是一样的。

函数类型

回到函数参数,下面提供一个函数max传递了两个参数,并返回一个最大的

1
2
3
4
5
6
scala> def max(a: Int, b: Int) = if(a > b) a else b
max: (a: Int,b: Int)Int
scala> max(5, 4)
res8: Int = 5
scala> max(5, 7)
res9: Int = 7

在Scala中,return是可选的,在函数中你可以不用使用return关键,它将返回的是最后一个表达式的值。在上面的例子中,if
else语句中的表达式部分为ab,又因为ab都是最后一个表达式,因此,当if条件为true时返回a,否则返回b

尽管return是可选的,你也不用在函数定义的时候声明函数的参数类型。Scala是在函数调用时推断出函数的返回类型,而不是在函数声明的时候12

类型推断

如果你有诸如Haskell,OCaml等类型推断编程语言的使用背景,你会对Scala参数定义的方式感到怪异。原因是,Scala不使用Hindley-Milner算法进行类型推断;实际上Scala的类型推断是基于本地声明的信息,也就是本地类类型推断。类型推断超出本书的范围,你可以阅读更多有关于Hindley-Milner类型推断算法的资料以及为什么它是有用的。

参数化类型 (parameterized type)

有时我们要通过List作为参数传入一个函数中,但是不能确定List参数传入的是Int还是String,面对这种情况,Scala中提供了一个参数化类型,可以使得在函数调用的时候在决定函数参数的类型。

1
2
3
4
5
6
scala> def toList[A](value:A) = List(value)
toList: [A](value: A)List[A]
scala> toList(1)
res16: List[Int] = List(1)
scala> toList("Scala rocks")
res15: List[java.lang.String] = List(Scala rocks)

在声明函数时,标识未知的参数类型为A。当toList方法被调用时,A将被传入的参数的类型代替,由于List在Scala中也使用了参数化类型,所以将返回实际参数的类型。

注意:如果你是一个Java编程员,你会发现Java的泛型和Scala的参数化类型十分相似。不同的是,Java是使用(<>)而Scala则是使用([])。另外稍微不同的是Scala通常是使用A,B,C… Z标识,而Java通常是T,K,V和E。

函数字面量

在Scala中,你把函数作为参数传递给另一个函数,这种把函数作为参数传递的宽松机制被称为闭包(closure)(函数作参数传递不总是闭包,这会在第四节介绍)。Scala提供了一个简便的机制创建函数,就是你只需要写函数主体,这种形式叫做函数字面量。

1
2
scala> val evenNumbers = List(2, 4, 6, 8, 10)
evenNumbers: List[Int] = List(2, 4, 6, 8, 10)

我们可以使用List中的foldLeft方法计算list中的和。foldLeft方法传递两个参数,一个初始化值和一个二元运算。二元运算提供了初始化值和list中的所有元素。二元运算将作为一个函数,这个函数包含了两个参数用于自身操作,如

1
2
scala> evenNumbers.foldLeft(0) { (a: Int, b:Int) => a + b }
res19: Int = 30

如上例子,第一个参数为(0)表示初始化值,第二个参数为{ (a: Int, b:Int) => a + b },它是一个二元运算,在本例中,它也是一个匿名函数,或称为一个没有预定义函数名的函数。由于在Scala中可以使用类型推断,因此下面形式是等价的

1
2
scala> evenNumbers.foldLeft(0) { (a, b) => a + b }
res20: Int = 30

分析:在Scala中,foldLeft方法定义方式为def foldLeft[B](z: B)(f: (B, A) => B): B,其中[B]为参数化,(z: B)为第一个参数,B是要返回的对象,(f: (B, A) => B)为第二个参数,它是一个二元运算,他表示将(B,A)得到的结果赋予给B,用数学公式表示就是 y = f(x,y)。在本例中就是b = a + b,实际上就是个累加器。

Scala允许你对匿名函数做更进一步的操作:你可以去掉匿名函数的参数部分,仅仅保留方法主体,这种情况下,匿名函数的参数要用占位符_代替。下划线在Scala中有特殊意义,在本例中下划线就相当于占位符,因此,下面写法也是等价的

1
2
scala> evenNumbers.foldLeft(0) { _ + _ }
res21: Int = 30

下划线在Scala中用得比较多,占位符_可以用在任何地方,具体表示什么值取决于所在的上下文,并且是在其被真正调用的时候才决定其值。函数字面量是Scala中的习惯用法,在Scala基础代码和库中会频繁出现。
回到第一章的例子我们就可以明白,占位符代表了exists方法中的参数

1
val hasUpperCase = name.exists(_.isUpper)

使用Scala闭包和第一类函数

闭包就是在它被定义的环境中封装起来的方法。例如,闭包会跟踪函数内部指向函数外部的变量的变化。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
注意:闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。
这里要说明,Scala中没有break或continue,也不是保留字。Scala是一门可扩展的语言,因此,你可以扩展来达到break的作用。我们可以通过Scala的异常处理机制来实现break。因为,抛出一个异常会是执行队列停止,使得catch语句块只执行到这之前,因为return在Scala中是多余的,Scala总是返回最后一个语句。我们通过声明异常,就可以达到模拟break的实现,如我们定义一个抛出异常的break方法

1
def break = new RuntimeException("break exception")

下面我们以此创建一个闭包函数

1
def breakable(op: => Unit) { ... }

这里的op: => Unit是什么?右箭头=>表示函数breakable中要求一个函数作为参数;箭头右边的为breakable函数的返回类型(——本例中Unit相当于Java中的void);op为参数名。因为在右箭头(=>)左边没有定义任何东西,这意味着作为参数的函数自身不具有参数,若其要传递参数,以foldLeft为例,需要如下定义

1
def foldLeft(initialValue: Int, operator: (Int, Int) => Int)= { ... }

我们以Scala实现搜索SCALA_HOME环境变量为例

1
2
3
4
5
6
7
8
9
10
11
12
val breakException = new RuntimeException("break exception")
def breakable(op: => Unit) {
try {
op
} catch { case _ => }
}
def break = throw breakException
def install = {
val env = System.getenv("SCALA_HOME")
if(env == null) break
println("found scala home lets do the real work")
}

调用该闭包:breakable(install),因为breakable中的实际参数为install,而install的参数为空,所以另外一种方式就是通过行内调用,如

1
2
3
4
5
breakable {
val env = System.getenv("SCALA_HOME")
if(env == null) break
println("found scala home lets do the real work")
}

说明 在Scala中,如果函数的最后一个参数为function类型,你就可以把该函数作为闭包传递。这个语法糖对创建DSL非常有用。下一章开始将介绍闭包如何转换为对象,记住,在Scala中,一切都是对象。

注意 在Scala的库中早就已经提供了breakable,参考scala.util.control.Breaks。Scala中没有break关键字,如果你需要使用break你需要使用Breaks即可。

Working with Array and List

在第四章为数据结构部分,在此之前,先来了解一下List和Array,这样我们就可以写一些比较有用的Scala脚本。
下面例子中,array是scala.Array中的一个实例,scala.Array类似Java中的Array类。

1
2
3
4
5
6
7
scala> val array = new Array[String](3)
array: Array[String] = Array(null, null, null)
scala> array(0) = "This"
scala> array(1) = "is"
scala> array(2) = "mutable"
scala> array
res37: Array[String] = Array(This, is, mutable)

记住,在Scala中,类型信息和参数化使用方括号,他们的作用是等同的。参数化是用于在创建实例的时候计算出该实例的数据类型。
下面打印出每个Array的元素

1
2
3
4
scala> array.foreach(println)
This
is
mutable

由于Scala中默认引入了Predef,所以,在使用Array对象的同时,请查看scala.collection.mutable.ArrayLike类,它被声明为trait(特质)。因此在使用Array实例的时候,可以动态地使用ArrayLike提供的功能。

注意 Predef默认将Array转换为scala.collection.mutable.ArrayOps.ArrayOps,该类是ArrayLike的子类。因此,ArrayLike就相当于一个接口为其子类提供了大量的方法,不同的是,在Scala中是通过trait声明,而在Java中是通过interface声明。

命令行参数

你可以使用一个隐式的val变量args来获取命令行输入的参数,如输出在控制台输入的参数

1
args.foreach(println)

我们把该表达式存储为一个myfirstScript.scala文件,在控制台输入如下命令 scala myfirstScript.scala my first script将输出如下结果

1
2
3
my 
first
script

Array是一个可变数据结构。通过向array中添加元素会改变array实例并产生副作用。在函数式编程中,方法不应该有副作用。方法的唯一作用就是计算值并返回值而不改变实例。因此,你可以使用List代替,在Scala中,List是不可变的并使得函数式编程变得容易。创建一个List可以如下创建

1
2
scala> val myList = List("This", "is", "immutable")
myList: List[java.lang.String] = List(This, is, immutable)

也可以如下创建

1
2
scala> val myList = scala.collection.immutable.List("This", "is","immutable")
myList: List[java.lang.String] = List(This, is, immutable)

什么是scala.collection.immutable.colon?

在上面例子中,你调用getClass方法查看对象的类型,你会对其输出觉得奇怪

1
2
3
scala> myList.getClass
res42: java.lang.Class[_] = class
scala.collection.immutable.$colon$colon

这是因为scala.collection.immutable.List是一个抽象类,它包含两个实现类,scala.Nilscala.::.。在Scala中,::是个有效标识符,你可以用它来命名一个类。Nil代表一个空的list,scala.::则代表非空的list。

可变集合大多数情况用于添加和删除元素,但不可变集合是不会被改变的,但可以通过创建新的实例实现,如

1
2
3
4
5
6
scala> val oldList = List(1, 2)
oldList: List[Int] = List(1, 2)
scala> val newList = 3 :: oldList
newList: List[Int] = List(3, 1, 2)
scala> oldList
res45: List[Int] = List(1, 2)

上述例子中,::用于创建一个新的实例并将元素添加的list的前面,如果要放到最后,可以使用:+方法

1
2
scala> val newList = oldList :+ 3
newList: List[Int] = List(1, 2, 3)

Scala中提供了一个特殊的对象Nil用于表示空List,因此,你可以用它创建一个空列表

1
2
scala> val myList = "This" :: "is" :: "immutable" :: Nil
myList: List[java.lang.String] = List(This, is, immutable)

在Scala中,若要删除一个元素,可以使用-方法,但是它是过时的。取而代之的是使用filterNot方法,该方法提供一个断言(predicate),然后根据断言选择出所有不符合的元素并放置到新的实例里面。

1
2
scala> val afterDelete = newList.filterNot(_ == 3)
afterDelete: List[Int] = List(1, 2)

关于集合更多内容,将在本书第四章4.3节作着重介绍。

Controlling flow with loops and ifs

if~else流程控制语句和其他编程语言一样,if为true则执行if语句块,否则执行else语句块。不同的是,在Scala中所有语句都是表达式,而它的值取决于最后一个表达式的值。因此,声明一个值取决于某些条件,如

1
2
3
4
5
val someValue = if(some condition) value1 else value2
scala> val useDefault = false
useDefault: Boolean = false
scala> val configFile = if(useDefault) "custom.txt" else "default.txt"
configFile: java.lang.String = default.txt

在Java中可以使用三元表达式,但在Scala中不支持,因为表达式是多余的,你甚至可以用if~else组合更加复杂的表达式语句。
循环语句在Scala中全面支持,比如while循环、do-while循环,但这两个都比较独立,和Java以及C#的使用没有异样。在
Scala中真正比较有趣的是for循环。

For-comprehensions

For-comprehensions 就像一把瑞士军刀一样,你可以用它做许多事情。

1
2
3
4
5
val files = new java.io.File(".").listFiles
for(file <- files) {
val filename = file.getName
if(fileName.endsWith(".scala")) println(file)
}

和Java和C#不同是地方是file <- files,在Scala中叫做生成器(generator),生成器的工作就是用于迭代,箭头右边的表示集合。用Java写则

1
2
3
4
for(File file: files) {
String filename = file.getName();
if(filename.endsWith(".scala")) System.out.println(file);
}

在本例中,你可以不用声明迭代对象的类型。另外一种写法

1
2
3
4
5
for(
file <- files;
fileName = file.getName
if fileName.endsWith(".iml")
) println(file)

不同的是,在括号内的所有变量都是val类型的,因此它们不能被修改并减少代码的副作用。
你也可以声明多个生成器(generator),如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scala> val aList = List(1, 2, 3)
aList: List[Int] = List(1, 2, 3)
scala> val bList = List(4, 5, 6)
bList: List[Int] = List(4, 5, 6)
scala> for { a <- aList; b <- bList } println(a + b)
5
6
7
6
7
8
7
8
9

实际上这是两层循环,即当a=1时,对b=4,5,6进行迭代,依此类推。你也可以用小括号()代替中括号。

从for-comprehension中可以看出了Scala的两种特性。即命令式和函数式。上面例子即为命令式,这种模式中容许语句反复运行数次而不返回值,直到某些条件改变。另一种命令式(functional form)也就序列解析(sequence comprehension),在这种模式中,更偏向于使用值而不是执行表达式语句,并且不返回值。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala> for { a <- aList; b <- bList } yield a + b
res27: List[Int] = List(5, 6, 7, 6, 7, 8, 7, 8, 9)
为了取代输出一个值,Scala中使用yield关键字代替返回的值,因此当你实际需要输出时,替换yield操作
scala> val result = for { a <- aList; b <- bList } yield a + b
result: List[Int] = List(5, 6, 7, 6, 7, 8, 7, 8, 9)
scala> for(r <- result) println(r)
5
6
7
6
7
8
7
8
9

看起来函数式比命令式要累赘,但是想想。你已经将计算(本例为两个数相加)和使用(输出结果)进行了分离,这提高了代码复用和函数的组合能力,这就是函数式编程的一个好处。例如下面是for-yield复用的一个例子

1
2
scala> val xmlNode = <result>{result.mkString(",")}</result>
xmlNode: scala.xml.Elem = <result>5,6,7,6,7,8,7,8,9</result>

mkString是定义在scala.collection.immutable.List中的一个方法,但是如果在表达中直接输出结果会怎样?显然这不好实现。记住,所有东西在Scala中都是表达式,并且都有一个返回值。例如将代码改为如下仍然可以获得一个返回值,但是返回的是一个空的集合

1
2
3
4
5
6
7
8
9
10
11
scala> for { a <- aList; b <- bList } yield { println(a+b)}
5
6
7
6
7
8
7
8
9
res32: List[Unit] = List((), (), (), (), (), (), (), (), ())

因为yield 后面跟着的是println,println返回值类型是unit,即相当于Java中的void。

Pattern Matching

模式匹配属于Scala函数式编程中的一个概念部分。模式匹配跟Java中的switch case类似。如下列出模式匹配在Scala和Java中的实现
-一般匹配模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Ordinal {
public static void main(String[] args) {
ordinal(Integer.parseInt(args[0]));
}
public static void ordinal(int number) {
switch(number) {
case 1: System.out.println("1st"); break;
case 2: System.out.println("2nd"); break;
case 3: System.out.println("3rd"); break;
case 4: System.out.println("4th"); break;
case 5: System.out.println("5th"); break;
case 6: System.out.println("6th"); break;
case 7: System.out.println("7th"); break;
case 8: System.out.println("8th"); break;
case 9: System.out.println("9th"); break;
case 10: System.out.println("10th"); break;
default : System.out.println("Cannot do beyond 10");
}
}
}

Scala中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ordinal(args(0).toInt)
def ordinal(number:Int) = number match {
case 1 => println("1st")
case 2 => println("2nd")
case 3 => println("3rd")
case 4 => println("4th")
case 5 => println("5th")
case 6 => println("6th")
case 7 => println("7th")
case 8 => println("8th")
case 9 => println("9th")
case 10 => println("10th")
case _ => println("Cannot do beyond 10")
}

和Java不同的是,你不用声明main方法入口,不用使用break语句,因为在Scala的case中不会跳转到其它语句上,default值则使用_占位符匹配所有。因为Scala支持脚本,因此在终端输入运行

1
scala Ordinal.scala <your input>

在Scala中,case _是可选的,移除后如果不匹配,则Scala会提示错误

1
2
3
4
5
scala> 2 match { case 1 => "One" }
scala.MatchError: 2
at .<init>(<console>:5)
at .<clinit>(<console>)
...

这点和Java的default不同,Java中去掉default后,如果不匹配,不会抛出任何信息,因为Java中这不算是异常。

类型匹配

Java中的switch case只能用于断言或枚举,而在Scala中,你可以匹配字符串,值,类型,变量,常量和构造器。更多模式匹配的概念会在下一章介绍,特别是构造器的匹配。下面是Scala模式匹配的一个例子

1
2
3
4
5
6
def printType(obj: AnyRef) = obj match {
case s: String => println("This is string")
case l: List[_] => println("This is List")
case a: Array[_] => println("This is an array")
case d: java.util.Date => println("This is a date")
}

这个例子中用于匹配类型,如

1
2
3
4
5
6
7
8
scala> printType("Hello")
This is string
scala> printType(List(1, 2, 3))
This is List
scala> printType(new Array[String](2))
This is an array
scala> printType(new java.util.Date())
This is a date
中辍操作匹配(插入操作匹配)

更为强大的是Scala中支持中辍操作匹配,如

1
2
3
4
5
scala> List(1, 2, 3, 4) match {
case f :: s :: rest => List(f, s)
case _ => Nil
}
res7: List[Int] = List(1, 2)

上面例子中表示要求list元素匹配f::ss::rest并输出f::ss,满足这种匹配的结果有1::2::3,4,则输出f::ss结果就是1,2

case增强

Scala允许你在case语句中使用扩展

1
2
3
4
5
def rangeMatcher(num:Int) = num match {
case within10 if within10 <= 10 => println("with in 0 to 10")
case within100 if within100 <= 100 => println("with in 11 to 100")
case beyond100 if beyond100 < Integer.MAX_VALUE => println("beyond 100")
}

有了上面这些功能后,我们对原来的例子进行改进

1
2
3
4
5
val suffixes = List("th", "st", "nd", "rd", "th", "th", "th", "th", "th","th")
def ordinal(number:Int) = number match {
case tenTo20 if 10 to 20 contains tenTo20 => number + "th"
case rest => rest + suffixes(number % 10)
}

此处调用了RichInt的to方法,该方法创建了一个范围对象(scala.collection.immutable
.Inclusive),该对象包含一个contains方法用于判断返回。最后一个表达式将不在10到20的变量映射到rest上。这叫做 可变模式匹配。你可以通过下标访问list中的特定元素。

Exception handling

前面的breakable例子初步接触了异常处理。Scala中的异常处理和Java有些许的不同。Scala支持单一的try/catch语句块,在这个单一catch语句块里面你可以使用模式匹配来捕获异常。实际上catch语句块就是匹配语句块,因此前面你所学到的模式匹配技术都可以用到catch语句块中。如修改rangeMatcher为

1
2
3
4
5
6
def rangeMatcher(num:Int) = num match {
case within10 if within10 <= 10 => println("with in 0 to 10")
case within100 if within100 <= 100 => println("with in 11 to 100")
case _ => throw new IllegalArgumentException(
"Only values between 0 and 100 are allowed")
}

现在,将其包含到try/catch语句中

1
2
3
4
scala> try {
rangeMatcher1(1000)
} catch { case e: IllegalArgumentException => e.getMessage }
res19: Any = Only values between 0 and 100 are allowed

Scala中没有类型检测的概念,所有异常都是未检测的。这是非常强大和灵活的,因为对于编程人员,你可以决定是否需要抛出异常,尽管Scala异常处理被实现得非常不一样,但行为机制和Java是一模一样的,通过不进行异常检测,是的Scala可以更容易地和Java现有的框架进行相互操作。你会在本书各处看到Scala异常处理的例子。

Command-line REST client: building a working example

什么是REST?

REST是(REpresentational State Transfer)的缩写,意即表述性状态转移。它是分布式超媒体系统(如WWW)的软件架构风格。
系统遵循如下REST原则的称为RESTful:

①Application state and functionality are divided into resources.
应用的状态和功能被分成资源。

②Every resource is uniquely addressable using a universal syntax.
任何一个资源都有唯一一个访问入口,可使用一般语法访问。

③All resources share a uniform interface for transfer of state between client and resource, consisting of well-defined operations (GET, POST, PUT, DELETE, OPTIONS, and so on, for RESTful web services) and content types.
所有资源共享标准接口用于在客户~资源之间进行状态转换,持有良好的操作和上下文类型。

④A protocol that’s client/server, stateless cacheable, and layered.
协议是C/S,无状态的,缓存的,分层的。

Introducing HttpClient library

HttpClient3是Apache的一个框架,它是一个客户端HTTP传输库。作用是用于发送和接口HTTP消息。它不是浏览器,也不执行与HTTP传输无关的事情,诸如JavaScript解析或猜测客户content-type内容等。HttpClient的主要工作就是执行HTTP方法。HttpClient会接受用户的请求对象(如HttpPost或HttpGet),然后将请求发送到目标服务器并返回响应对象或者抛出异常。
HttpClient封装了每个HTTP方法类型在一个对象中,这些方法在org.apache.http.client.methods中提供。本例将使用4中请求类型 GET, POST, DELETE, 和 OPTIONS。HttpClient默认提供的client已经足够我们使用了

1
2
val httpDelete = new HttpDelete(url)
val httpResponse = new DefaultHttpClient().execute(httpDelete)

根据HTTP规范,HTTP的POST方法有点不同,POST方法和PUT方法是实体封装的。
如果要在脚本中使用HttpClient,需要导入相应的包,Scala和Java一样,使用import关键字导入,不同的是Scala是使用_表示包中的所有类

1
2
3
4
5
6
7
import org.apache.http._
import org.apache.http.client.entity._
import org.apache.http.client.methods._
import org.apache.http.impl.client._
import org.apache.http.client.utils._
import org.apache.http.message._
import org.apache.http.params._

关于import的更多用法,将在下一章介绍

Building the client, step by step

这部分为本章所有知识点用例,请参考项目代码RestClient.scala,并尝试动手调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package net.scala.chapter2.o

// Compile in sbt
// Run in sbt>run get https://raw.githubusercontent.com/nraychaudhuri/scalainaction/master/chap02/breakable.scala
//
// The command line in sbt is
// >run (post | get | delete | options) -d <request parameters comma separated -h <headers comma separated> <url>
// at minimum you should specify action(post, get, delete, options) and url

import java.security.cert.X509Certificate
import javax.net.ssl._

import org.apache.http.client.entity.UrlEncodedFormEntity
import org.apache.http.client.methods._
import org.apache.http.conn.scheme.Scheme
import org.apache.http.conn.ssl.{SSLSocketFactory, X509HostnameVerifier}
import org.apache.http.impl.client._
import org.apache.http.message._

object RestClient extends App {

/*解析参数*/
def parseArgs(args: Array[String]): Map[String, List[String]] = {
def nameValuePair(paramName: String) = {
def values(commaSeperatedValues: String) = commaSeperatedValues.split(",").toList

val index = args.indexWhere(_ == paramName)
(paramName, if (index == -1) Nil else values(args(index + 1)))
}
Map(nameValuePair("-d"), nameValuePair("-h"))
}

def splitByEqual(nameValue: String): Array[String] = nameValue.split('=')

def headers = for (nameValue <- params("-h")) yield {
def tokens = splitByEqual(nameValue)
new BasicHeader(tokens(0), tokens(1))
}

def formEntity = {
def toJavaList(scalaList: List[BasicNameValuePair]) = {
java.util.Arrays.asList(scalaList.toArray: _*)
}

def formParams = for (nameValue <- params("-d")) yield {
def tokens = splitByEqual(nameValue)
new BasicNameValuePair(tokens(0), tokens(1))
}

def formEntity = new UrlEncodedFormEntity(toJavaList(formParams), "UTF-8")
formEntity
}

def handlePostRequest() = {
val httppost = new HttpPost(url)
headers.foreach {
httppost.addHeader(_)
}
httppost.setEntity(formEntity)
val responseBody = httpClient.execute(httppost, new BasicResponseHandler())
println(responseBody)
}

def handleGetRequest() = {
val query = params("-d").mkString("&")
val httpget = new HttpGet(s"$url?$query")
headers.foreach {
httpget.addHeader(_)
}
val responseBody = httpClient.execute(httpget, new BasicResponseHandler())
println(responseBody)
}

def handleDeleteRequest() = {
val httpDelete = new HttpDelete(url)
val httpResponse = httpClient.execute(httpDelete)
println(httpResponse.getStatusLine)
}

def handleOptionsRequest() = {
val httpOptions = new HttpOptions(url)
headers.foreach {
httpOptions.addHeader(_)
}
val httpResponse = httpClient.execute(httpOptions)
println(httpOptions.getAllowedMethods(httpResponse))
}

/*require 函数用于抛出异常*/
require(args.size >= 2, "at minimum you should specify action(post, get, delete, options) and url")
val command = args.head
val params = parseArgs(args)
val url = args.last

/** SNI(Server Name Indication) 问题
* 针对https服务器会使用SNI选择证书进行发送,但是本例不支持SNI(如一些Android系统),在SSL/TLS握手期间,
* 服务器无法根据客户端选择哪种SNI证书发送,因此需要让其验证时为True
*/
val xtm = new X509TrustManager {
override def getAcceptedIssuers: Array[X509Certificate] = null
override def checkClientTrusted(p1: Array[X509Certificate], p2: String): Unit = {}
override def checkServerTrusted(p1: Array[X509Certificate], p2: String): Unit = {}
}
val hostnameVerifier = new X509HostnameVerifier {
override def verify(p1: String, p2: SSLSocket): Unit = {}
override def verify(p1: String, p2: X509Certificate): Unit = {}
override def verify(p1: String, p2: Array[String], p3: Array[String]): Unit = {}
override def verify(p1: String, p2: SSLSession): Boolean = true
}
//TLS1.0与SSL3.0基本上没有太大的差别,可粗略理解为TLS是SSL的继承者,但它们使用的是相同的SSLContext
val ctx = SSLContext.getInstance("TLS")
//使用TrustManager来初始化该上下文,TrustManager只是被SSL的Socket所使用
ctx.init(null, Array(xtm), null)
//创建SSLSocketFactory
var socketFactory = new SSLSocketFactory(ctx)
socketFactory.setHostnameVerifier(hostnameVerifier)

val httpClient = new DefaultHttpClient()
httpClient.getConnectionManager.getSchemeRegistry
.register(new Scheme("https", socketFactory, 443))

command match {
case "post" => handlePostRequest()
case "get" => handleGetRequest()
case "delete" => handleDeleteRequest()
case "options" => handleOptionsRequest()
}

}

Summary

本章涵盖了Scala的大部分基础概念,如数据类型、变量和函数,以及如何安装和配置Scala。更重要的是,你学会了如何定义函数,Scala语句块,函数式语言的概念,模式匹配和for-comprehension。你更学习了如何通过模式匹配技术来定义异常。同时本章也介绍了List和Array集合的使用以及如何编写Scala脚本。本章还介绍了REPL编程环境,并且REPL将通篇出现。本章最后还介绍了运用所学知识点编写REST程序代码。下一章开始,我们将着重介绍Scala类和对象。