分号
分号在scala中是可选的,scala编译器能够自动推断语句的结束,所以不必每行语句都添加,当然可以用在同一行写多条语句的情况.
// 行尾有等号时表示还有更多的语句
def equalSign(s:String) =
println("Another line.")
// 行尾有一个开放的大括号表示下一行有更多的代码
def equalSign2(s:String) = {
println("Another line.")
}
// 行尾有逗号,句号或操作符均表示下一行有更多代码
def commas(s1:String,
s2:String) = Console.
println("comma: " + s1 +
" , " + s2)
变量声明
scala允许你在声明变量时指认其为不可变(immutable)或可变(mutable)变量.顾名思义,不可变变量在声明后,其值就不能再做改变了,可变变量则可以.无论何时,声明后的变量类型不可改变.
// 声明一个不可变Array
scala> val array:Array[String] = new Array(5) // 声明一个包含5个String元素的不可变数组
array: Array[String] = Array(null, null, null, null, null)
scala> array = new Array(2) // 把array变量声明为一个新的Aarray,错误
<console>:8: error: reassignment to val
array = new Array(2)
scala> array(0) = "Hello" // 初始化元素的值
scala> array
res1: Array[String] = Array(Hello, null, null, null, null)
一个变量被声明后必须被初始化.
// 声明一个可变的Double
scala> var stockPrice: Double = 100.0 // 声明一个可变的Double类型变量
stockPrice: Double = 100.0
scala> stockPrice = 200.0 // 修改变量stockPrice的值
stockPrice: Double = 200.0
同时,在scala中的types, char, byte, short, int, long, float, double, boolean这些基本类型都是包含方法的对象,类似于引用类型.
当变量作为类的构造参数时,声明为val或var后,该变量则对应为该类的不可变或可变属性:
scala> class Person(val name: String, var age: Int)
defined class Person
scala> class Person(val name: String, var age: Int)
defined class Person
scala> p.name // Show the value of firstName.
res0: String = Dean Wampler
scala> p.age // Show the value of age.
res2: Int = 29
scala> p.name = "Buck Trends" // 不允许修改声明为val的参数
<console>:9: error: reassignment to val
p.name = "Buck Trends" ^
scala> p.age = 30
p.age: Int = 30 // 允许修改声明为var的参数
尽量使用immutable类型的参数可以最大限度的消除因为参数可变性造成的bug.
Ranges
Int, Long, Float, Double, Char, BigInt类型的随机数生成:
1 to 10
Range(1,2,3,4,5,6,7,8,9,10)
1 until 10
Range(1, 2, 3, 4, 5, 6, 7, 8, 9)
1 to 10 by 3
Range(1, 4, 7, 10)
10 to 1 by -3
1L to 10L by 3
1.1f to 10.3f by 3.1f
NumericRange(1.1, 4.2, 7.2999997)
1.1f to 10.3f by 0.5f
NumericRange(1.1, 1.6, 2.1, 2.6, 3.1, 3.6, 4.1, 4.6, 5.1, 5.6, 6.1, 6.6, 7.1, 7.6, 8.1, 8.6, 9.1, 9.6, 10.1)
1.1 to 10.3 by 3.1
NumericRange(1.1, 4.2, 7.300000000000001)
'a' to 'g' by 3
NumericRange(a, d, g)
BigInt(1) to BigInt(10) by 3
NumericRange(1, 4, 7, 10)
BigDecimal(1.1) to BigDecimal(10.3) by 3.1
NumericRange(1.1, 4.2, 7.3)
偏函数
偏函数是声明为 PartialFunction[-A,+B] 类型的一种函数,其中’A’是函数接受的参数类型,’B’是函数返回值类型.偏函数最大的特点就是只接受和处理其参数类型子集参数类型,而对于这个类型子集之外的参数抛出运行时异常.
这与模式匹配中的case语句语义相近,在模式匹配时,通常是定义一组具体的模式,然后使用’_’处理所有为定义的模式.如果一组case语句没有涵盖所有情况,则这个case语句就可以被视为一个偏函数.
val second:PartialFunction[List[Int],Int] = {
case List(x::y::_) => y
}
这个函数接受一个List[Int]类型的参数,返回一个Int类型的结果,这个结果是这个List参数的第二个元素.但是如果传入的List只有一个元素,就会抛出运行时异常.
如果在调用偏函数前想要检查一下参数是否在其定义域范围内,可以调用偏函数的isDefinedAt()方法:
scala> second.isDefinedAt(List(2,3,4))
res0: Boolean = true
scala> second.isDefinedAt(List(2))
res1: Boolean = false
事实上,scala编译器把偏函数second编译成了:
new PartialFunction[List[Int], Int] {
def apply(xs: List[Int]) = xs match {
case x :: y :: _ => y
}
def isDefinedAt(xs: List[Int]) = xs match {
case x :: y :: _ => true
case _ => false
}
}
这种行为是编译期行为,我们必须显示的声明函数为PartialFunction类型,则编译器会把该函数编译成完整的普通函数.
因此,想要把一组case语句声明为一个偏函数,必须显式的声明其为PartialFunction类型.当确认程序不会传入不可处理的值时,就可以使用偏函数,这样一旦有定义域之外的值传入,程序会自动抛出运行时异常,而不需要手动编写抛出异常代码,节省代码量,只需处理异常即可.
方法声明
带默认值的方法和命名参数
定义一个Point类:
case class Point(x:Double = 0.0, y:Double = 0.0){ // 定义一个带有初始参数值的Point类
def shift(deltax:Double = 0.0, deltay:Double = 0.0) = // 定义一个带有默认参数值shift方法
copy(x + deltax, y + deltay)
}
带有参数列表的方法
定义一个抽象类Shape:
abstract class Shape() {
/**
* Draw takes TWO argument LISTS, one list with an offset for drawing,
* and the other list that is the function argument we used previously.
*/
def draw(offset: Point = Point(0.0, 0.0))(f: String => Unit): Unit =
f(s"draw(offset = $offset), ${this.toString}")
}
Shape类的draw方法接受两个参数,第一个是Point类型的偏移值,并且提供了默认值Point(0.0, 0.0),即无偏移.第二个参数是一个函数,该函数接受一个String类型的参数并返回Unit.最终,draw方法返回Unit.
可以按如下方式调用该方法:
case class Circle(center: Point, radius: Double) extends Shape
case class Rectangle(lowerLeft: Point, height: Double, width: Double) extends Shape
s.draw(Point(1.0, 2.0))(str => println(s"ShapesDrawingActor: $str"))
s.draw(Point(1.0, 2.0)){str => println(s"ShapesDrawingActor: $str")}
s.draw(Point(1.0, 2.0)) { str =>
println(s"ShapesDrawingActor: $str")
}
s.draw(Point(1.0, 2.0)) {
str => println(s"ShapesDrawingActor: $str")
}
s.draw() {
str => println(s"ShapesDrawingActor: $str")
}
s.draw(Point(1.0, 2.0),
str => println(s"ShapesDrawingActor: $str")
)
s.draw(f = str => println(s"ShapesDrawingActor: $str"))
初试Futures
scala.concurrent.Future的API使用隐式转换参数来减少代码中的引用.是scala中提供的另外一种并发工具.Akka会使用Futures,但是如果你不需要使用完整的Akka,同样可以单独的使用Futures.
如果你再Future中包装一些动作,这些动作会以异步的方式执行,Future同时提供了多种方法来处理动作执行的结果,比如结果完成的时候提供回调方法:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global // 提供隐式转换
def sleep(millis: Long) = {
Thread.sleep(millis)
}
// Busy work ;)
def doWork(index: Int) = {
sleep((math.random * 1000).toLong)
index
}
(1 to 5) foreach { index =>
val future = Future { // 使用Future封装函数执行动作
doWork(index)
}
future onSuccess {
case answer: Int => println(s"Success! returned: $answer")
}
future onFailure {
case th: Throwable => println(s"FAILURE! returned: $th")
}
}
sleep(1000) // Wait long enough for the "work" to finish.
println("Finito!")
嵌套函数定义和递归
使用嵌套函数定义一个阶乘计算器:
def factorial(i: Int): Long = {
def fact(i: Int, accumulator: Int): Long = {
if (i <= 1) accumulator else fact(i - 1, i * accumulator)
}
fact(i, 1)
}
(0 to 5) foreach ( i => println(factorial(i)) )
内部嵌套的函数不断的调用自己,这是一个递归函数,递归函数必须定义返回值类型,同时该函数只能在factorial函数内部可见.
在fact函数中调用fact(i - 1, i * accumulator)时,由于要保存之前”n”的值,必须分配一个新的函数栈,如果n值很大是,函数栈将很快被耗尽.尾递归则可以很好的处理这个问题,尾递归是指在函数处理的最后一步,只调用该递归函数本身,此时无需记录其他变量,当前的函数栈可以被重复利用,在效率上和循环是等价的.
import scala.annotation.tailrec
def factorial(i: Int): Long = {
@tailrec
def fact(i: Int, accumulator: Int): Long = {
if (i <= 1) accumulator else fact(i - 1, i * accumulator)
}
fact(i, 1)
}
(0 to 5) foreach ( i => println(factorial(i)) )
内部的fact函数要么返回结果,要么调用本身,变量accumulator每次递归调用都会更新值,直到边界条件满足是返回该值,即为最后的计算结果.注释 @tailrec可以确保写出的程序是正确的尾递归,否则编译时会报错误,以提醒修改代码.
JVM本身并不提供对尾递归的优化,java的编译器也不支持,只有scala的编译器提供.
字面值
各种基本类型的字面值与java类似.
Option,Some,and None:拒绝nulls
每种语言都会有一些关键字来引用那些没有任何值的变量类型,比如java中是null,是一个关键字而不是一个类型的实例,因此他不能调用任何方法,为什么会返回一个关键字而不是一个类型的实例,当需要一个实例时?
当一个值是null而不是任何实例的实例时,对其调用任何方法,则会报NullPointerExceptions,因此接到一个返回值时首先要检查是不是null然后再执行调用,但又如何判断不是null呢?
而scala中的Option可以很好的解决这个问题,它告诉你,返回值可能会没有意义.比如Option[String],指可能会返回一个String类型的值,也可能返回一个没有意义的值.Option有两个子类:Some和None,当返回Some的时候说明有一个正确的值,如果是None则是一个没有意义的值.
val stateCapitals = Map(
"Alabama" -> "Montgomery",
"Alaska" -> "Juneau",
// ...
"Wyoming" -> "Cheyenne"
)
stateCapitals.get("Alabama") res: Some(Montgomery)
stateCapitals.get("Wyoming") res: Some(Cheyenne)
stateCapitals.get("Unknown") res: None
stateCapitals.get("Wyoming").getOrElse("Oops!") res: Some(Cheyenne)
stateCapitals.get("Unknown").getOrElse("Oops2!") res: Some(Oops2!)
Map的get方法返回值类型为Option[T],本例中T为String.如果Option.get返回的结果是Some,则Some.get时返回Some中的值,但是如果Option.get的值是None的话,使用None.get会抛出一个 NoSuchElementException异常,而使用getOrElse则可以有效避免异常.
密封类
上面讨论的Option有一个很好的特性:只有两个有效的子类型Some和None.这是一种很好的设计方法,要么这样,否则那样,没有第三种情况出现.
scala中的关键字sealed可以实现这样的目标:
sealed abstract class Option[+A] ... { ... }
sealed关键字告诉编译器所有的子类必须在当前文件声明.Scala标准库中的Option的两个子类即是在Option类文件中实现.