文章

Dart函数与闭包深度解析

从函数作为一等公民到闭包捕获机制,拆解Dart函数式编程的核心特性和面试高频陷阱。

Dart函数与闭包深度解析

一句话概括

Dart 的函数是「一等公民」——可以赋值、传参、返回、存储,而闭包通过引用捕获词法作用域中的变量,实现了函数与其定义环境的绑定,这是 Flutter 中回调、状态管理、Widget 构建的基础。

核心知识点

1. 函数是一等公民——四种用法的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 赋值给变量
final greeter = (String name) => 'Hello, $name!';

// 2. 作为参数传递(高阶函数)
[1, 2, 3].map((e) => e * 2);           // map 接收一个函数
[1, 2, 3].where((e) => e.isEven);       // where 也接收一个函数

// 3. 作为返回值(函数工厂)
String Function(String) greetingFactory(String lang) {
  switch (lang) {
    case 'zh': return (name) => '你好,$name!';
    default:   return (name) => 'Hello, $name!';
  }
}

// 4. 存储在集合中
final ops = [(int x) => x + 1, (int x) => x * 2];
ops[0](5); // 6

这和 Flutter 的关系: build 方法是函数,setState 的回调是函数,onPressed 是函数——Flutter 的整个 UI 构建就是函数组合。

2. 闭包:引用捕获,不是值拷贝

这是面试高频题——Dart 的闭包捕获的是变量的引用(内存地址),不是值的快照。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var value = 10;
final closure = () => value;

print(closure()); // 10

value = 42;
print(closure()); // 42 ← 不是 10!证明了是引用捕获

// 多闭包共享同一变量
var counter = 0;
final inc = () => counter += 1;
final read = () => counter;

inc();
inc();
print(read()); // 2 — inc 改的,read 也能看到

与 Swift 的区别: Swift 闭包默认值捕获([value] 拷贝语义),Dart 默认引用捕获。这是坑也是灵活性。

3. 经典的「循环中的闭包」陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// JS 经典 bug 在 Dart 中的表现 ↓

// 情况1:var 声明在 for 循环体内 → 每次循环重新绑定,安全!
final callbacks1 = <Function()>[];
for (var i = 0; i < 3; i++) {
  callbacks1.add(() => print(i));
}
callbacks1.forEach((cb) => cb()); // 0, 1, 2 ✅ Dart 的 var 每次迭代重新绑定

// 情况2:变量声明在 for 外部 → 所有闭包共享同一个变量
final callbacks2 = <Function()>[];
int j;
for (j = 0; j < 3; j++) {
  callbacks2.add(() => print(j));
}
callbacks2.forEach((cb) => cb()); // 3, 3, 3 ❌ 同一个 j 被改成了 3

为什么 Dart 比 JS 安全? Dart 的 for (var i 语义上每次迭代创建新的 i 绑定。而 JS 的 var 没有块作用域——这也是 JS 需要 let 的原因。

4. forEach 里用 async 的坑

这是实际开发中最容易踩的闭包 + 异步组合拳:

1
2
3
4
5
6
7
8
9
10
11
// ❌ 错:forEach 不等 async 回调完成
items.forEach((item) async {
  await fetch(item);
});
print('全部完成');  // 这句先打印!forEach 不等 async

// ✅ 对:用 for 循环
for (final item in items) {
  await fetch(item);
}
print('全部完成');  // 现在真的完成了

根因: forEach 的回调没有返回值被利用——它只是个 void Function(T),async 产生的 Future 被丢弃了。

5. Tear-off——方法引用语法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final numbers = [1, 2, 3, 4, 5];

// 之前需要写匿名函数
numbers.forEach((e) => print(e));

// Tear-off:直接传方法引用
numbers.forEach(print);  // 等价于上面,更简洁

// 实例方法的 tear-off:每次创建新的闭包对象
final g = Greeter('Hi');
final f1 = g.greet;  // tear-off:绑定了 this
final f2 = g.greet;
print(f1 == f2);      // false — 每次 tear-off 都是新闭包

// 注意:实例方法 tear-off 创建新对象,静态方法 tear-off 复用同一对象

其实你每天都在用

  • setState(() { _count++; }): 这是一个捕获了 _count 的闭包,Flutter 在合适的时机调用它来触发重建
  • ListView.builder(itemBuilder: (context, index) => ...): itemBuilder 是一个闭包参数——你传给 ListView,ListView 在需要渲染时调用
  • onTap: () => Navigator.push(...): 闭包在用户点击时才执行,延迟了 Navigator.push 的调用时机
  • Stream.listen((event) { ... }) 忘记取消订阅: 闭包持有外部 Widget 引用,Widget 销毁后闭包还在 → 内存泄漏
  • Future.delayed 后的 setState 报错: Widget 已销毁,但闭包还活着,尝试 setState 时抛异常——需要在 dispose 里取消 timer

常见误解(FAQ)

❌ 误区1:「箭头函数和块函数体性能有差异」

没有。=> expr 只是 { return expr; } 的语法糖,编译后生成的指令完全相同。选择纯粹看可读性——单表达式用箭头,多语句用块体。

❌ 误区2:「闭包捕获的是变量的值」

是引用,不是值。这一点和 JS 一样。所以闭包里看到的永远是最新值——这也是回调能正常工作的原因(被回调时读取最新状态)。

❌ 误区3:「Dart 的闭包和 JS 的闭包一样」

核心机制一样(引用捕获 + 词法作用域),但 Dart 更安全:for (var i) 每次迭代独立绑定(JS 的 var 不这样)、Sound Null Safety 让捕获的变量类型明确。

❌ 误区4:「函数作为参数传太多了影响性能」

Dart 的闭包在编译时被实现为轻量级对象(CapturedVariable + Function Pointer)。创建闭包只是分配一个小对象,和新建一个普通 class 实例开销差不多。真正该担心的是闭包不小心捕获了大的 Widget 导致内存泄漏,而不是创建本身。

一句话总结

Dart 的函数和闭包是 Flutter 一切 UI 交互的底层语言——build 是函数、setState 是闭包、onTap 是闭包、Stream.listen 还是闭包;理解引用捕获避免共享变量踩坑,理解 tear-off 写更干净的代码。

本文由作者按照 CC BY 4.0 进行授权