Scala学习笔记-核心特性(1)-函数式编程
函数式编程简介
不同范式的对比:
- 面向过程:按照步骤按照顺序解决问题
- 面向对象:分解对象、行为、属性,然后通过对象关系以及行为调用来解决问题。耦合低,复用性高,可维护性强
- 函数式编程:面向对象和面向过程都是命令式编程,但是函数式编程并不关心具体的运行过程,更加关心数据之间的映射关系。纯粹的函数式编程语言中没有变量的概念,所有的量都是常量。整个执行过程就是在不停的进行表达式的求值,每一段程序都有返回值。(易于编程人员理解,相对地编译器处理起来比较复杂)
函数式编程的编程效率更高,并且由于函数式编程的不可变性,对于函数来说,输入固定则输出固定,与环境上下文等无关。利于并行处理,所以特别适用于大数据处理领域。
Scala将函数式编程和面向对象编程融合在一起。
函数基本语法
基本语法
在Scala中,函数定义如下:
1 |
|
其中包括函数名称、函数参数和对应的类型、函数返回类型以及函数体。
函数和方法的区别:
- 完成某一功能的程序语句的集合称为函数
- 类中的函数称为方法
- 函数并没有重载和重写的概念,但是方法有
注意事项:
- Scala语言可以在任何的语法结构中声明任何的语法
- Scala中函数可以嵌套定义,在Java中方法中不能再写方法
- 函数式编程语言中,函数是一等公民,可以像对象一样赋值,作为参数返回值等,可以在任何代码块中定义函数
函数参数
函数参数的情形:有参、无参、有返回值、无返回值(Unit)
可变参数的写法:
1 |
|
- 如果参数列表存在多个参数,则将可变参数放置在最后
在Java中,如果一个方法的参数是可变参数,那么传入一个数组对象也是能够接收的,不过在Scala中,是不能将数组对象传入可变参数,需要而是需要增加
:_*
进行解析,例如我们希望能够使用可变参数:
1
2
3
def main(args: Array[String]): Unit = {
test(args:_*)
}
默认参数和具名参数
1 |
|
- 一般情况下,将有默认值的参数放置在参数列表的后面(不满足顺序也不会报错)
- 在Scala中有默认参数以及具名参数,但是在Java中没有类似的用法
函数至简原则
- return可以省略、Scala会使用函数体的最后一行代码作为返回值
- 如果函数体只有一行代码,可以省略花括号
- 如果返回值类型能够推断出来,则可以省略
- 如果有return,则不能省略返回值类型,必须指定
- 如果函数中明确声明了返回Unit,那么即使函数体中使用了return关键字也不起作用
- Scala如果期望是无返回值类型,可以省略等号(废弃)
- 无参函数声明可以不加括号,调用也可以不加(废弃)
- 不关心函数名称的时候,函数名称和def也可以省略
高阶函数
在Scala中,函数是一等公民,我们可以定义函数、调用函数、将函数作为值传递、将函数作为参数进行传递、将函数作为返回值
- 注意这里函数作为参数,接收的类型该如何书写
1 |
|
匿名函数
没有名称的函数就是匿名函数,也称为Lambda表达式,格式如下:
1 |
|
- 匿名函数定义的时候不能有函数的返回值类型
- 参数列表中的类型可以省略,会根据进行自动的类型推导
- 类型省略之后发现只有一个参数,则圆括号可以省略
- 匿名函数如果只有一行,则大括号也可以省略
- 如果参数在函数体中只出现一次,则参数省略,且后面的参数可以使用
_
进行代替 _
下划线必须按照顺序进行接收,使用下划线的时候必须省略参数列表和箭头
1 |
|
闭包和函数柯里化
闭包:如果一个函数,访问到了它的外部的局部变量的值,那么这个函数和它所处的环境包括那个变量称为闭包。
观察如下示例:
1 |
|
函数f1
的返回值是一个函数,通过f接收这个返回值后可以进行函数调用,因此最后得到的结果是5+10=15
。但是按照常规分析,这里是存在问题的。上面存在函数的嵌套定义,但是并不存在函数的嵌套调用。我们在调用f1()
的时候,并没有执行f2
,只是得到了f2
作为一个函数的返回值,因此此时f1
的栈帧应该已经弹出了,后面再调用f2
的时候理应无法访问到f1
的局部变量,但是这里确确实实访问到了,这就是闭包的效果。
在Scala中,一切皆对象,因此一个函数实际上也是对象。我们调用一个函数,实际上在JVM的堆内存上有一个函数对象,里面存放了局部变量。后续调用f2
的时候,虽然栈帧弹出了,但是堆上的对象还是存在的,因此能够访问到。这里堆上的函数对象,清除的工作依赖于GC机制。
与闭包概念一起出现的还有函数柯里化的概念。
函数柯里化(Currying):指的是将一个参数列表的多个参数,变成多个参数列表的过程。也就是将普通的多参数函数变成高阶函数的过程。例如,上面的函数f1
也可以进行柯里化,写成下面的样子,之后就可以连续调用。这样的写法更符合阅读习惯。
1 |
|
函数递归
递归:函数调用自身
- 函数调用自身
- 必须要有跳出的逻辑
- scala中的递归必须声明函数返回值类型
在递归过程中,往往会导致栈中空间被大量占用。但是在一种情况下,Scala中可以对栈的占用进行优化,即尾递归的情形。
在传统的递归中,典型的模型是首先执行递归调用,然后获取递归调用的返回值并计算结果。这种方式下,在每次递归调用返回之前,我们并不能得到计算结果。如果一个函数所有的递归形式的调用都出现在函数的末尾,则这种情况称为尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性可以使得在递归过程中没有保存栈帧的必要,可以通过覆盖当前的栈帧而不是在其之上重新添加,因此所使用的栈空间就大大缩减,这使得实际的运行效率更高。
对尾递归的优化依赖于语言,递归可以写成尾递归的形式,在Scala中会对尾递归进行优化,Java中并没有这样的机制。
1 |
|
其中的注解tailrec
会检查下面的递归函数是否满足尾递归的形式,如果不满足则会报错。在IDEA中也会智能检查尾递归和普通递归,函数旁边会显示不同的标志。
控制抽象
在Scala中存在值调用和名调用两种方式
- 值调用:按值传递参数,计算值之后再进行传递
- 名调用:按名称传递参数,直接使用实参替换函数中使用形参的地方,相当于直接在预处理的时候替换,可以传递代码块
1 |
|
对应的输出结果如下:
1 |
|
注意这里名调用中参数类型的指定,需要使用=>
,并且需要指定代码块或者参数的返回值
我们可以利用名传递来实现一个while关键字的功能,见后面的应用举例
惰性加载
当函数返回值被声明为lazy的时候,函数的执行将被推迟,直到我们首次使用这个值,该函数才会被执行。这种函数被称为惰性函数。(注意,lazy不能修饰var类型的变量,只能修饰函数的返回值)
1 |
|
运行结果如下:
1 |
|
函数应用举例
模拟Map映射、Filter过滤和Reduce聚合
1 |
|
利用同一个函数实现在整数上的四则运算
1 |
|
利用名传递实现while关键字的功能
1 |
|