深入理解Scala匿名函数:函数式编程基石

Java 8之前你写回调,得这么写:

java 复制代码
// Java 8之前:匿名内部类
button.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    System.out.println("clicked");
  }
});

四行代码,就为了传一个回调函数。这就是Java早期的"匿名函数"------匿名内部类。

Java 8之后呢?

java 复制代码
// Java 8:Lambda
button.setOnClickListener(v -> System.out.println("clicked"));

一行搞定。

Scala呢?Scala从第一天就有匿名函数------而且它的匿名函数比Java的Lambda更彻底、更灵活。

scala 复制代码
// Scala:匿名函数
val add = (a: Int, b: Int) => a + b
add(1, 2)  // 3

// 用在集合操作里
List(1, 2, 3).map(x => x * 2)  // List(2, 4, 6)
List(1, 2, 3).filter(x => x > 1)  // List(2, 3)

这篇文章不讲"匿名函数是什么"这种入门废话,直接说干货:Scala的匿名函数底层是什么?它和Java的Lambda比好在哪?闭包到底是怎么捕获变量的?有什么坑?


先从一个痛点说起

Java 8之前,你想把函数当参数传怎么办?答案是:传一个接口的实现。

java 复制代码
// Java:用接口实现"函数当参数"
interface Calculator {
  int calculate(int a, int b);
}

public int compute(int a, int b, Calculator calc) {
  return calc.calculate(a, b);
}

// 调用:传一个匿名内部类
compute(1, 2, new Calculator() {
  @Override
  public int calculate(int a, int b) {
    return a + b;
  }
});

这就是Java早期的"函数式编程"------你得先定义一个接口,再写一个匿名内部类,才能把函数当参数传。啰嗦,繁琐,但没办法------因为Java是面向对象语言,一切皆对象,函数不是一等公民。

Scala呢?Scala从第一天就把函数当一等公民------你可以把函数赋值给变量、当参数传、当返回值返回。

scala 复制代码
// Scala:函数是一等公民
val add = (a: Int, b: Int) => a + b       // 函数赋值给变量
val result = compute(1, 2, add)            // 函数当参数传

def compute(a: Int, b: Int, f: (Int, Int) => Int): Int = {
  f(a, b)
}

没有接口,没有匿名内部类------一行=>就是一个函数。

这就是Scala匿名函数和Java匿名内部类的本质区别:Java的匿名函数是"对象的语法糖",Scala的匿名函数是"函数的语法糖"。


匿名函数的本质:它是一个FunctionN对象

很多人以为Scala的匿名函数就是个语法糖------你写(a: Int) => a * 2,编译器帮你生成一个类。

对,但不完整。

scala 复制代码
val double = (a: Int) => a * 2

这行代码在底层生成了什么?------一个Function1[Int, Int]的实例。

scala 复制代码
// 编译器帮你生成的代码,大概长这样
val double = new Function1[Int, Int] {
  override def apply(a: Int): Int = a * 2
}

double(5)  // 10------因为有apply方法,所以可以像函数一样调用

看到了吗?Scala的匿名函数本质上就是一个实现了FunctionN接口的对象。你有几个参数,它就是Function1、Function2、Function3......一直到Function22。

scala 复制代码
// 一个参数 → Function1
val f1 = (x: Int) => x + 1

// 两个参数 → Function2
val f2 = (x: Int, y: Int) => x + y

// 三个参数 → Function3
val f3 = (x: Int, y: Int, z: Int) => x + y + z

Scala最多支持22个参数的函数------因为标准库只定义了Function1到Function22。

这就是为什么Scala的匿名函数可以像函数一样调用------因为它是一个对象,但它有apply方法,所以你可以obj(args)这样调用。


闭包:匿名函数最强大的地方

匿名函数最神奇的能力是什么?------它可以捕获外部变量。

scala 复制代码
var multiplier = 2

val multiply = (x: Int) => x * multiplier
multiply(5)  // 10

multiplier = 3
multiply(5)  // 15------注意!这里变了!

看到了吗?multiply这个函数,捕获了外部变量multiplier。当multiplier变了,函数的行为也变了。

这就是闭包。

闭包 = 函数 + 它捕获的环境。

Java的匿名内部类也能捕获变量------但它有一个限制:你只能捕获final或等效final的变量。你不能在匿名内部类里修改外部变量。

java 复制代码
// Java:匿名内部类只能捕获final变量
int multiplier = 2;

Runnable r = () -> {
  System.out.println(multiplier * 5);
  // multiplier = 3;  // 编译报错!不能修改外部变量
};

Scala呢?Scala的闭包可以捕获变量,甚至可以修改变量。

scala 复制代码
var count = 0

val increment = () => {
  count += 1
  println(s"count = $count")
}

increment()  // count = 1
increment()  // count = 2
increment()  // count = 3

为什么Scala能做到?因为Scala的闭包捕获的不是变量的值------而是变量本身。它捕获的是一个引用,通过这个引用可以读写外部变量。

Java的闭包是"值捕获"------你捕获的是变量的值,不能改。Scala的闭包是"变量捕获"------你捕获的是变量本身,可以读写。

这就是函数式编程和命令式编程的区别------函数式语言把函数当一等公民,所以它的闭包也是完整的闭包。


几个常见的简写

Scala的匿名函数有很多简写语法,写起来非常爽。

1. 下划线占位符

scala 复制代码
List(1, 2, 3).map(_ * 2)      // List(2, 4, 6)
List(1, 2, 3).filter(_ > 1)   // List(2, 3)
List(1, 2, 3).reduce(_ + _)    // 6

