【Scala】基础3:函数式编程

Scala是一门多范式编程语言,函数式编程是Scala的一种重要特性

最基本的Scala函数是可重用的命名表达式,定义函数使用def关键字def 函数名(参数列表):返回值类型 = {函数体}

定义无输入的函数

例如:def hi = {"hi"} 或 def hi() = {"hi"}

定义没有参数的函数时括号是可选择的。如果定义时有括号,则调用时的括号也是可选择的;但如果定义时没有括号,调用时一定不能用括号。

定义函数时指定返回类型:def hi():String = {"hi"}

定义函数

函数的函数体基本上由表达式或表达式块组成,在这里最后一行将成为表达式的返回值,相应地也就是函数的返回值。

有些情况可能需要在函数的表达式块结束前退出并返回一个值,可以采用return关键字来指定函数的返回值,然后退出函数。

定义函数时参数必须加类型

定义函数时指定参数

def info(name:String,age:Int):String = {
   s"${name}今年${age}岁"
}

用命名参数调用函数

调用函数的惯例是按原先定义时的顺序指定参数。在Scala中,还可以按名调用参数,这样就允许不按顺序指定参数。

举例:

def greet(prefix:String,name:String) = s"$prefix $name"

def main(args: Array[String]): Unit = {

    println(greet("Ms","Brown"))

    println(greet(name="Brown",prefix="Ms"))
  
}

定义参数的默认值

定义参数默认值后,调用函数时仍可以指定参数的值,若没有指定就使用默认值。

def greet1(prefix:String="Ms",name:String) = s"$prefix $name"
println(greet1(name="Brown"))

def greet2(name:String ,prefix:String="Ms") = s"$prefix $name"
println(greet2("Brown","Mr"))

可变参数 

要标志一个参数匹配一个或多个输入实参,在函数定义中需要该参数类型后面增加一个星号。

def sum(items:Int*):Int= {
    var total = 0
    for(i <- items) total +=i
    total
}

println(sum(1,2,3,4,5))

参数组

可以把参数表分解为参数组,每个参数组分别用小括号分隔。相当于链式结构

调用时的格式与定义时相对应,也使用小括号分隔开

参数组适用于后面会提到的柯里化

def max(x:Int)(y:Int) = if(x>y) x else y

println(max(10)(20))

使用表达式块调用函数

通过使用表达式块调用函数,可以完成一些计算、验证和其他动作,然后利用这个块的返回值调用函数。

举例:

def formatEuro(amt:Double) = f"$amt%.2f"

println(formatEuro(3.4646))
	
println(formatEuro{val rate=1.32;0.235+0.7123+rate*5.32})

递归函数

递归函数就是调用自身的函数,可能要检查某类参数或外部条件来避免函数调用陷入无限循环。

举例:求x的n次方

def power(x:Int,n:Int):Long = {
    if (n>=1) x*power(x,n-1)
    else 1
}

尾递归

Scala编译器可以用尾递归优化一些递归函数,使得递归调用不使用额外的栈空间,使用当前的栈空间。

必须最后一个语句是递归调用的函数才能完成尾递归优化,而且需要在函数上方加上指定的注解

上面求x的n次方的例子需要修改,将递归语句放到最后一句,如下:

@annotation.tailrec
def power(x:Int,n:Int):Long = {
    if(n<1) 1
    else x*power(x,n-1)
}

但是这样还不行,因为最后一个执行的语句是x和一个返回值的乘法,递归函数没在末尾,还需要修改。

@annotation.tailrec
def power(x:Int,n:Int,t:Int=1):Long={
    if(n<1)t
    else power(x,n-1,x*t)
}

嵌套函数

有些情况下,需要在一个方法中重复某个逻辑,但是把它作为一个外部方法又没有太大意义。对于这种情况,就可以在函数中定义另一个内部函数,这个内部函数只能在该函数中使用。

举例:求三个数的最大值

def outterFunc(a:Int,b:Int,c:Int) = {
    def innerFunc(x:Int,y:Int) = if (x>y) x else y
    innerFunc(a,innerFunc(b,c))
}

首类函数

函数式编程的一个关键是函数应当是首类的。

“首类”表示函数不仅能得到声明和调用,还可以作为一个数据类型用在这个语言的任何地方。 

那么定义一个函数类型的值或变量,函数类型要怎么表示呢?

——答案就是!函数的类型是其输入类型和返回值类型的一个简单组合,由一个箭头(=>)从输入类型指向输出类型。当参数有两个及以上时必须使用括号

