一、函数和闭包
1.本地函数
概念上来说就是函数中的函数,类似于本地变量。访问的范围也同样局限于定义的函数中
2.函数字面量
简单来说就是可以把函数当字面量一样的定义和使用,例如可以定义(x:Int) => x + 1这样的匿名函数,也可以把它赋给某个变量 val increase = (x:Int) => x + 1,然后使用increase(10)这种方式进行调用。当定义的函数有多条语句时可以使用花括号,例如:
(x:Int) =>
{
println("Hello")
x + 1
}
当函数字面量被当作参数传入别的函数中时,某些时候可以使用一些简写的方式,例如:
val numbers = List(1,2,3,4,5,6)
/**下面的函数字面量中x的类型可以被推断出来,所以类型声明可以省略,x外的括号也可以被省略,写成 numbers.filter(x => x > 3)*/
numbers.filter((x) => x > 3)
上述的函数字面量还可以使用占位符方式进行更简写的方式进行简化,当函数字面量中的参数只在字面量函数中出现一次,即可使用下划线代替这个参数,即上面的方式还可以简化为:
numbers.filter(_ > 3)
当你自定义函数字面量而没有作为参数传入某个函数时,scala的类型推断是不起作用的,例如:val test = _+_
,这样的定义会报错,可以在下划线后加入类型声明,val test = _:Int+_:Int
,此时的两个下划线代表两个参数,而不是同一个。
3.部分应用函数
下划线不仅可以只替代一个参数,还可以替代整个的参数列表,例如定义如下的函数
def sum(a:Int,b:Int,c:Int) = a + b + c
val a = sum _
a(1,2,3)
以上的程序中,sum _是一个部分应用函数,下划线代表着多个参数,本例中是三个。然后赋值给a,然后传给a三个参数。在编译时scala会生成一个a的单例对象,对象中有apply方法包含三个参数。相当于调用,a.apply(1,2,3)还可以把sum包装成另外一种只需要一个,或者两个参数的函数,比如
val b = sum(1,_:Int,3)
b(2)//相当于b.apply(1,2,3)
4.闭包
其实这个概念我一直讲不太清楚,还是举个书上的例子吧。比如定义一个函数字面量:val a = (x:Int) => x + other直接这么定义的话肯定会报错,因为找不到other这个变量。但下面这种定义就可以:
var other = 10
val a = (x:Int) => x + other
像上面这样定义的在运行时创建的函数值,被称为闭包。原因other在函数字面量定义时被称作*变量,而在运行时根据函数字面量创建函数值时需要捕获这个*变量才能完成函数值的创建。在捕获*变量前的函数字面量被称为开放项,而捕获*变量创建函数值的这个过程即是关闭这个开放项,所以被称为闭包。而捕获的这个*变量是捕获*变量的本身,并不是它指向的值。简单来讲就是即使闭包创建之后,other变量指向的值改变了,即other = 20,则a(10)的值也会变为30。
外面的other的改变影响了闭包之内函数值的变化,同样闭包内的变化也可以影响外面的变量。比如:
val arr = Array(1,2,3,4,5)
var sum = 0
//处于外面的sum变量在sum闭包中被改变,也影响到了外面的sum变量
arr.foreach(sum += _)
如果闭包创建了多个备份,且被调用了多次,则使用的实例是在闭包被创建时活跃的。例如:
def increaser(a:Int) = (x:Int) => a + x
val inc10 = increaser(10)
val inc20 = increaser(20)
//此时inc10上a绑定的值是10,inc20上面绑定的值是20
println(inc10(10))//20
println(inc20(20))//40
5.可变长度参数
函数的参数列表中有多个相同类型的参数时,可以使用类型后加一个星号进行简化,例如:
def test(args:String*){}
test("a","b","c")
/**在test内部args被转化为一个数组,但如果直接给test一个数组的话会报错,需要传数组的话,可以使用下面这种方式传*/
//_*这个符号告诉编译器将arr的每个元素当作参数传递
test(arr:_*)
6.尾递归
和普通的递归相比,尾递归的最后一次调用是递归函数本身。和尾递归相比递归每次都使用大量的栈空间来创建自身的调用,但尾递归可以只调用函数本身。比如
//此示例不属于尾递归,因为最后一次调用函数之后执行了别的操作
def boom(x:Int):Int={
if(x == 0) throws new Exception("boom!")
else boom(x - 1) + 1
}
//这次就是尾递归了,最后一次调用为函数本身,且没有多余操作
def boom(x:Int):Int={
if(x == 0) throws new Exception("boom!")
else boom(x - 1)
}
但尾递归优化由于scala运行在JVM上,导致局限性很大。scala中的尾递归优化只能应用于每次递归调用都是调用函数自身的程序,例如下面两个例子这样也是无法应用优化的。
//两个函数间相互调用
def isEven(x:Int):Boolean = if(x == 0) true else isOdd(x - 1)
def isOdd(x:Int):Boolean = if(x == 0) false else isEven(x - 1)
//调用函数值
val funValue = nestedFun _
def nestedFun(x:Int){
if(x != 0){println(x);funValue(x - 1)}
}
二、控制抽象
1.减少代码重复
就是使用函数值这样的高阶函数来尽量减少重复的代码工作,举个书中的栗子,要遍历一个文件列表,从中找出符合条件的文件,如下:
object FileMatcher{
private def filesHere = new java.io.File(".").listFiles
private def fileMatching(matcher:(String,String) => Boolean)={
for (file <- filesHere; if matcher(file.getName)) yield file
}
def filesEnding(query:String)={
fileMatching(_.endsWith(query))
}
def filesContaining(query:String)={
fileMatching(_.contains(query))
}
def filesRegex(query:String)={
fileMatching(_.matches(query))
}
}
上面的单例对象对外提供了三个功能,分别可以查找以某些字符结尾的,包含某些字符的和匹配正则的文件。函数中使用了传递函数值和闭包,如果使用Java的话,上面的工作量就会不小了。
2.柯里化
指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。例如:
def curriedSum(x:Int)(y:Int) = x + y
//调用
curriedSum(1)(2)
//实际的调用过程是调用了两个普通函数,如下
def first = (x:Int) => x + y
def second = first(1)
second(2)
*有一个小技巧的地方是,当函数只有一个参数的时候,调用函数的时候小括号()可以替换为花括号{}
3.传名参数
与函数值不同,传名参数没有参数的只有表达式。例如一个断言架构,设置一个标志位,当标志位为假时,什么都不做,为真时则返回表达式的值:
var flag = true
//传名参数的定义,不包含参数,使用=>开头后加返回类型
def byNameAssert(predicate: => Boolean)={
if(flag) predicate
}
println(byNameAssert(1 > 2))
flag = false
println(byNameAssert(1 > 2))
三、组合与继承
1.抽象类
定义的语法同Java一样,使用abstract开头。唯一不同的地方是抽象类中的抽象方法不需要使用abstract开头来定义,和普通方法定义一致,但不需要写方法体。 例如 def test(a:Int):Int
2.定义无参方法
定义:def test:Int = 10
使用惯例,只要方法中没有参数且方法仅能够通过所包含对象的属性去访问可变状态(方法不能改变可变状态),就使用无参方法。使用另一种描述是如果函数执行了操作即使没有参数也应该带一个空括号,如果函数仅仅是读取了某个属性值,则可以省略空括号。为的是客户端代码调用时的功能明确性。
无参方法和字段的区别,一是访问速度方面,字段更快一些,因为字段在类初始化时就计算好了值,而方法则是在每次调用时进行计算。另一方面是字段会占用比方法更多的内存空间
3.重写方法和字段
子类可以重写父类的无参方法或非私有字段。
Java中有四个命名空间字段、方法、类型和包,scala中只有两个值(字段、方法、包和单例对象)和类型(类和物质名)。所以在scala中不允许同一个类中出现同名的字段和方法
scala中重写父类方法需要override关键字,在实现抽象方法时则不需要。
4.调用超类构造器
这个比较简单,就举一个例子
class A(a:Int){}
class B(b:Int,c:Int) extends A(a)
四、层级
1.scala的类层级
scala中所有的类都是Any的子类。继承关系如下图。Any中默认定义了一些常用的方法,比如==、!=、equals、hashCode、toString。其中==和!=被定义为final不能被子类重写
Any有两个子类,一个是AnyVal另一个是AnyRef。AnyVal是scala内建值的基类。除8个基本类型(与Java中的基本类型对应)外,Unit类似于Java中的void,表示没有任何返回值,Unit只有一个实例值是()。一些基本类型的的操作,比如 1 to 5,像这种基本类中没有的方法就会通过隐式转换,将Int转换为了RichInt类型。
另一个AnyRef是所有引用类型的基类,类似于Java中的Object。在scala中的类还继承自一个名为ScalaObject的特质,希望通过其中包含的编译器定义和实现加速scala程序。目前物质中只有一个方法$tag,在内部使用加速模式匹配。
2.底层类型
scala类层级中有两个底层类型Null和Nothing。Null是所有引用类型的子类,不兼容值类型。Nothing则是所有类型的子类型,但并没有任何值,它的作用之一是标明不正常终止,例如scala标准库中的Perdef对象有一个方法,定义如下:
def error(message:String):Nothing={throw new RuntimeException(message)}
//因为error的返回类型为Nothing,这个方法可以在任何返回类型的方法中调用
def test(a:Int,b:Int):Int={if (a > b) a else error("exception")}