第4章 函数与高阶函数

Scala 的核心竞争力:函数是值,可以被传递、组合、柯里化;map / filter / reduce 从此成为日常写法。

函数第5-6周3 讲9 段示例代码

本章学习目标

  • 能定义带默认参数、命名参数、可变参数的函数
  • 能写出匿名函数、函数值、闭包与柯里化函数
  • 会用 map / filter / reduce / flatMap / foreach 处理集合
  • 能解释“函数是一等公民”对代码结构的影响

本章导览

核心内容

4.1

定义函数:默认参数、命名参数、可变参数、单表达式函数

把参数写活:默认值、命名、可变参数与单表达式。

定义带默认参数和命名参数的函数用可变参数接收任意个同类型实参用单表达式写法省掉大括号和 return

知识点

函数定义的基本形状

  • def 名字(参数: 类型): 返回类型 = 表达式,返回类型可省,但公共函数建议写。
  • 参数必须写类型,返回类型可以由编译器推断。
  • 函数体是表达式,最后一行就是返回值,不需要 return。

默认参数与命名参数

  • 默认值写在参数类型后面:prefix: String = "Hi"。
  • 调用时用名字指定参数,顺序可以变:greet(punct = "?", name = "小明")。
  • 命名参数能避免一排同类型实参放错位置。

可变参数

  • Int* 表示 0 个或多个 Int,函数体里当 Seq[Int] 用。
  • 直接传 sum(1, 2, 3);已有集合要展开:sum(xs: _*)。
  • Scala 3 的展开写法是 xs*,2.13 用 : _*。

单表达式函数

  • def square(n: Int) = n * n 不需要大括号。
  • 多行逻辑用大括号,但最后一行仍然负责返回值。
  • 少用 return;它会改变控制流,在嵌套闭包里还可能踩非局部返回的坑。

代码示例

▶ 默认参数 + 命名参数
SCALA
object Args {
  def greet(name: String, prefix: String = "你好", punct: String = "!"): String =
    s"$prefix,$name$punct"

  def main(args: Array[String]): Unit = {
    println(greet("小明"))
    println(greet("小明", punct = "?"))
    println(greet(punct = "。", name = "小红"))
  }
}
👀 运行结果
你好,小明!
你好,小明?
你好,小红。
▶ 可变参数与集合展开
SCALA
object Varargs {
  def sum(xs: Int*): Int = xs.sum

  def main(args: Array[String]): Unit = {
    println(sum(1, 2, 3))

    val nums = List(4, 5, 6)
    println(sum(nums: _*))
  }
}
👀 运行结果
6
15
▶ 单表达式函数与函数当参数
SCALA
object SingleExpr {
  def square(n: Int): Int = n * n
  def sumOfSquares(a: Int, b: Int): Int = square(a) + square(b)

  def main(args: Array[String]): Unit = {
    println(sumOfSquares(3, 4))
    println(List(1, 2, 3).map(square))
  }
}
👀 运行结果
25
List(1, 4, 9)

常见错误

⚠️ 踩坑与解决
  • def sum(xs: Int*) = xs.sum; sum(List(1, 2, 3))
    → 参数是 Int*,不能直接塞一个 List[Int]。;写 sum(List(1, 2, 3): _*),或直接展开成 sum(1, 2, 3)。
  • def f(a: Int = 1, b: Int) = a + b; f(2)
    → 2 会先填给 a,b 没有值;默认参数让调用更难猜。;把无默认值的参数放前面,或写 f(b = 2) 明确指定。

💡 参数超过三个,或连续出现两个同类型参数时,用命名参数,可读性立刻提升。

4.2

函数是值:匿名函数、函数类型、柯里化、闭包

把函数当值传来传去,理解柯里化和闭包。

写匿名函数并看懂 Int => Int定义柯里化函数并做部分应用解释闭包为什么能记住外部变量

知识点

函数类型与函数值

  • Int => Int 表示接收一个 Int、返回一个 Int 的函数类型。
  • val inc = (x: Int) => x + 1 把匿名函数存进变量。
  • 函数值能赋给变量、放进集合、作为参数传入或作为返回值。

方法转函数值

  • def 是方法,不是值;在需要函数值的地方要 eta 展开。
  • 在期望函数类型的位置,编译器会自动展开:List(1, 2).map(square)。
  • 部分应用要写清楚:val add10: Int => Int = add(10),或显式写 add(10)(_)。

柯里化

  • def add(a: Int)(b: Int) 把两个参数拆成两个参数列表。
  • 部分应用:val add10: Int => Int = add(10)。
  • 好处是先把公共参数定下来,再复用到不同输入;不是所有函数都值得柯里化。

闭包

  • 匿名函数可以捕获外部变量,这种“函数 + 捕获环境”就是闭包。
  • 捕获 var 时看到的是最新值;每次调用生成新闭包就各自持有一份状态。
  • 闭包让函数返回后,局部变量仍然活着。

代码示例

▶ 匿名函数与函数类型
SCALA
object FuncValue {
  def main(args: Array[String]): Unit = {
    val inc: Int => Int = x => x + 1
    val add = (a: Int, b: Int) => a + b

    println(inc(41))
    println(add(2, 3))
    println(List(1, 2, 3).map(inc))
  }
}
👀 运行结果
42
5
List(2, 3, 4)
▶ 柯里化与部分应用
SCALA
object Curry {
  def add(a: Int)(b: Int): Int = a + b