_就是"这个参数"的占位符。如果函数只有一个参数,你可以用_代替参数名。

2. 类型推断

scala 复制代码
// 不用写参数类型------编译器帮你推断
List(1, 2, 3).map(x => x * 2)

// 用下划线更简洁
List(1, 2, 3).map(_ * 2)

Scala的类型推断会根据上下文推断参数类型------List[Int].map的参数肯定是Int,所以你不用写。

3. 多参数函数

scala 复制代码
// 两个参数的函数
val add = (_: Int) + (_: Int)
add(1, 2)  // 3

每个_代表一个参数------第一个_是第一个参数,第二个_是第二个参数。


几个常见的坑

坑1:闭包捕获的是变量,不是值

scala 复制代码
var total = 0

val addToTotal = (x: Int) => total += x

addToTotal(10)
addToTotal(20)
println(total)  // 30

很多人第一次写闭包的时候,以为闭包捕获的是变量的值。但实际上,闭包捕获的是变量本身------它是一个引用,指向那个变量。

所以当你修改变量的时候,所有闭包看到的值都变了。

这个特性有时候会导致意外------比如你在循环里创建闭包:

scala 复制代码
// 这个会出问题
val functions = (1 to 3).map { i =>
  () => i
}

functions.foreach(f => println(f()))
// 你以为输出 1 2 3?
// 实际上输出什么?

这个问题在Java和JavaScript里都有------因为闭包捕获的是变量本身,不是变量的值。Scala也一样。

坑2:序列化问题

这个是大数据开发里最常见的坑------Flink/Spark里用Scala匿名函数,有时候会报序列化错误。

为什么?因为Scala的匿名函数是一个对象------它实现了FunctionN接口。如果你用的是普通的类,它没有实现Serializable,序列化的时候就会报错。

scala 复制代码
// 这个在Flink/Spark里会报序列化错误
val data = List(1, 2, 3)
data.map(x => x * 2)  // 匿名函数没有实现Serializable

怎么解决?把匿名函数提取成一个独立的类,或者用lambda的方式。

这就是为什么Flink官方文档里说:JdbcSink的第二个参数不能用Scala匿名函数写,否则会报序列化错误。

坑3:函数的arity限制

Scala最多支持22个参数的函数------因为标准库只定义了Function1到Function22。

如果你需要23个参数的函数怎么办?------把多个参数打包成一个元组,或者用柯里化。


什么时候用匿名函数?

匿名函数最适合的场景就是:函数只在一个地方用,不需要复用。

比如:

scala 复制代码
// 集合操作------匿名函数用一次就扔
list.map(_ * 2)
list.filter(_ > 10)
list.reduce(_ + _)

// 回调------传一个一次性的回调
button.onClick(() => println("clicked"))

什么时候不用?------如果这个函数你要在多个地方复用,那就给它起个名字,定义成命名函数。

scala 复制代码
// 复用的函数------用命名函数
def double(x: Int): Int = x * 2

list.map(double)
list.filter(double(_) > 4)

简单说:一次性的操作用匿名函数,复用的操作用命名函数。


写在最后

我写这篇文章的时候,特意没罗列匿名函数有多少种写法------因为写法很简单,谁都能写。

真正值得花时间理解的是:匿名函数为什么是函数式编程的基石?

因为函数式编程的核心思想就是:把函数当参数传。

你不能把函数当参数传,就没有map、filter、reduce这些集合操作。没有这些集合操作,就没有声明式编程。没有声明式编程,就没有函数式编程。

Java花了很长时间才意识到这一点------Java 8才加了Lambda。但Java的Lambda是不完整的------它只能捕获final变量,不能修改变量。它是一个"阉割版"的闭包。

Scala呢?Scala从第一天就有完整的匿名函数和闭包------它可以捕获变量,可以修改变量,可以把函数当一等公民。

匿名函数不是一个"语法糖"------它是函数式编程的基石。没有匿名函数,就没有函数式编程。

你写的不是一个"没有名字的函数"------你写的是一个可以被传递、被组合、被组合的计算单元。

这就是函数式编程的威力:把计算当数据传递。

相关推荐
apcipot_rain1 小时前
C++转Java日记(05)
java·开发语言·c++
apcipot_rain10 小时前
C++转Java日记(04)
java·开发语言·c++
云和数据.ChenGuang11 小时前
LangGraph 异步 Node 的设计与写法实践
开发语言·人工智能·python·机器学习·php
蜗牛一步步往上爬14 小时前
第十六章 C++语言在深度学习中的应用
开发语言·c++·深度学习
张张123y16 小时前
DSH 插件调试避坑:client.js 设置面板、热重载与高频报错排查表
开发语言·javascript·人工智能·开源·ecmascript·后端开发·ai agent
冻柠檬飞冰走茶19 小时前
字母移动游戏题解:贪心求最长可保留连续子串(附 C 实现)
c语言·开发语言·数据结构·算法·动态规划
FRing0320 小时前
vue2干货总结7(自定义指令、插槽、单页应用程序、路由的使用(VueRouter))
开发语言·前端·vue.js·vue2
雪落漂泊20 小时前
C++算法 二分查找篇(上)
开发语言·c++·算法
波力海苔夹心脆67521 小时前
C# WPF 资源字典详解:ResourceDictionary、MergedDictionaries 与自定义控件案例
开发语言·c#·wpf·入门教程·resource·资源字典