函数式编程
面向对象和面向过程的区别
- 设计思路——面向过程的编程以事件或任务为中心,关注于如何通过一系列步骤来完成特定的任务。它强调的是任务执行的顺序和流程。相比之下,面向对象的编程则以对象为中心,将数据和相关操作封装在一起,通过对象之间的交互来完成任务。面向对象设计强调的是对象的职责和交互。
- 代码组织——在面向过程的编程中,代码通常按照功能模块或过程来组织,每个过程负责一部分特定的任务。而在面向对象的编程中,代码围绕对象来组织,每个对象包含数据(属性)和行为(方法),对象的设计反映了现实世界中的实体和概念。
- 抽象层次——面向过程的编程通常关注于具体的操作和算法,而面向对象的编程则更侧重于高层次的抽象,通过类和对象来模拟现实世界的实体和它们之间的关系。
- 可维护性——面向过程的代码由于其紧密耦合的特点,修改一处可能会影响到整个系统。而面向对象的代码通常更容易维护和扩展,因为对象内部的修改不会影响到其他对象。
- 总——面向过程适用于解决明确、线性的问题,而面向对象则更适合处理复杂的、非线性的、需要高度抽象和模块化的问题。在实际开发中,两者往往会结合使用,以达到最佳的开发效果。
函数式编程是一种将计算视为函数运算的编程范式。它的核心在于使用函数作为主要的构建块,并强调没有副作用的函数和不可变性的数据结构,函数式编程尽量避免在程序中使用共享的状态和可变的数据,这有助于减少并发问题和提高程序的可预测性。不可变性的特点特别适合大数据的分布式并行处理的特点。
为完成某一功能的程序语句的集合,称为函数。类中的函数称之为方法。
def sum( x : Int , y : Int ) : Int = {
x + y
}
// def是定义函数的关键字,sum是函数的函数名,x、y是参数名,Int是参数类型,Int是函数返回值类型,x+y是函数体。
def fdk (name : String ) : String = {
println(“我是” + name)
Return 你好 “name”
}
fkd (name = “fdk” ) //赋值name
println( fdk ( ) ) //打印出方法和返回值
(name : String )是参数,你好 “name”是返回值
函数参数存在可变参数,如果参数列表存在多个参数,那么可变参数一般放置在最后。
可变参数在数据类型后面加 *
def f1 ( str : String* ) : Unit = {
如果参数列表存在多个参数,那么可变参数一般放置在最后,传入参数时易于判断归属
def f2 ( str1 : String , str2 : String*) : Unit = {
参数默认值,一般将有默认值的参数放置在参数列表的后面
def f3(name: String = "atguigu"): Unit = { //定义默认值参数atguigu
println("My school is" + name)
}
f3(name="school")
f3() //当没有传入数值时,值为默认值
带命参数
ef f4(name: String, age: Int): Unit = {
println(s"${age}岁的${name}在电专学习")
}
f4(name="alice", age=20)
f4(age=20,name="bob") //直接对参数名称传入数据,不用再按顺序
函数至简原则
- return可以省略,Scala会使用函数体的最后一行代码作为返回值
def f1(name: String): String = {
name
}
println(f1( name = "atguigu" ))
- 如果函数体只有一行代码,可以省略大括号
def f2(name: String): String = name
println(f2( name = "atguigu" ))
- 返回值类型如果能推断出来,那么可以省略
def f3(name: String) = name
println(f3( name = "atguigu" ))
- 如果有return,则不能省略返回值类型,必须指定
def f4(name: String) : String = { //必须加返回数据类型,否则返回数据类型为Nothing
return name //用了return
}
println(f4( name = "atguigu" ))
- 如果函数明确声明unit,那么即使函数体中使用return关键字也不起作用
def f5(name: String) : Unit = {
return name //返回一个空括号,return不起作用
}
- Scala如果期望是无返回值类型,可以省略等号
def f6(name: String){ //不给返回值类型,就是无返回值类型,空括号
Println(name)
}
- 如果参数无参,但是声明了参数列表,那么调用时,小括号,可以不加
def f7() : Unit = {
println("atguigu" )
}
f7() //正常调用
f7 //不加小括号的调用,正常运行
- 如果函数没有参数列表,那么小括号可以省略,调用时小括号必须省略
def f8 : Unit = { //省略了函数名后的小括号
println("atguigu" )
}
f8() //调用时小括号必须省略,否则会报错
f8
- 如果不关心名称,只关心逻辑处理,那么函数名可以省略
(name: String) => { println(name) } //匿名函数,lambda表达式
匿名函数的简化原则
定义了一个函数,要处理的数据定死,传入操作,以函数作为参数输入。
def f(func : String => Unit) :Unit = {
func("atguigu") //数据定死
}
f ( (name:String) => { println(name) } ) //传入操作
- 参数的类型可以省略,会根据形参进行自动的推导
f( (name) => {println ( name } )
- 类型省略之后,发现只有一个参数,则圆括号可以省略,没有参数或多个参数,圆括号不能省略。
f( name => {println ( name } )
- 匿名函数如果只有一行,则大括号可以省略
f( (name) => println ( name )
- 如果参数只出现一次,则参数省略且后面参数可以一用_代替
f( println (_) )
- 如果可以判断出,当前传入的println是一个函数体,而不是调用语句,可以直接省略下划线
f( println )
匿名函数
没有名字的函数就是匿名函数
( x:Int )=>( 函数体 )
x为参数名称,Int为数据类型
实际实例,定义一个“二元运算”函数,只操作1和2两个数,具体运算通过参数传入
def F1(fun: (Int,Int)=>Int ):Int ={
fun(1,2)
}
val add = (a:Int,b:Int) => a+b
val minus= (a:Int,b:Int) => a-b
println(F1(add))
println(F1(minus))
简化上方匿名函数
println( F1( (a:Int,b:Int) => a+b) ) //一次简化
println( F1( (a,b) => a+b) ) //二次简化
println( F1( _+_ ) ) //三次简化
函数高阶用法
- 函数作为值进行传递,f是一个函数
val f1: Int=>Int = f //第一种写法,f1是新函数,第一个Int是参数的类型,第二个Int是返回值的类型,f是被接收的函数
val f2 = f _ //第二种写法,f2是新函数,f后面加下划线,表示f是个函数,而不是其他
- 函数作为参数进行传递
参数定义死,方法传入的以前以学
定义一个参数和方法都传入的二元计算函数
def dualEval(op: (Int, Int)=>Int,a:Int,b:Int):Int={
op(a,b)
}
传入数据和方法的方式
-
- 先定义方法,再将具体数据传入方法,,方法再传入函数
def add(a:Int,b:Int): Int={
a + b
}
println(dualEval(add,12,35))
-
- 将上面的方法简化,再传入数值
println(dualEval( (a , b) => a+b ,12,35) )
-
- 再一步将上面方法简化
println(dualEval(_+_,12,35))
3.函数作为函数的返回值返回
def f5():Int=>Unit = {
def f6(a:Int):Unit = {
println("f6调用"+a)
}
f6 //将函数直接返回
}
第一种调用方法
val f6=f5()
println(f6)
println(f6(25))
第二种调用方法
println(f5()(25))
对数组进行处理,将操作抽象出来,处理完毕之后返回一个新的数组
val arr:Array[Int]=Array(11,21,31,41,51)
//对数组进行处理,将操作抽象出来,处理完毕之后的结果返回一个新的数组
def sss(ar:Array[Int],op:Int=>Int):Array[Int]={
for(elem<-ar)yield op(elem)
}
思路:传入两个参数,数组和方法,再在函数中运算,再输入进一个新的数组中。
首先,定义了一个整数数组arr,包含5个元素:11, 21, 31, 41, 51。
接着,定义了一个名为sss的函数,它接受两个参数:一个整数数组ar和一个操作函数op。这个函数的作用是对数组ar中的每个元素应用操作函数op,并返回一个新的数组。
//定义一个加一操作
def addOne(elem:Int):Int={
elem + 1
}
//调用函数
val newArray:Array[Int] = sss(arr,addOne)
println(newArray.mkString ( "," ) )
然后,定义了一个名为addone的函数,它接受一个整数参数elem,并返回elem+1。这个函数的作用是将输入的整数加1。
接下来,调用sss函数,将数组arr和函数addone作为参数传入。这将对数组arr中的每个元素加1,并将结果存储在新的数组newArray中。最后,打印出新数组newArray。
//传入匿名函数,实现元素翻倍
val newArray2 = sss(arr,_ * 2)
println(newArray2.mkString(","))
上述方法的简化,调用sss函数,这次传入一个匿名函数elem => elem * 2,它的作用是将输入的整数翻倍。这将对数组arr中的每个元素翻倍,并将结果存储在新的数组newArray2中。
题:定义一个函数func,它接收一个Int类型的参数,返回一个函数(记作f1),它返回的函数f1,接收一个String类型的参数,同样返回一个函数(记作f2),函数f2接收一个Char类型的参数,返回一个Boolean的值
要求调用函数func(0)(“”)(‘0’)得到的返回值为false,其他情况均返回true
正常写法为:
def func(i:Int):String=>(Char=>Boolean)= {
def f1(s: String): Char => Boolean = {
def f2(c:Char): Boolean={
if (i==0 && s=="" && c=='0') false else true
}
f2
}
f1
}
println( func(0)("")('0') )
简化后的写法为:
def func(i:Int):String=>(Char=>Boolean)= {
s => c => if (i==0 && s=="" && c=='0') false else true
}
println( func(0)("")('0') )
用柯里化,在实际应用中,最常用的方式,数据名后面加的是传入数据类型,括号后面的Boolean是最终返回数据类型
def func(i:Int)(s:String)(c:Char):Boolean= {
if (i==0 && s=="" && c=='0') false else true
}
println( func(0)("")('0') )
函数柯里化&闭包
闭包:如果一个函数,访问到了它的外部(局部)变量的值,那么这个函数和它所处的环境,称为闭包。
函数柯里化:把一个参数列表的多个参数,变成多个参数列表。
闭包的经典用法:
def addByFour(a:Int):Int=>Int ={
def addb(b:Int):Int = {
a+b
}
addb
}
分层输入两个数,平衡了适用性和通用性
可以将第一层的量作为定量,第二层的量作为变量,这样就扩展了通用性。
用lambda表达式实现
def addByA1(a:Int):Int=>Int={
(b:Int) =>{
a+b
}
}
简写上方函数
def addByA2(a:Int):Int=>Int = a + _
柯里化
def addCurrying(a:Int)(b:Int):Int={
a + b
}
闭包和柯里化是绑定的,柯里化的底层一定是闭包,闭包不一定是柯里化
递归
一个函数/方法在函数/方法体中又调用了本身,我们称之为递归调用,在递归调用中一定要有一个基准情形(退出机制)
递归的问题,在计算时中会占用大量栈空间
递归实现计算阶乘
def fact(n:Int):Int={
if(n==0)return 1 //基准情形,return需要放在这里
fact(n-1) * n //调用自身,实现阶乘
}
尾递归实现
将递归的结果赋给一个变量,减少栈消耗
def tailFact(n:Int):Int = {
@tailrec
def loop(n:Int,currRes:Int):Int={
if(n == 0) return currRes
loop(n-1,currRes * n)
}
loop(n,currRes =1)
}
函数体中使用了@tailrec注解,表示该函数使用了尾递归优化,函数内部定义了一个名为Loop的辅助函数,用于实现尾递归,Loop函数接受两个参数,一个是当前的计数器n,另一个是当前的累加结果currRes,在Loop函数中,如果n等于0,则返回currRes;否则递归调用Loop函数,将n减1,并将currRes乘以n作为新的累加结果。
控制抽象
def f1():Int={ //被调用的函数
println("f1调用")
12
}
值调用:把计算之后的值传递过去(之后函数中就是一个值)
def f0(a:Int):Unit={
println("a:"+a) //打印出来为:
println("a:"+a) //f1调用
} //a:12
f0(f1()) //a:12
名调用:把代码传递过去,传递的不再是具体的值,而是代码块(之后函数中就是一个代码块,像一个脚本)
def f2(a: =>Int):Unit={ //打印出来为:
println("a:" + a) //f1调用
println("a:" + a) //a:12
} //f1调用
f2(f1()) //a:12
惰性加载
当函数返回值被声明为lazy时,函数的执行将被推迟,直到我们首次对此取值,该函数才会执行,这种函数我们称为惰性加载。
def sum(a:Int,b:Int):Int={ //打印出来为:
println("3.调用sum") //1.函数调用
a+b //3.调用sum
} //2.result=60
lazy val result:Int= sum(13,47)
println("1.函数调用")
println("2.result="+result)
result常量调用函数sum时,没有立刻执行,而是等到要使用result的时候,再去执行其中的操作(如拖延症,不到最后一刻绝不行动)