Scala函数式编程基础
函数式编程把函数当做一等公民,充分利用函数、 支持的函数的多种使用方式。比如:在Scala当中,函数是一等公民,像变量一样,既可以作为函数的参数使用,也可以将函数赋值给一个变量. 函数的创建不用依赖于类或者对象,而在Java当中,函数的创建则要依赖于类、抽象类或者接口.
函数式编程中,将函数也当做数据类型,因此可以接受函数当作输入(参数)和输出(返回值)。(增强了编程的粒度)
1)函数/方法的定义
def 函数名 ([参数名: 参数类型], ...)[[: 返回值类型] =] {
语句... //完成某个功能
return 返回值
}
(1)函数声明关键字为def (definition)
(2)[参数名: 参数类型], ...:表示函数的输入(就是参数列表), 可以没有。 如果有,多个参数使用逗号间隔
(3)函数中的语句:表示为了实现某一功能代码块
(4)函数可以有返回值,也可以没有
(5)返回值形式1: // def 函数名(参数列表) : 数据类型 = {函数体} // 返回值确定,清晰
(6)返回值形式2: // def 函数名(参数列表) = {函数体} // 有返回值, 类型是推断出来的
(7)返回值形式3: // def 函数名(参数列表) {函数体} // 无返回值 Unit
(8)如果没有return ,默认以执行到最后一行的结果作为返回值
def sum(n1: Int, n2: Int): Int = {
return n1 + n2
}
---------------------------------------------------------------------------------------------------------
(1)函数的形参列表可以是多个, 如果函数没有形参,调用时 可以不带()
(2)形参列表和返回值的数据类型可以是值类型和引用类型。
(3)Scala中的函数可以根据函数体最后一行代码自行推断函数返回值类型。那么在这种情况下,return关键字可以省略
(4)因为Scala可以自行推断,所以在省略return关键字的场合,返回值类型也可以省略
(5)如果函数明确使用return关键字,那么函数返回就不能使用自行推断了,这时要明确写成 : 返回类型 = ,当然如果你什么都不写,即使有return 返回值为()
(6)如果函数明确声明无返回值(声明Unit),那么函数体中即使使用return关键字也不会有返回值
(7)如果明确函数无返回值或不确定返回值类型,那么返回值类型可以省略(或声明为Any)
(8)Scala语法中任何的语法结构都可以嵌套其他语法结构(灵活),即:函数/方法中可以再声明/定义函数/方法,类中可以再声明类。
(9)递归函数未执行之前是无法推断出来结果类型,在使用时必须有明确的返回值类型
2)默认值的参数
(1)Scala函数的形参,在声明参数时,直接赋初始值(默认值),这时调用函数时,如果没有指定实参,则会使用默认值。如果指定了实参,则实参会覆盖默认值。
(2)如果存在多个参数,每一个参数都可以设定默认值,那么这个时候,传递的参数到底是覆盖默认值,还是赋值给没有默认值的参数,就不确定了(默认按照声明顺序[从左到右])。在这种情况下,可以采用带名参数
def f6 ( p1: String = "v1", p2: String ) {
println(p1 + p2);
}
f6("v2" )
f6(p2="v2")
3)可变参数
//支持0到多个参数
def sum(args: Int*) : Int = { }
//支持1到多个参数
def sum(n1: Int, args: Int*) : Int = { }
(1)args 是集合, 通过 for循环 可以访问到各个值。
(2)可变参数需要写在形参列表的最后。
4)方法和函数
方法转为函数
def f1(): Int = {100}
println(f1)
var f2 = f1
var f3 = f1 _
println(f3) // 打印的是100吗?
// 如果要打印f3函数的返回值
println(f3())
5)匿名函数
val f1 = () => “abc” // f1是什么
// 输入字符串和Double值, 变成整数求和.
// 完整的写法:
val f2: (String, Double) => Int = (a: String, b : Double) => a.toInt + b.toInt
// 简化版1, 后面的函数写法省略全部类型
val f3: (String, Double) => Int = (a, b) => a.toInt + b.toInt
// (String, Double) => Int 是一个数据类型(函数签名)
// 简化版2 , 省略函数签名
Val f4 = (a: String, b:Double) => a.toInt + b.toInt
6)传值调用与传名调用
var money = 100
def buy(): Int = {
money -= 10
money
}
def test1(a: Int) = {
println(a)
println(a)
}
def test2(a: => Int) = {
println(a)
println(a)
}
def main(args: Array[String]): Unit = {
test1(buy)
test2(buy)
}
7)高阶函数
将其他函数作为参数或返回值为一个函数的函数函数(higher-order function)
// 函数的第一个参数类型是另一个函数
def apply(f: Int => String, v: Int) = f(v)
def fmtInt(n: Int) : String = "[整数值{" + n + "}]"
def main(args: Array[String]): Unit = {
println(apply(fmtInt, 1200))
}
// 函数的返回值是一个函数
def addBy(n: Int) = {
(d : Double) => n + d
}
def main(args: Array[String]): Unit = {
println(addBy(50)(80.223))
}
8)函数柯里化(Currying)
柯里化指的是将原来接受多个参数的函数变成新的接受一个参数的函数的过程, 新函数的参数接受原来的第二个参数为唯一参数, 如果有n个参数, 就是把这个函数分解成n个新函数的过程
// 原始函数, 有3个参数的函数
def addMulti(a: Int, b: Int, c: Int) = (a + b) * c
// 函数A的返回值是一个函数B, 函数B的返回值是函数C
def addMulti(a: Int) = {
(b: Int) => (c: Int) => (a + b) * c
}
def main(args: Array[String]): Unit = {
println(addMulti(50)(80)(20))
}
-----------------------------------------------------------------------
比较两个字符串在忽略大小写的情况下是否相等, 注意,这里是两个任务:
1)全部转大写(或小写)
2)比较是否相等
方式1: 简单的方式,使用一个函数完成.
def eq2(s1: String)(s2: String): Boolean = {
s1.toLowerCase == s2.toLowerCase
}
//方式2:使用稍微高级的用法(隐式类):形式为 str.方法()
//我们认为比较字符串是否相等是两件事,先转成小写[函数], 再比较是否相等[函数]
def eq(s1: String, s2: String): Boolean = {
s1.equals(s2)
}
implicit class TestEq(s: String) {
def checkEq(ss: String)(f: (String, String) => Boolean): Boolean = {
f(s.toLowerCase, ss.toLowerCase)
}
}
str1.checkEq(str2)(_.equals(_))
9)参数(类型)推断
参数类型推断写法说明/规则
(1)参数类型是可以推断时,可以省略参数类型
(2)当传入的函数,只有单个参数时,可以省去括号
(3)如果变量只在=>右边只出现一次,可以用_来代替
val list = List(1, 2, 3, 4)
println(list.map((x:Int)=>x + 1)) // map是一个高阶函数,因此也可以直接传入一个匿名函数,完成map
println(list.map((x)=>x + 1)) // 当遍历list时,参数类型是可以推断出来的,可以省略数据类型Int
println(list.map(x=>x + 1)) // 当传入的函数,只有单个参数时,可以省去括号
println(list.map(_ + 1)) // 如果变量只在=>右边只出现一次,可以用_来代替
val res = list.reduce(_+_)
10)过程
将函数的返回类型为Unit的函数称之为过程(procedure),如果明确函数没有返回值,那么等号可以省略
(1)注意区分: 如果函数声明时没有返回值类型,但是有 = 号,可以进行类型推断最后一行代码。这时这个函数实际是有返回值的,该函数并不是过程。(这点在讲解函数细节的时候讲过的.)
(2)开发工具的自动代码补全功能,虽然会自动加上Unit,但是考虑到Scala语言的简单,灵活,最好不加.
11)惰性函数
惰性集合在需要时提供其元素,无需预先计算它们,这带来了一些好处。首先,您可以将耗时的计算推迟到绝对需要的时候。其次,您可以创造无限个集合,只要它们继续收到请求,就会继续提供元素。
当函数返回值被声明为lazy时,函数的执行将被推迟,直到我们首次对此取值,该函数才会执行。这种函数我们称之为惰性函数
def main(args: Array[String]): Unit = {
lazy val res = sum(10, 20)
println("-----------------")
println("res=" + res)
}
def sum(n1 : Int, n2 : Int): Int = {
println("sum() 执行了..")
return n1 + n2
}
(1)lazy 不能修饰 var 类型的变量
(2)不但是 在调用函数时,加了 lazy ,会导致函数的执行被推迟,我们在声明一个变量时,如果给声明了 lazy ,那么变量值得分配也会推迟。 比如 lazy val i = 10
12)异常
Scala提供try和catch块来处理异常。try块用于包含可能出错的代码。catch块用于处理try块中发生的异常。可以根据需要在程序中有任意数量的try...catch块。
try {
val r = 10 / 0
} catch {
case ex: ArithmeticException=> println(“捕获了除数为零的算术异常")
case ex: Exception => println("捕获了异常")
} finally {
// 最终要执行的代码
println("scala finally...")
}
(1)我们将可疑代码封装在try块中。 在try块之后使用了一个catch处理程序来捕获异常。如果发生任何异常,catch处理程序将处理它,程序将不会异常终止。
(2)Scala的异常的工作机制和Java一样,但是Scala没有“checked(编译期或受检)”异常,即Scala没有编译异常这个概念,异常都是在运行的时候捕获处理。
(3)用throw关键字,抛出一个异常对象。所有异常都是Throwable的子类型。throw表达式是有类型的,就是Nothing,因为Nothing是所有类型的子类型,所以throw表达式可以用在需要类型的地方
def main(args: Array[String]): Unit = {
val res = test()
println(res.toString)
}
def test(): Nothing = {
throw new Exception("不对")
}
(4)在Scala里,借用了模式匹配的思想来做异常的匹配,因此,在catch的代码里,是一系列case子句来匹配异常。【前面案例可以看出这个特点, 模式匹配我们后面详解】,当匹配上后 => 有多条语句可以换行写,类似 java 的 switch case x: 代码块..
(5)异常捕捉的机制与其他语言中一样,如果有异常发生,catch子句是按次序捕捉的。因此,在catch子句中,越具体的异常越要靠前,越普遍的异常越靠后,如果把越普遍的异常写在前,把具体的异常写在后,在scala中也不会报错,但这样是非常不好的编程风格。
(6) finally子句用于执行不管是正常处理还是有异常发生时都需要执行的步骤,一般用于对象的清理工作,这点和Java一样。
(7) Scala提供了throws关键字来声明异常。可以使用方法定义声明异常。它向调用者函数提供了此方法可能引发此异常的信息。 它有助于调用函数处理并将该代码包含在try-catch块中,以避免程序异常终止。在scala中,可以使用throws注释来声明异常
def main(args: Array[String]): Unit = {
f11()
}
@throws(classOf[NumberFormatException])
def f11() = {
"abc".toInt
}