  def main(args: Array[String]): Unit = {
    val add10: Int => Int = add(10)
    val add10b = add(10)(_)

    println(add(1)(2))
    println(add10(5))
    println(add10b(7))
  }
}
👀 运行结果
3
15
17
▶ 闭包:每个计数器各自持有状态
SCALA
object Counter {
  def makeCounter(): () => Int = {
    var n = 0
    () => {
      n += 1
      n
    }
  }

  def main(args: Array[String]): Unit = {
    val c1 = makeCounter()
    val c2 = makeCounter()
    println(c1())
    println(c1())
    println(c2())
  }
}
👀 运行结果
1
2
1

常见错误

⚠️ 踩坑与解决
  • val f = _ + 1
    → 占位符 _ 缺少参数类型,编译器不知道它要接收什么。;写 val f: Int => Int = _ + 1,或 val f = (x: Int) => x + 1。
  • val addTen = add(10)(add 是柯里化函数)
    → 缺少第二个参数列表,而且没有期望的函数类型触发自动展开。;写 val addTen: Int => Int = add(10),或 val addTen = add(10)(_)。

💡 先写普通多参数函数;当你发现自己反复传同一组前置参数时,再考虑柯里化。

4.3

高阶函数:map / filter / reduce / flatMap / foreach

用 map/filter/reduce/flatMap 把循环压成一行。

用 map 和 filter 完成一对一变换与筛选用 reduce/fold 做聚合,知道空集合的坑用 flatMap 展开一对多,用 foreach 处理副作用

知识点

map:一对一

  • List(1, 2, 3).map(_ * 2) 得到等长的新 List。
  • map 不筛选元素,也不会改变元素个数。
  • map 返回新集合,原集合保持不变。

filter:留下想要的

  • filter(p) 保留 p 为 true 的元素,filterNot 取反。
  • filter 后集合可能变短,甚至为空。
  • 先 filter 再 map 通常更省,因为被过滤掉的元素不再参与变换。

reduce 与 fold:把集合压成一个值

  • reduce(_ + _) 用第一个元素当初始值;空集合会抛异常。
  • foldLeft(init)(op) 显式给初始值,对空集合也安全。
  • foldLeft 的函数第一个参数是累加器,第二个参数是当前元素。

flatMap 与 foreach

  • flatMap 等于 map 后再 flatten,适合一对多或过滤空值。
  • map 返回 List 套 List 时,flatMap 会摊平一层。
  • foreach 只执行副作用,返回 Unit;构造新集合要用 map。

代码示例

▶ map 与 filter 的日常组合
SCALA
object MapFilter {
  def main(args: Array[String]): Unit = {
    val nums = List(1, 2, 3, 4, 5, 6)
    val evens = nums.filter(_ % 2 == 0)

    println(evens)
    println(evens.map(_ * 10))
    println(nums.filter(_ > 3).map(n => s"<$n>"))
  }
}
👀 运行结果
List(2, 4, 6)
List(20, 40, 60)
List(<4>, <5>, <6>)
▶ reduce 与 foldLeft 聚合
SCALA
object ReduceFold {
  def main(args: Array[String]): Unit = {
    val nums = List(3, 1, 4, 1, 5)

    println(nums.reduce(_ + _))
    println(nums.foldLeft(0)(_ + _))
    println(nums.foldLeft(Int.MinValue)((a, b) => math.max(a, b)))
    println(nums.foldLeft(List.empty[Int])((acc, x) => x :: acc))
  }
}
👀 运行结果
14
14
5
List(5, 1, 4, 1, 3)
▶ flatMap 展开与 foreach 打印
SCALA
object FlatMapDemo {
  def main(args: Array[String]): Unit = {
    val words = List("scala", "is", "fun")
    println(words.flatMap(w => List(w, w.toUpperCase)))

    val opts = List(Some(1), None, Some(3))
    println(opts.flatMap(o => o.toList))

    opts.foreach {
      case Some(n) => println(s"got $n")
      case None => println("empty")
    }
  }
}
👀 运行结果
List(scala, SCALA, is, IS, fun, FUN)
List(1, 3)
got 1
empty
got 3

常见错误

⚠️ 踩坑与解决
  • val out = List(1, 2, 3).map(println)
    → println 返回 Unit,map 会把它收集成 List[Unit],不是要保留的数据。;只打印用 foreach(println);要保留值就让 map 的 Lambda 返回元素本身。
  • List.empty[Int].reduce(_ + _)
    → reduce 没有初始值,空集合没有第一个元素可用。;改用 List.empty[Int].foldLeft(0)(_ + _),或先用 nonEmpty 判断。
  • List(1, 2).map(n => List(n, n * 10)) 得到 List(List(1, 10), List(2, 20))
    → map 保留一层嵌套,不会自动摊平。;改用 flatMap(n => List(n, n * 10)),结果是一层的 List(1, 10, 2, 20)。

💡 先想结果的形状:长度不变用 map,变短用 filter,只出一个值用 reduce/fold,一变多用 flatMap,只打印用 foreach。