//定义一个函数info
def info(name:String,age:Int):String = {
    s"${name}今年${age}岁"
}

//定义一个值f,是info函数类型的
//既然是一个函数类型的值,那么就可以当作函数去用
//info怎么使用,f就怎么使用
val f:(String,Int) => String  = info

还有一种方法是使用通配符去定义函数类型的值或变量,但是这样代码可读性会降低

val fNew  = info _

高阶函数

以函数作为参数或返回值的函数。

举例:

def safeStringOp(s:String,f:String=>String)={
    
    if(s==null) s else f(s)
}
  
def reverser(s:String) = s.reverse
  
println(safeStringOp(null,reverser))
  
println(safeStringOp("ready",reverser))

匿名函数

没有名字的函数,定义匿名函数的语法很简单,箭头(=>)左边是参数列表,右边是函数体,而且当匿名函数作为参数时,匿名函数的参数类型可以省略

//正常定义的max函数
def max(x:Int,y:Int) = if(x>y) x else y
	
//赋至一个函数值
val maximize :(Int,Int) => Int = max
	
//用匿名函数定义
val maximize = (x:Int,y:Int) => if(x>y) x else y


def safeStringOp(s:String,f:String=>String)={

    if(s==null) s else f(s)
}  
	
println(safeStringOp(null,(s:String) => s.reverse)
   	
println(safeStringOp("ready",(s:String) => s.reverse))

匿名函数中还可以使用通配符(_)来替换参数,但是通配符的使用有两个条件:

       ①有几个参数必须使用几个参数,且一个参数只能使用一次    

       ②必须按顺序使用

def safeStringOp(s:String,f:String=>String)={
    	
    if(s==null) s else f(s)
  	
}  
	
println(safeStringOp(null,_.reverse)
   	
println(safeStringOp("ready",_.reverse))

柯里化

调用函数时(包括常规函数和高阶函数),通常要在调用中指定函数的所有参数。如果想保留一些参数不想再次输入,除了使用默认值以外还可以怎么做呢?

举例:

def factoror(x:Int,y:Int) = y % x == 0    //定义一个函数

val f = factoror _    // 所有参数都不保留

val x = f(7,20)    //调用

val multipleof3 = factoror(3,_:Int)    //如果想保留一些参数,可以部分应用这个函数,使用通配符替代其中一个参数。

val x = multipleof3(78)    //调用

要部分应用函数,还有一种更简洁的方法,可以使用有多个参数表的函数。应用一个参数表中的参数,另一个参数表不应用。这种技术称谓函数柯里化。

举例: 

def factoror(x:Int)(y:Int) = y % x == 0

//将x固定成2,要是想固定y可以使用命名参数或者更改函数定义时的顺序
val isEven = factoror(2) _    

val z = isEven(32)

传名参数和传值参数

       Scala的解释器在解析函数参数时有两种方式:一种是先计算出参数表达式的值,再应用到函数内部;另一种是直接将未计算的参数表达式带入到函数内部,什么时候用到这个参数,什么时候计算表达式。前者叫做传值调用(call-by-value),后者叫做传名调用(call-by-name)。

       传值调用在进入函数体之前就对参数表达式进行了计算,这避免了函数内部多次使用参数时重复计算其值,在一定程度上提高了效率。但是传名调用的一个优势在于,如果参数在函数体内部没有被使用到,那么它就不用计算参数表达式的值了。在这种情况下,传名调用的效率会高一点。

       简单来说,用 => 定义传名参数,只传入了一个表达式,在调用时才会去执行,使用 函数名 调用;

                        用 () => 定义传值参数,传入计算后的值, 使用 函数名() 调用

var flag: Boolean = true
  
def useOrNotUse(x: Int, y: => Int) = {    //将y定义为传名参数
    
    flag match{
      
        case true => x
      
        case false => x + y
    }

}

举例:结合下面代码的输出,理解传名和传值的执行顺序

def timeByValue(t: () => Long) = {    //定义参数为传值函数
    println("valueStart...")
    println( t() )    //调用时使用()
    println("valueStop...")
}
def timeByName(t: => Long) = {    //定义参数为传名函数
    println("nameStart")
    println(t)    //调用时不可以加()
    println("nameStop")
}
def time() = {
    println("获取时间内, 单位为 纳秒")
    System.nanoTime()
}
def main(args: Array[String]): Unit = {
    timeByValue({println("传值顺序");time;})
    timeByName({println("传名顺序");time;})
}

猜你喜欢

转载自blog.csdn.net/hr786250678/article/details/86301980