一、基础
1、表达式
scala> 1 + 1
val res0: Int = 2
res0是解释器自动创建的变量名称,用来指代表达式的计算结果。它是Int类型,值为2。
Scala中(几乎)一切都是表达式。
2、值
你可以给一个表达式的结果起个名字赋成一个不变量(val)。
scala> val two = 1 + 1
val two: Int = 2
你不能改变这个不变量的值。
3、变量
如果你需要修改这个名称和结果的绑定,可以选择使用 var。
scala> var name = "steve"
var name: String = steve
scala> name = "marius"
// mutated name
scala> name
val res1: String = marius
4、函数
你可以使用 def 创建函数。
定义 addOne 函数:
scala> def addOne(m: Int): Int = m + 1
def addOne(m: Int): Int
在Scala中,你需要为函数参数指定类型。
使用 addOne 函数:
scala> val three = addOne(2)
val three: Int = 3
如果函数不带参数,你可以不写括号。
定义不带参数的 three 函数并使用:
scala> def three() = 1 + 2
def three(): Int
scala> three()
val res2: Int = 3
scala> three
val res3: Int = 3
5、匿名函数
你可以创建匿名函数。
创建匿名函数:
scala> (x: Int) => x + 1
val res4: Int => Int = $Lambda$1049/0x00000008010be840@6130a6f5
这个函数为名为 x 的 Int 变量加 1。
使用匿名函数:
scala> res4(1)
val res5: Int = 2
你可以传递匿名函数,或将其保存成不变量。
保存匿名函数为不变量 addOne 并使用:
scala> val addOne = (x: Int) => x + 1
val addOne: Int => Int = $Lambda$1052/0x00000008010c1040@5cef5fc9
scala> addOne(1)
val res6: Int = 2
如果你的函数有很多表达式,可以使用{}来格式化代码,使之易读。
定义函数 timesTwo:
scala> def timesTwo(i: Int): Int = {
| println("hello world")
| i * 2
| }
def timesTwo(i: Int): Int
对匿名函数也是这样的。
定义匿名函数:
scala> { i: Int =>
| println("hello world")
| i * 2
| }
val res15: Int => Int = $Lambda$1095/0x0000000801097840@60eabf53
在将一个匿名函数作为参数进行传递时,这个语法会经常被用到。
6、部分应用(Partial application)
你可以使用下划线“_”部分应用一个函数,结果将得到另一个函数。Scala使用下划线表示不同上下文中的不同事物,你通常可以把它看作是一个没有命名的神奇通配符。在{ _ + 2 }的上下文中,它代表一个匿名参数。你可以这样使用它:
定义函数 adder,并部分使用 adder 定义不变量 add2:
scala> def adder(m: Int, n: Int) = m + n
def adder(m: Int, n: Int): Int
scala> val add2 = adder(2, _:Int)
val add2: Int => Int = $Lambda$1056/0x00000008010c6040@6aa152b7
scala> add2(3)
val res7: Int = 5
你可以部分应用参数列表中的任意参数,而不仅仅是最后一个。
7、柯里化函数
有时会有这样的需求:允许别人一会在你的函数上应用一些参数,然后又应用另外的一些参数。
例如一个乘法函数,在一个场景需要选择乘数,而另一个场景需要选择被乘数。
定义乘法函数 multiply:
scala> def multiply(m: Int)(n: Int): Int = m * n
def multiply(m: Int)(n: Int): Int
你可以直接传入两个参数。
使用乘法函数 multiply:
scala> multiply(2)(3)
val res8: Int = 6
你可以填上第一个参数并且部分应用第二个参数。
部分应用乘法函数 multiply 得到不变量 timesTwo 并使用:
scala> val timesTwo = multiply(2) _
val timesTwo: Int => Int = $Lambda$1064/0x00000008010c2040@7802fc4e
scala> timesTwo(3)
val res9: Int = 6
8、可变长度参数
这是一个特殊的语法,可以向方法传入任意多个同类型的参数。例如要在多个字符串上执行 String 的 capitalize 函数,可以这样写:
定义带有可变长度参数的函数 capitalizeAll,分别传入一个字符串和多个字符串:
scala> def capitalizeAll(args: String*) = {
| args.map { arg =>
| arg.capitalize
| }
| }
def capitalizeAll(args: String*): Seq[String]
scala> capitalizeAll("rarity")
val res11: Seq[String] = ArraySeq(Rarity)
scala> capitalizeAll("rarity","applejack")
val res12: Seq[String] = ArraySeq(Rarity, Applejack)
9、类
定义类 Calculator,创建 Calculator 对象 calc,调用 calc 函数 add,并调用 calc 不变量 brand
scala> class Calculator {
| val brand: String = "HP"
| def add(m: Int, n: Int): Int = m + n
| }
class Calculator
scala> val calc = new Calculator
val calc: Calculator = Calculator@5db94eaf
scala> calc.add(1,2)
val res13: Int = 3
scala> calc.brand
val res14: String = HP
上面的例子展示了如何在类中用def定义方法和用val定义字段值。方法就是可以访问类的状态的函数。
10、构造函数
构造函数不是特殊的方法,他们是除了类的方法定义之外的代码。让我们扩展计算器的例子,增加一个构造函数参数,并用它来初始化内部状态。
定义类 Calculator,其中定义构造函数参数 color
scala> class Calculator(brand: String) {
| /**
| * A constructor.
| */
| val color: String = if (brand == "TI") {
| "blue"
| } else if (brand == "HP") {
| "black"
| } else {
| "white"
| }
| // An instance method.
| def add(m: Int, n: Int): Int = m + n
| }
class Calculator
注意两种不同风格的注释。
你可以使用构造函数来构造一个实例:
利用构造函数构造实例 calc
scala> val calc = new Calculator("HP")
val calc: Calculator = Calculator@4aae493
scala> calc.color
val res16: String = black
上文的Calculator例子说明了Scala是如何面向表达式的。颜色的值就是绑定在一个if/else表达式上的。Scala是高度面向表达式的:大多数东西都是表达式而非指令。
11、继承
从类 Calculator 中继承定义类 ScientificCalculator
scala> class ScientificCalculator(brand: String) extends Calculator(brand) {
| def log(m: Double, base: Double) = math.log(m) / math.log(base)
| }
class ScientificCalculator
重载方法:
对类 ScientificCalculator 重载方法,定义类 EvenMoreScientificCalculator
scala> class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {
| def log(m: Int): Double = log(m, math.exp(1))
| }
class EvenMoreScientificCalculator
12、抽象类
你可以定义一个抽象类,它定义了一些方法但没有实现它们。取而代之是由扩展抽象类的子类定义这些方法。你不能创建抽象类的实例。
scala> abstract class Shape {
| def getArea():Int // subclass should define this defined class Shape
| }
class Shape
scala> class Circle(r: Int) extends Shape {
| def getArea():Int = { r * r * 3 }
| }
class Circle
scala> val s = new Shape
^
error: class Shape is abstract; cannot be instantiated
scala> val c = new Circle(2)
val c: Circle = Circle@2f2dd0c9
抽象类有什么意义?
13、特质(Traits)
特质是一些字段和行为的集合,可以扩展或混入(mixin)你的类中。
scala> trait Car {
| val brand: String
| }
trait Car
scala> trait Shiny {
| val shineRefraction: Int
| }
trait Shiny
scala> class BMW extends Car {
| val brand = "BMW"
| }
class BMW
通过with关键字,一个类可以扩展多个特质:
scala> class BMW extends Car with Shiny {
| val brand = "BMW"
| val shineRefraction = 12
| }
class BMW
14、类型
此前,我们定义了一个函数的参数为Int,表示输入是一个数字类型。其实函数也可以是泛型的,来适用于所有类型。当这种情况发生时,你会看到用方括号语法引入的类型参数。下面的例子展示了一个使用泛型键和值的缓存。
scala> trait Cache[K, V] {
| def get(key: K): V
| def put(key: K, value: V)
| def delete(key: K)
| }
15、apply 方法
当类或对象有一个主要用途的时候,apply方法为你提供了一个很好的语法糖。
scala> class Foo {}
class Foo
scala> object FooMaker {
| def apply() = new Foo
| }
object FooMaker
scala> val newFoo = FooMaker()
val newFoo: Foo = Foo@2554a03c
或
scala> class Bar {
| def apply() = 0
| }
class Bar
scala> val bar = new Bar
val bar: Bar = Bar@32b05872
scala> bar()
val res17: Int = 0
在这里,我们实例化对象看起来像是在调用一个方法。
16、单例对象
单例对象用于持有一个类的唯一实例。通常用于工厂模式。
scala> object Timer {
| var count = 0
| def currentCount(): Long = {
| count += 1
| count
| }
| }
object Timer
可以这样使用:
scala> Timer.currentCount()
val res18: Long = 1
单例对象可以和类具有相同的名称,此时该对象也被称为“伴生对象”。我们通常将伴生对象作为工厂使用。
下面是一个简单的例子,可以不需要使用’new’来创建一个实例了。
scala> class Bar(foo: String)
class Bar
scala> object Bar {
| def apply(foo: String) = new Bar(foo)
| }
object Bar
17、函数即对象
在Scala中,我们经常谈论对象的函数式编程。这是什么意思?到底什么是函数呢?
函数是一些特质的集合。具体来说,具有一个参数的函数是Function1特质的一个实例。这个特质定义了apply()语法糖,让你调用一个对象时就像你在调用一个函数。
scala> object addOne extends Function1[Int, Int] {
| def apply(m: Int): Int = m + 1
| }
object addOne
scala> addOne(1)
val res19: Int = 2
这个Function特质集合下标从0开始一直到22。为什么是22?这是一个主观的魔幻数字(magic number)。我从来没有使用过多于22个参数的函数,所以这个数字似乎是合理的。
apply语法糖有助于统一对象和函数式编程的二重性。你可以传递类,并把它们当做函数使用,而函数本质上是类的实例。
这是否意味着,当你在类中定义一个方法时,得到的实际上是一个Function的实例?不是的,在类中定义的方法是方法而不是函数。在repl中独立定义的方法是Function的实例。
类也可以扩展Function,这些类的实例可以使用()调用。
scala> class AddOne extends Function1[Int, Int] {
| def apply(m: Int): Int = m + 1
| }
class AddOne
scala> val plusOne = new AddOne()
val plusOne: AddOne = <function1>
scala> plusOne(1)
val res20: Int = 2
可以使用更直观快捷的extends (Int => Int)代替extends Function1[Int, Int]
scala> class AddOne extends (Int => Int) {
| def apply(m: Int): Int = m + 1
| }
class AddOne
18、包
你可以将代码组织在包里。
package com.twitter.example
在文件头部定义包,会将文件中所有的代码声明在那个包中。
值和函数不能在类或单例对象之外定义。单例对象是组织静态函数(static function)的有效工具。
package com.twitter.example
object colorHolder { val BLUE = "Blue" val RED = "Red" }
现在你可以直接访问这些成员
println("the color is: " + com.twitter.example.colorHolder.BLUE)
注意在你定义这个对象时Scala解释器的返回:
scala> object colorHolder {
| val Blue = "Blue"
| val Red = "Red"
| }
defined module colorHolder
这暗示了Scala的设计者是把对象作为Scala的模块系统的一部分进行设计的。
19、模式匹配
这是Scala中最有用的部分之一。
(1)匹配值
scala> val times = 1
val times: Int = 1
scala> times match {
| case 1 => "one"
| case 2 => "two"
| case _ => "some other number"
| }
val res21: String = one
使用守卫进行匹配:
scala> times match {
| case i if i == 1 => "one"
| case i if i == 2 => "two"
| case _ => "some other number"
| }
val res22: String = one
注意我们是怎样获取变量’i’的值的。
在最后一行指令中的_是一个通配符;它保证了我们可以处理所有的情况。 否则当传进一个不能被匹配的数字的时候,你将获得一个运行时错误。我们以后会继续讨论这个话题的。
(2)匹配类型
你可以使用 match来分别处理不同类型的值。
scala> val text = "Error"
val text: String = Error
scala> def bigger(o: Any): Any = {
| o match {
| case i: Int if i < 0 => i - 1
| case i: Int => i + 1
| case d: Double if d < 0.0 => d - 0.1
| case d: Double => text + "s"
| }
| }
def bigger(o: Any): Any
(3)匹配类成员
还记得我们之前的计算器吗。
让我们通过类型对它们进行分类。
一开始会很痛苦。
def calcType(calc: Calculator) = calc match {
case _ if calc.brand == "HP" && calc.model == "20B" => "financial"
case _ if calc.brand == "HP" && calc.model == "48G" => "scientific"
case _ if calc.brand == "HP" && calc.model == "30B" => "business"
case _ => "unknown"
}
(⊙o⊙)哦,太痛苦了。幸好Scala提供了一些应对这种情况的有效工具。
20、样本类(Case Classes)
使用样本类可以方便得存储和匹配类的内容。不用new关键字就可以创建它们。
scala> case class Calculator(brand: String, model: String)
class Calculator
scala> val hp20b = Calculator("HP", "20b")
val hp20b: Calculator = Calculator(HP,20b)
样本类基于构造函数的参数,自动地实现了相等性和易读的toString方法。
scala> val hp20b = Calculator("HP", "20b")
val hp20b: Calculator = Calculator(HP,20b)
scala> val hp20B = Calculator("HP", "20b")
val hp20B: Calculator = Calculator(HP,20b)
scala> hp20b == hp20B
val res23: Boolean = true
样本类也可以像普通类那样拥有方法。
(1)使用样本类进行模式匹配
样本类就是被设计用在模式匹配中的。让我们简化之前的计算器分类器的例子。
val hp20b = Calculator("HP", "20B")
val hp30b = Calculator("HP", "30B")
def calcType(calc: Calculator) = calc match {
case Calculator("HP", "20B") => "financial"
case Calculator("HP", "48G") => "scientific"
case Calculator("HP", "30B") => "business"
case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)
}
最后一句也可以这样写
case Calculator(_, _) => "Calculator of unknown type"
或者我们完全可以不将匹配对象指定为Calculator类型
case _ => "Calculator of unknown type"
或者我们也可以将匹配的值重新命名。
case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)
21、异常
Scala中的异常可以在try-catch-finally语法中通过模式匹配使用。
try {
remoteCalculatorService.add(1, 2)
} catch {
case e: ServerIsDownException => log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")
} finally {
remoteCalculatorService.close()
}
try也是面向表达式的
val result: Int = try {
remoteCalculatorService.add(1, 2)
} catch {
case e: ServerIsDownException => {
log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")
0
}
} finally {
remoteCalculatorService.close()
}
这并不是一个完美编程风格的展示,而只是一个例子,用来说明try-catch-finally和Scala中其他大部分事物一样是表达式。
当一个异常被捕获处理了,finally块将被调用;它不是表达式的一部分。
二、集合
(一)基本数据结构
1、数组 Array
数组是有序的,可以包含重复项,并且可变。
scala> val numbers = Array(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
val numbers: Array[Int] = Array(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
scala> numbers(3) = 10
scala> numbers
val res1: Array[Int] = Array(1, 2, 3, 10, 5, 1, 2, 3, 4, 5)
2、列表 List
列表是有序的,可以包含重复项,不可变。
scala> val numbers = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
val numbers: List[Int] = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
scala> numbers(3) = 10
^
error: value update is not a member of List[Int]
did you mean updated?
3、集合 Set
集合无序且不可包含重复项。
scala> val numbers = Set(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
val numbers: scala.collection.immutable.Set[Int] = HashSet(5, 1, 2, 3, 4)
4、元组 Tuple
元组在不使用类的情况下,将元素组合起来形成简单的逻辑集合。
scala> val hostPort = ("localhost", 80)
val hostPort: (String, Int) = (localhost,80)
与样本类不同,元组不能通过名称获取字段,而是使用位置下标来读取对象;而且这个下标基于1,而不是基于0。
scala> hostPort._1
val res3: String = localhost
scala> hostPort._2
val res4: Int = 80
元组可以很好得与模式匹配相结合。
hostPort match {
case ("localhost", port) => ...
case (host, port) => ...
}
在创建两个元素的元组时,可以使用特殊语法:->
scala> 1 -> 2
val res5: (Int, Int) = (1,2)
5、映射 Map
它可以持有基本数据类型。
scala> Map(1 -> 2)
val res6: scala.collection.immutable.Map[Int,Int] = Map(1 -> 2)
scala> Map("foo" -> "bar")
val res7: scala.collection.immutable.Map[String,String] = Map(foo -> bar)
这看起来像是特殊的语法,不过不要忘了上文讨论的->可以用来创建二元组。
Map()方法也使用了从第一节课学到的变参列表:Map(1 -> “one”, 2 -> “two”)将变为 Map((1, “one”), (2, “two”)),其中第一个元素是映射的键,第二个元素是映射的值。
映射的值可以是映射甚至是函数。
scala> Map(1 -> Map("foo" -> "bar"))
val res8: scala.collection.immutable.Map[Int,scala.collection.immutable.Map[String,String]] = Map(1 -> Map(foo -> bar))
scala> def timesTwo(x: Int): Int = x * 2
def timesTwo(x: Int): Int
scala> Map("timesTwo" -> { timesTwo(_) })
val res10: scala.collection.immutable.Map[String,Int => Int] = Map(timesTwo -> $Lambda$1120/0x00000008010f7840@56a6aadb)
6、选项 Option
Option 是一个表示有可能包含值的容器。
Option基本的接口是这样的:
trait Option[T] {
def isDefined: Boolean
def get: T
def getOrElse(t: T): T
}
Option本身是泛型的,并且有两个子类: Some[T] 或 None
我们看一个使用Option的例子:
Map.get 使用 Option 作为其返回值,表示这个方法也许不会返回你请求的值。
scala> val numbers = Map("one" -> 1, "two" -> 2)
val numbers: scala.collection.immutable.Map[String,Int] = Map(one -> 1, two -> 2)
scala> numbers.get("two")
val res11: Option[Int] = Some(2)
scala> numbers.get("three")
val res12: Option[Int] = None
现在我们的数据似乎陷在Option中了,我们怎样获取这个数据呢?
直觉上想到的可能是基于isDefined方法进行条件判断。
// We want to multiply the number by two, otherwise return 0.
val result = if (res1.isDefined) {
res1.get * 2
} else {
0
}
我们建议使用getOrElse或模式匹配处理这个结果。
getOrElse 让你轻松地定义一个默认值。
val result = res1.getOrElse(0) * 2
模式匹配能自然地配合Option使用。
val result = res1 match {
case Some(n) => n * 2
case None => 0
}
(二)函数组合子
List(1, 2, 3) map squared对列表中的每一个元素都应用了squared平方函数,并返回一个新的列表List(1, 4, 9)。我们把类似于map的操作称作组合子。
1、map
map对列表中的每个元素应用一个函数,返回应用后的元素所组成的列表。
scala> val numbers = List(1, 2, 3, 4)
val numbers: List[Int] = List(1, 2, 3, 4)
scala> numbers.map((i: Int) => i * 2)
val res15: List[Int] = List(2, 4, 6, 8)
或传入一个函数 (Scala编译器自动把我们的方法转换为函数)
scala> def timesTwo(i: Int): Int = i * 2
def timesTwo(i: Int): Int
scala> numbers.map(timesTwo)
val res16: List[Int] = List(2, 4, 6, 8)
2、foreach
foreach很像map,但没有返回值。foreach仅用于有副作用[side-effects]的函数。
scala> numbers.foreach((i: Int) => i * 2)
什么也没有返回。
你可以尝试存储返回值,但它会是Unit类型(即 void)
scala> val doubled = numbers.foreach((i: Int) => i * 2)
val doubled: Unit = ()
3、filter
filter 移除任何对传入函数计算结果为false的元素。返回一个布尔值的函数通常被称为谓词函数[或判定函数]。
scala> numbers.filter((i: Int) => i % 2 == 0)
val res18: List[Int] = List(2, 4)
scala> def isEven(i: Int): Boolean = i % 2 == 0
def isEven(i: Int): Boolean
scala> numbers.filter(isEven)
val res19: List[Int] = List(2, 4)
4、zip
zip 将两个列表的内容聚合到一个对偶列表中。
scala> List(1, 2, 3).zip(List("a", "b", "c"))
val res20: List[(Int, String)] = List((1,a), (2,b), (3,c))
5、partition
partition 将使用给定的谓词函数分割列表。
scala> val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val numbers: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
scala> numbers.partition(_ % 2 == 0)
val res21: (List[Int], List[Int]) = (List(2, 4, 6, 8, 10),List(1, 3, 5, 7, 9))
6、find
find返回集合中第一个匹配谓词函数的元素。
scala> numbers.find((i: Int) => i > 5)
val res22: Option[Int] = Some(6)
7、drop & dropWhile
drop 将删除前 i 个元素
scala> numbers.drop(5)
val res23: List[Int] = List(6, 7, 8, 9, 10)
dropWhile 将删除匹配谓词函数的第一个元素。例如,如果我们在numbers列表上使用dropWhile函数来去除奇数, 1将被丢弃(但3不会被丢弃,因为他被2“保护”了)。
scala> numbers.dropWhile(_ % 2 != 0)
val res24: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)
8、foldLeft
scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)
val res25: Int = 55
0为初始值(记住numbers是List[Int]类型),m作为一个累加器。
可视化观察运行过程:
scala> numbers.foldLeft(0) {(m: Int, n: Int) => println("m: " +m+" n: "+n);m+n}
m: 0 n: 1
m: 1 n: 2
m: 3 n: 3
m: 6 n: 4
m: 10 n: 5
m: 15 n: 6
m: 21 n: 7
m: 28 n: 8
m: 36 n: 9
m: 45 n: 10
val res26: Int = 55
9、foldRight
和foldLeft一样,只是运行过程相反。
scala> numbers.foldRight(0){(m: Int, n: Int) => println("m: " + m + " n: " + n); m + n }
m: 10 n: 0
m: 9 n: 10
m: 8 n: 19
m: 7 n: 27
m: 6 n: 34
m: 5 n: 40
m: 4 n: 45
m: 3 n: 49
m: 2 n: 52
m: 1 n: 54
val res27: Int = 55
10、flatten
flatten将嵌套结构扁平化一个层级。
scala> List(List(1, 2), List(3, 4)).flatten
val res28: List[Int] = List(1, 2, 3, 4)
11、flatMap
flatMap是一种常用的组合子,结合映射[mapping]和扁平化[flattening]。 flatMap需要一个处理嵌套列表的函数,然后将结果串连起来。
scala> val nestedNumbers = List(List(1, 2), List(3, 4))
val nestedNumbers: List[List[Int]] = List(List(1, 2), List(3, 4))
scala> nestedNumbers.flatMap(x => x.map(_ * 2))
val res29: List[Int] = List(2, 4, 6, 8)
可以把它看做是“先映射后扁平化”的快捷操作:
scala> nestedNumbers.map((x: List[Int]) => x.map(_ * 2)).flatten
val res30: List[Int] = List(2, 4, 6, 8)
这个例子先调用map,然后调用flatten,这就是“组合子”的特征,也是这些函数的本质。
参考资料:Scala 课堂