Flutter 基础体系 · 第 23/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。

Dart 函数与闭包:参数、类型、捕获、Callable 和 API 设计

函数不仅是“传入参数并返回结果”的语法结构。在 Dart 中,函数同时也是一种值,可以被赋给变量、作为参数传递、从函数中返回,并且能够捕获定义位置周围的词法环境。闭包正是“函数值 + 被捕获环境”的组合。

理解函数、闭包和 Callable,需要同时掌握四层关系:

  1. 参数语法决定调用者如何传值;
  2. 函数类型决定哪些函数可以安全地互相替代;
  3. 闭包捕获决定函数值在离开定义位置后仍然能访问什么;
  4. API 设计决定调用者是否能正确处理异步、生命周期和错误。

本文示例基于 Dart 3 的空安全语言能力。函数语法本身在 Android、iOS、桌面和 Web 上一致,但异步调度、Isolate 和 Flutter 生命周期会受到编译目标和运行时环境影响。


一、函数在 Dart 中到底是什么

1.1 函数声明与函数值

最直接的函数声明如下:

int add(int a, int b) {
  return a + b;
}

add 是函数的名字,int add(int a, int b) 描述函数的返回类型和参数类型。

函数也可以写成表达式:

final multiply = (int a, int b) => a * b;

print(multiply(2, 3)); // 6

这里:

(int a, int b) => a * b

是一个匿名函数。它被赋给了变量 multiply,因此可以通过 multiply(2, 3) 调用。

函数表达式可以省略部分类型,让上下文推导类型:

final multiply = (int a, int b) => a * b;

由于参数已经声明为 int,返回值由表达式推导为 int。也可以显式写返回类型:

final int Function(int, int) multiply =
    (int a, int b) => a * b;

这两种写法表达的是同一个函数类型:

int Function(int, int)

其中:

  • int 是返回类型;
  • Function 表示这是函数类型;
  • (int, int) 是两个位置参数的类型。

函数的名字本身不是函数类型;名字只是引用函数值的一个标识符。


1.2 函数可以被传递和返回

int applyBinary(
  int left,
  int right,
  int Function(int, int) operation,
) {
  return operation(left, right);
}

void main() {
  final result = applyBinary(3, 4, (a, b) => a * b);
  print(result); // 12
}

执行过程是:

  1. 创建匿名函数 (a, b) => a * b
  2. 将该函数值传给 operation
  3. applyBinary 调用 operation(left, right)
  4. 最终返回 3 * 4

因此,函数参数不是特殊的“回调语法”,而是普通的值传递。所谓“回调”,通常只是指被传入并在稍后调用的函数。

函数也可以作为返回值:

int Function(int) makeAdder(int amount) {
  return (int value) => value + amount;
}

void main() {
  final addFive = makeAdder(5);

  print(addFive(10)); // 15
  print(addFive(20)); // 25
}

makeAdder(5) 返回一个新函数。这个新函数在 makeAdder 执行结束后仍然可以访问 amount,这就是闭包机制,后文会详细解释。


二、函数参数:位置、可选、命名与默认值

Dart 的参数主要分为:

  • 必需位置参数;
  • 可选位置参数;
  • 必需命名参数;
  • 可选命名参数。

这些类别不是装饰性语法,而是 API 调用契约的一部分。


2.1 必需位置参数

String joinNames(String first, String second) {
  return '$first $second';
}

void main() {
  print(joinNames('Ada', 'Lovelace'));
}

调用者必须:

  1. 传入两个参数;
  2. firstsecond 的顺序传递;
  3. 提供与参数类型兼容的值。

下面的调用都不成立:

// joinNames('Ada');                 // 参数数量不足
// joinNames('Lovelace', 'Ada');     // 语法上合法,但语义顺序可能错误
// joinNames(1, 2);                  // 类型错误

位置参数适合参数数量少、顺序直观的函数。参数较多时,继续增加位置参数会使调用容易出错。


2.2 可选位置参数

可选位置参数使用方括号:

String greet(String name, [String punctuation = '!']) {
  return 'Hello, $name$punctuation';
}

void main() {
  print(greet('Dart'));       // Hello, Dart!
  print(greet('Dart', '.'));  // Hello, Dart.
}

punctuation 可以不传。未传时使用默认值 !

可选位置参数必须位于必需位置参数之后:

// 错误示例:必需参数不能放在可选位置参数之后
// String invalid([String prefix], String name) => '$prefix$name';

默认值必须是编译期常量,例如:

void configure([Duration timeout = const Duration(seconds: 5)]) {}

下面的写法不成立,因为 DateTime.now() 不是编译期常量:

// void configure([DateTime time = DateTime.now()]) {}

如果默认值需要运行时计算,应在函数体内处理:

void configure([DateTime? time]) {
  final actualTime = time ?? DateTime.now();
  print(actualTime);
}

2.3 命名参数

命名参数放在大括号中:

String formatUser({
  required String name,
  int age = 0,
  bool showAge = true,
}) {
  if (showAge) {
    return '$name ($age)';
  }
  return name;
}

调用时必须使用参数名:

void main() {
  print(formatUser(
    name: 'Ada',
    age: 36,
  ));

  print(formatUser(
    name: 'Grace',
    showAge: false,
  ));
}

命名参数的优点是调用点具有自描述性:

// 位置参数:不容易看出 36 和 false 的含义
// formatUser('Ada', 36, false);

// 命名参数:含义清楚
formatUser(name: 'Ada', age: 36, showAge: false);

required 表示命名参数必须由调用者提供:

void sendRequest({
  required Uri url,
  Map<String, String> headers = const {},
}) {
  print(url);
}

下面的调用会产生静态错误:

// sendRequest(); // 缺少 required 参数 url

注意:required 只表示调用时必须提供参数,不表示参数值一定非空。空安全下,下面的声明仍然允许传入 null

void logValue({required String? value}) {
  print(value);
}

如果既要求必须传入,又要求不能为 null,应使用:

void logValue({required String value}) {
  print(value);
}

2.4 默认值与 null 的区别

以下两个 API 的语义不同:

void first({String value = 'default'}) {
  print(value);
}

void second({String? value}) {
  print(value ?? 'default');
}

对于 first

first(); // 使用默认值 default
// first(value: null); // 类型错误,String 不能接收 null

对于 second

second();          // value 为 null,输出 default
second(value: null); // value 显式为 null,仍输出 default
second(value: 'x');  // 输出 x

如果 API 需要区分“未提供”和“显式传入 null”,单纯的默认参数无法表达这种区别,需要使用额外的哨兵对象或独立状态设计。


2.5 初始化形式参数与 super 参数

Dart 支持使用参数直接初始化字段:

class User {
  final String name;
  final int age;

  User(this.name, {this.age = 0});
}

它等价于更冗长的写法:

class User {
  final String name;
  final int age;

  User(String name, {int age = 0})
      : name = name,
        age = age;
}

Dart 3 还支持将构造函数参数直接转发给父类:

class BaseWidget {
  final String id;

  BaseWidget(this.id);
}

class ChildWidget extends BaseWidget {
  ChildWidget(super.id);
}

这些是构造函数参数语法,不改变函数类型的基本规则,但它们说明了 Dart 参数设计的一贯目标:在保证初始化顺序和类型检查的前提下减少样板代码。


三、函数类型:Function 不是类型安全的首选

3.1 精确函数类型

函数类型使用如下形式:

String Function(int)

表示:

  • 接收一个 int
  • 返回一个 String

例如:

String stringify(int value) => value.toString();

final String Function(int) converter = stringify;

print(converter(42)); // 42

多个参数的类型直接列出:

double Function(double, double) divide;

带命名参数的函数类型也必须声明参数名:

typedef Formatter = String Function(
  String input, {
  required bool uppercase,
});

String format(
  String input, {
  required bool uppercase,
}) {
  return uppercase ? input.toUpperCase() : input;
}

void main() {
  final Formatter formatter = format;

  print(formatter('dart', uppercase: true)); // DART
}

命名参数名属于函数类型的一部分。下面的函数与 Formatter 不完全匹配:

String otherFormat(
  String input, {
  required bool upper,
}) {
  return upper ? input.toUpperCase() : input;
}

// Formatter f = otherFormat; // 参数名不匹配

原因是调用者通过 uppercase: 调用。如果允许名称不一致,类型系统就无法保证调用表达式成立。


3.2 typedef:给函数类型命名

复杂函数类型可以使用 typedef

typedef Predicate<T> = bool Function(T value);
typedef AsyncLoader<T> = Future<T> Function();

使用方式:

bool isEven(int value) => value.isEven;

bool anyMatch<T>(Iterable<T> values, Predicate<T> predicate) {
  for (final value in values) {
    if (predicate(value)) {
      return true;
    }
  }
  return false;
}

void main() {
  final result = anyMatch<int>(
    [1, 3, 4, 7],
    isEven,
  );

  print(result); // true
}

typedef 通常有三个价值:

  1. 让 API 签名可读;
  2. 让多个 API 复用同一个函数契约;
  3. 在 IDE 和错误信息中提供更明确的语义名称。

例如:

typedef OnProgress = void Function(double value);
typedef OnError = void Function(Object error, StackTrace stackTrace);

相比直接写:

void start(
  void Function(double) onProgress,
  void Function(Object, StackTrace) onError,
) {}

命名后的函数类型更容易表达业务含义。


3.3 Functiondynamic 与具体函数类型的区别

Function 只表示“某个函数”,不描述它接收什么参数或返回什么类型:

void run(Function callback) {
  callback();
}

这段代码的问题是:callback() 是否需要参数,编译器无法通过 Function 类型准确检查。调用方传入如下函数时就可能在运行时失败:

run((int value) {
  print(value);
});

run 按无参数方式调用它,但传入函数需要一个 int,最终会产生运行时错误。

更安全的写法是明确函数类型:

void run(void Function() callback) {
  callback();
}

如果确实需要传参数:

void runWithValue(void Function(int) callback) {
  callback(10);
}

dynamic 的风险更大,因为它会关闭大量静态检查:

void unsafe(dynamic callback) {
  callback('text', 1, false);
}

除非 API 本身就是动态调用边界,否则应优先使用具体函数类型,而不是 Functiondynamic


3.4 函数类型的替代关系

函数类型的替代必须保证:调用者按照声明调用时,被替代函数一定能够正确接收参数并产生兼容结果。

先看参数方向:

void acceptObject(Object value) {
  print(value);
}

void acceptString(String value) {
  print(value);
}

void main() {
  void Function(String) callback = acceptObject;
  callback('Dart'); // 合法
}

为什么 void Function(String) 可以接收 void Function(Object)

因为调用者只承诺传入 String,而 String 一定是 Object,所以 acceptObject 能安全处理所有调用。

反过来不安全:

// void Function(Object) callback = acceptString;

调用者可能这样调用:

callback(123);

acceptString 不能接收 int。因此,函数参数类型在替代关系中遵循逆变直觉:被替代函数应能接收至少同样宽泛的参数。

再看返回值:

String makeString() => 'Dart';

void main() {
  Object Function() producer = makeString;
  print(producer()); // Dart
}

调用者只要求得到一个 Object,返回 String 是安全的,因为 StringObject 的子类型。

反过来则不安全:

Object makeObject() => Object();

// String Function() producer = makeObject; // 不成立

因为 makeObject() 可能返回一个并非 String 的对象。


3.5 void 返回值的特殊使用边界

Dart 允许在某些上下文中忽略函数返回值,例如:

String compute() => 'result';

void consume(void Function() callback) {
  callback();
}

void main() {
  consume(compute);
}

compute 的返回值被忽略了。这种能力适合明确表示“调用方不关心结果”的同步回调,但对异步 API 可能造成问题。

不要把异步回调设计成:

void register(void Function() callback) {
  callback();
}

然后让调用方传入异步函数:

register(() async {
  await saveData();
});

此时 register 看不到 Future,也无法等待异步完成或捕获异步错误。更准确的 API 应写成:

void register(Future<void> Function() callback) {
  // 这里仍然需要由调用流程决定如何 await
}

如果 API 本身负责调用并等待:

Future<void> register(Future<void> Function() callback) async {
  await callback();
}

这样完成状态和错误才能沿着 Future 传播给调用者。


四、函数引用、匿名函数与 Tear-off

4.1 Tear-off 是什么

将已有函数或方法作为值引用,而不是立即调用,称为 tear-off:

int square(int value) => value * value;

final operation = square;   // 引用函数
final result = square(4);   // 调用函数

print(operation(5)); // 25
print(result);       // 16

final operation = square 没有执行 square;只有 operation(5) 才会调用它。

实例方法也可以被 tear-off:

class Calculator {
  int add(int a, int b) => a + b;
}

void main() {
  final calculator = Calculator();
  final int Function(int, int) operation = calculator.add;

  print(operation(2, 8)); // 10
}

calculator.add 会绑定到特定的 calculator 实例。调用 operation(2, 8) 时,方法内部的 this 就是该实例。

不要误写成:

// final operation = calculator.add(2, 8);

这会立即执行方法,并把返回的 int 赋给 operation,它不再是函数。


4.2 Tear-off 与匿名包装函数的差别

下面两种写法行为通常相同:

final a = calculator.add;
final b = (int x, int y) => calculator.add(x, y);

但它们仍有重要差别:

  • a 直接引用方法;
  • b 创建了一个新的闭包;
  • b 额外捕获了 calculator
  • 包装函数可以插入日志、参数转换或错误处理。
final loggedAdd = (int x, int y) {
  print('adding $x and $y');
  return calculator.add(x, y);
};

如果只是转发参数,优先使用 tear-off;如果需要改变行为,再使用匿名包装函数。


五、闭包:函数如何捕获外部变量

5.1 闭包的定义

闭包是能够访问其词法作用域中变量的函数值,即使该函数已经离开了变量原本的执行位置。

int Function() makeCounter() {
  var count = 0;

  return () {
    count++;
    return count;
  };
}

void main() {
  final counter = makeCounter();

  print(counter()); // 1
  print(counter()); // 2
  print(counter()); // 3
}

执行步骤如下:

  1. 调用 makeCounter()
  2. 创建局部变量 count,初始值为 0
  3. 创建匿名函数;
  4. 匿名函数捕获 count
  5. makeCounter() 返回匿名函数;
  6. 虽然 makeCounter() 已经返回,count 仍被返回的函数使用,因此继续存在;
  7. 每次调用闭包时,读取并修改同一个 count

关键点是:闭包捕获的不是 count 当前值的简单副本,而是可以继续访问的变量绑定。


5.2 捕获值与捕获变量

下面的代码使用可变变量:

List<Function()> makeCounters() {
  final result = <Function()>[];

  for (var i = 0; i < 3; i++) {
    final captured = i;
    result.add(() => captured);
  }

  return result;
}

void main() {
  final counters = makeCounters();

  for (final counter in counters) {
    print(counter());
  }
}

输出:

0
1
2

每次循环都创建了一个新的局部绑定 captured,每个闭包捕获自己的绑定。

这种显式写法比依赖循环变量捕获细节更清楚,尤其是在复杂循环、异步任务或需要兼容多种写法时。

如果闭包捕获的是一个可变对象,捕获的是对象引用对应的变量关系,而不是对象的深拷贝:

void main() {
  final values = <int>[];

  final readLength = () => values.length;

  values.add(1);
  values.add(2);

  print(readLength()); // 2
}

readLength 创建时列表为空,但它访问的是同一个 values 列表。后续对列表的修改会被闭包观察到。

final 只禁止重新给变量绑定赋值,不会冻结对象:

final values = <int>[];
values.add(1); // 合法

// values = <int>[]; // 错误:不能重新赋值

5.3 闭包与 late

late 表示变量将在声明后初始化:

late String name;

final readName = () => name;

name = 'Dart';

print(readName()); // Dart

闭包捕获的是变量绑定,不是创建闭包时的值。

如果在初始化前调用:

late String name;

final readName = () => name;

// print(readName()); // LateInitializationError

这不是普通的 null,而是违反了 late 变量必须先初始化再读取的运行时约束。


5.4 闭包的生命周期与内存保留

只要闭包仍然可达,它捕获的环境就可能继续存活:

Function makeReader() {
  final largeData = List<int>.filled(1000000, 1);

  return () => largeData.first;
}

即使 makeReader 返回后,largeData 仍可能因为闭包而不能被垃圾回收。

在 Flutter 中,常见的风险是:

  • 闭包注册给 Timer
  • 闭包注册给 StreamSubscription
  • 闭包注册给事件总线;
  • 闭包捕获了 State、大型缓存或 BuildContext
  • 页面销毁后,外部对象仍保留该闭包。

例如:

class ExampleState {
  Timer? _timer;

  void start() {
    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (_) {
        // 该闭包捕获了 this
        print('tick');
      },
    );
  }

  void dispose() {
    _timer?.cancel();
  }
}

这里真正重要的不是“闭包有内存泄漏”,而是引用链:

Timer -> 回调闭包 -> ExampleState

如果没有取消 Timer,定时器可能继续持有回调,回调又持有 State,导致页面对象不能按预期释放,并且在销毁后继续执行逻辑。


六、同步、异步函数与闭包捕获

6.1 Future 函数不是立即返回结果

Future<String> loadName() async {
  await Future<void>.delayed(const Duration(milliseconds: 10));
  return 'Dart';
}

loadName() 的返回值是 Future<String>,不是 String

回调类型也应体现这一点:

typedef Loader<T> = Future<T> Function();

Future<T> loadWithRetry<T>(
  Loader<T> loader, {
  int retries = 3,
}) async {
  Object? lastError;

  for (var attempt = 0; attempt < retries; attempt++) {
    try {
      return await loader();
    } catch (error) {
      lastError = error;
    }
  }

  throw StateError('加载失败:$lastError');
}

调用:

void main() async {
  var count = 0;

  final value = await loadWithRetry<String>(() async {
    count++;

    if (count < 2) {
      throw StateError('临时错误');
    }

    return 'success';
  });

  print(value); // success
}

每一步的因果关系是:

  1. loader 返回 Future<String>
  2. loadWithRetry 使用 await 等待它;
  3. 第一次调用抛出异常;
  4. catch 记录错误并进入下一次尝试;
  5. 第二次调用返回 success
  6. loadWithRetry 返回成功结果。

如果把该回调声明成 void Function(),调用者就无法通过函数签名知道异步操作何时完成。


6.2 await 不会复制被捕获变量

Future<void> example() async {
  var state = 'before';

  final readState = () => state;

  await Future<void>.delayed(const Duration(milliseconds: 1));

  state = 'after';

  print(readState()); // after
}

readState 捕获的是 state 变量。await 让当前异步函数暂时挂起,但不会把 state 复制成字符串 before

这在异步 UI 代码中尤其重要:

Future<void> load() async {
  final callback = () {
    print('使用当前字段');
  };

  await repository.fetch();

  callback();
}

如果对象在等待期间已经进入销毁状态,回调执行时可能访问无效的生命周期。Flutter 的 State 中通常应在异步间隔后检查:

Future<void> load() async {
  final data = await repository.fetch();

  if (!mounted) {
    return;
  }

  setState(() {
    // 使用 data 更新界面
  });
}

mounted 只能用于合适的 Flutter State 生命周期上下文;它不能解决所有业务竞态,例如请求结果过期、用户切换账户或多个请求返回顺序相反。


6.3 Dart 的异步模型与线程边界

普通 async/await 不会自动创建新线程。它们通常运行在当前 Isolate 的事件循环中:

同步代码 -> await 挂起当前 Future -> 事件循环处理其他任务
        -> Future 完成 -> 恢复后续代码

在单个 Isolate 中,Dart 代码通常不会像多线程共享内存那样同时执行,但异步任务仍会在 await 位置交错执行,因此仍然可能产生逻辑竞态。

Isolate 之间不共享普通 Dart 堆内存。不能把闭包当作通用的跨 Isolate RPC 参数:

// 不应假设可以把任意捕获大量状态的闭包发送到另一个 Isolate

跨 Isolate 通信应使用 SendPort、可发送的数据结构和明确的消息协议。具体可发送对象受 Dart 运行时和平台支持范围约束,任意闭包及其捕获环境不应作为 API 契约。

在 Web、Android、iOS 和桌面上,Dart 代码的函数与闭包语义保持一致,但后台执行和 Isolate 的具体可用能力可能不同,尤其不能把移动端或原生端的线程假设直接套用到 Web。


七、Callable:让对象像函数一样被调用

7.1 call 方法

Dart 允许类定义名为 call 的方法。拥有该方法的实例可以使用函数调用语法:

class Prefixer {
  final String prefix;

  Prefixer(this.prefix);

  String call(String value) {
    return '$prefix$value';
  }
}

void main() {
  final addLogPrefix = Prefixer('[LOG] ');

  print(addLogPrefix('started')); // [LOG] started
}

以下两种调用等价:

addLogPrefix('started');
addLogPrefix.call('started');

Prefixer 仍然是一个类实例,只是 Dart 为定义了 call 方法的对象提供了调用语法。


7.2 Callable 对象与普通闭包的取舍

普通闭包适合小型、局部行为:

final isPositive = (int value) => value > 0;

Callable 对象适合带有多个配置、状态或相关操作的行为对象:

class RetryPolicy {
  final int maxAttempts;
  final Duration delay;

  RetryPolicy({
    this.maxAttempts = 3,
    this.delay = const Duration(milliseconds: 100),
  });

  Future<T> call<T>(Future<T> Function() operation) async {
    Object? lastError;

    for (var attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error;

        if (attempt + 1 < maxAttempts) {
          await Future<void>.delayed(delay);
        }
      }
    }

    throw StateError('操作失败:$lastError');
  }
}

调用方式:

void main() async {
  final retry = RetryPolicy(maxAttempts: 2);
  var attempts = 0;

  final result = await retry(() async {
    attempts++;

    if (attempts == 1) {
      throw StateError('第一次失败');
    }

    return 'ok';
  });

  print(result); // ok
}

这个设计把重试次数、等待时间和调用逻辑组织在一个对象中,同时保留了函数调用的简洁外观。

普通闭包更轻量;Callable 对象可以:

  • 保存配置;
  • 保存可变状态;
  • 实现接口;
  • 暴露额外方法;
  • 在测试中被单独构造和验证。

不要仅为了“看起来像函数”就创建 Callable 类。如果行为没有独立状态或身份,普通函数或闭包通常更直接。


八、函数的身份、相等性与监听器注销

函数值可以作为对象保存,但不要把“代码内容相同”误认为“函数对象一定相同”。

final a = () => print('event');
final b = () => print('event');

print(identical(a, b)); // false

ab 是两个分别创建的闭包,即使函数体相同,也不是同一个函数对象。

这会影响监听器 API:

class EventSource {
  final _listeners = <void Function()>[];

  void addListener(void Function() listener) {
    _listeners.add(listener);
  }

  void removeListener(void Function() listener) {
    _listeners.remove(listener);
  }

  void emit() {
    for (final listener in List.of(_listeners)) {
      listener();
    }
  }
}

错误用法:

final source = EventSource();

source.addListener(() {
  print('changed');
});

// 这是另一个闭包,不是前面注册的那个
source.removeListener(() {
  print('changed');
});

正确做法是保存回调引用:

final source = EventSource();

void onChanged() {
  print('changed');
}

source.addListener(onChanged);
source.removeListener(onChanged);

在 Flutter 中也应遵循同样原则:

class ExampleState {
  final ValueNotifier<int> counter = ValueNotifier<int>(0);

  late final VoidCallback _listener = () {
    print(counter.value);
  };

  void init() {
    counter.addListener(_listener);
  }

  void dispose() {
    counter.removeListener(_listener);
    counter.dispose();
  }
}

这里 _listener 是一个稳定的函数引用。VoidCallback 是 Flutter 常用的函数类型别名,语义等价于 void Function()

如果使用 Stream.listenTimer.periodic 或第三方事件总线,还需要保存并释放对应的 StreamSubscriptionTimer 或取消句柄;仅保存闭包本身不一定足够。


九、API 设计:回调的类型应表达真实生命周期

9.1 同步回调与异步回调分开

同步处理器:

typedef ItemHandler<T> = void Function(T item);

异步处理器:

typedef AsyncItemHandler<T> = Future<void> Function(T item);

如果 API 要等待每个处理器完成:

Future<void> processItems<T>(
  Iterable<T> items,
  AsyncItemHandler<T> handler,
) async {
  for (final item in items) {
    await handler(item);
  }
}

调用:

void main() async {
  await processItems<int>([1, 2, 3], (item) async {
    await Future<void>.delayed(const Duration(milliseconds: 1));
    print(item);
  });
}

输出顺序是:

1
2
3

因为循环每次都 await handler(item),下一个元素要等上一个处理完成。

如果改为并行启动:

Future<void> processItemsInParallel<T>(
  Iterable<T> items,
  AsyncItemHandler<T> handler,
) async {
  await Future.wait(
    items.map(handler),
  );
}

那么多个处理器会同时处于等待状态,完成顺序可能不等于输入顺序。这个 API 选择会影响:

  • 是否保持顺序;
  • 是否允许并行;
  • 一个任务失败时其他任务是否继续;
  • 资源和请求压力;
  • 错误是否一次性聚合。

函数类型本身无法表达所有这些语义,因此 API 文档和实现必须保持一致。


9.2 错误传播应沿函数类型传递

一个明确的异步 API:

typedef Request<T> = Future<T> Function();

Future<T> withErrorReport<T>(
  Request<T> request,
  void Function(Object error, StackTrace stackTrace) onError,
) async {
  try {
    return await request();
  } catch (error, stackTrace) {
    onError(error, stackTrace);
    rethrow;
  }
}

调用:

void main() async {
  try {
    await withErrorReport<String>(
      () async {
        throw StateError('network unavailable');
      },
      (error, stackTrace) {
        print('记录错误:$error');
      },
    );
  } catch (error) {
    print('上层处理:$error');
  }
}

预期输出类似:

记录错误:Bad state: network unavailable
上层处理:Bad state: network unavailable

rethrow 保留原始错误的传播路径。若改成 throw error,可能改变堆栈信息;如果只是记录后继续让上层处理,rethrow 更符合语义。

如果错误回调本身也可能异步,应明确声明:

typedef AsyncErrorHandler = Future<void> Function(
  Object error,
  StackTrace stackTrace,
);

随后由调用方决定是否等待错误上报。不要把可能异步执行的错误处理器隐藏在 void Function 中。


9.3 可选回调的空安全写法

可选回调通常声明为 nullable:

void loadData({
  void Function(String data)? onSuccess,
  void Function(Object error)? onFailure,
}) {
  try {
    final data = 'loaded';
    onSuccess?.call(data);
  } catch (error) {
    onFailure?.call(error);
  }
}

onSuccess?.call(data) 的含义是:

  • 如果 onSuccess 非空,就调用它;
  • 如果为空,就跳过调用;
  • 不需要强制解包 onSuccess!

也可以写成:

final callback = onSuccess;
if (callback != null) {
  callback(data);
}

在复杂异步流程中,先保存局部变量有时更利于控制空值和生命周期,但这不会自动解决回调对象内部捕获的状态变化。


9.4 不要让回调承担过多协议

下面的 API 看似灵活,但状态协议隐藏在多个回调中:

void fetch({
  required void Function() onStart,
  required void Function(int) onProgress,
  required void Function(String) onSuccess,
  required void Function(Object) onError,
  required void Function() onComplete,
}) {}

调用者必须理解:

  • onStart 是否一定先调用;
  • 失败时是否也调用 onComplete
  • onProgress 是否可能重复;
  • onSuccessonError 是否互斥;
  • 回调抛错后流程是否继续。

对于一次性异步操作,Future 通常能更直接地表达成功和失败:

Future<String> fetch() async {
  return 'data';
}

只有当事件是多次发生、持续存在或需要实时通知时,回调、Stream 或监听器才更自然:

Stream<int> progress() async* {
  for (var value = 0; value <= 100; value += 25) {
    yield value;
    await Future<void>.delayed(const Duration(milliseconds: 10));
  }
}

此时事件的数量和生命周期由 Stream 表达,而不是由多个回调之间的约定隐含表达。


十、泛型函数与回调:让类型信息继续流动

泛型函数可以让回调与输入数据保持类型关联:

T transform<T, S>(
  S input,
  T Function(S value) converter,
) {
  return converter(input);
}

void main() {
  final length = transform<int, String>(
    'Dart',
    (value) => value.length,
  );

  print(length); // 4
}

这里:

  • S 是输入类型;
  • T 是输出类型;
  • converter 接收 S 并返回 T
  • 返回值类型与转换器的返回类型一致。

调用时通常可以依赖类型推导:

final length = transform('Dart', (String value) => value.length);

如果回调参数不写类型,Dart 也可能根据上下文推导:

final length = transform('Dart', (value) => value.length);

当推导失败或 API 过于复杂时,应显式标注类型,而不是用 dynamic 回避问题。


十一、常见失败表现与诊断方法

11.1 把函数调用结果当成函数

错误代码:

int add(int a, int b) => a + b;

// final operation = add(1, 2);

此时 operation 的类型是 int,因为 add(1, 2) 已经执行完毕。

如果需要保存函数,应使用 tear-off:

final int Function(int, int) operation = add;

判断方法是看是否写了括号:

  • add:引用函数;
  • add(1, 2):调用函数。

11.2 回调参数数量不匹配

void execute(void Function(String) callback) {
  callback('Dart');
}

// execute(() => print('done')); // 参数数量不匹配

回调类型要求一个 String 参数,因此应写成:

execute((value) => print(value));

或者显式忽略参数:

execute((_) => print('done'));

下划线参数表示该参数有意不使用。


11.3 异步异常没有被等待

void start(void Function() callback) {
  callback();
}

void main() {
  start(() async {
    throw StateError('failed');
  });
}

start 看不到返回的 Future,因此调用者也无法通过 await start(...) 等待这个异步操作。

修复为:

Future<void> start(Future<void> Function() callback) async {
  await callback();
}

void main() async {
  try {
    await start(() async {
      throw StateError('failed');
    });
  } catch (error) {
    print(error);
  }
}

诊断这类问题时,应沿着调用链检查:

  1. 回调实际返回什么;
  2. 参数类型是否写成 Future<...> Function(...)
  3. 中间层是否 await
  4. 最上层是否捕获错误。

11.4 闭包读取了变化后的变量

void main() {
  var message = 'old';

  final printMessage = () => print(message);

  message = 'new';

  printMessage(); // new
}

如果需要固定创建时的值,应创建新的不可变绑定:

void main() {
  var message = 'old';
  final fixedMessage = message;

  final printMessage = () => print(fixedMessage);

  message = 'new';

  printMessage(); // old
}

这里不是闭包“延迟复制”或“自动快照”,而是闭包读取它捕获的变量绑定。


十二、面向 Flutter 的生命周期设计

Flutter 中函数和闭包最容易与生命周期问题结合起来。以下代码展示一个典型的异步加载流程:

class DataState extends State<DataWidget> {
  bool _loading = false;
  String? _value;

  Future<String> fetchValue() async {
    await Future<void>.delayed(const Duration(seconds: 1));
    return 'loaded';
  }

  Future<void> load() async {
    setState(() {
      _loading = true;
    });

    try {
      final value = await fetchValue();

      if (!mounted) {
        return;
      }

      setState(() {
        _value = value;
        _loading = false;
      });
    } catch (error, stackTrace) {
      if (!mounted) {
        return;
      }

      setState(() {
        _loading = false;
      });

      FlutterError.reportError(
        FlutterErrorDetails(
          exception: error,
          stack: stackTrace,
        ),
      );
    }
  }
}

这里有三条独立的控制线:

  1. Future 表达请求何时完成;
  2. try/catch 表达请求失败如何传播;
  3. mounted 防止 State 已销毁后调用 setState

闭包可能捕获 this,但 mounted 检查不会自动取消网络请求。若请求本身支持取消,应在 dispose 中取消;若不支持取消,则至少在结果返回后丢弃过期结果。

对于监听器:

class CounterState extends State<CounterWidget> {
  late final VoidCallback _onCounterChanged;

  @override
  void initState() {
    super.initState();

    _onCounterChanged = () {
      if (!mounted) {
        return;
      }

      setState(() {});
    };

    widget.counter.addListener(_onCounterChanged);
  }

  @override
  void dispose() {
    widget.counter.removeListener(_onCounterChanged);
    super.dispose();
  }
}

注册和注销必须使用同一个回调引用,并且注销应发生在对象销毁过程中。对于定时器、订阅和事件源,也应分别保存取消句柄:

Timer? _timer;
StreamSubscription<int>? _subscription;

@override
void dispose() {
  _timer?.cancel();
  _subscription?.cancel();
  super.dispose();
}

Flutter 不会因为闭包是局部变量就自动取消它注册的外部资源。


十三、函数与闭包 API 的设计取舍

13.1 何时使用普通函数

使用命名函数的情况:

  • 行为有明确业务名称;
  • 需要在多个位置复用;
  • 需要单独测试;
  • 不依赖局部上下文;
  • 希望堆栈和诊断信息更清晰。
bool isValidEmail(String value) {
  return value.contains('@');
}

13.2 何时使用闭包

使用闭包的情况:

  • 行为只在一个调用点使用;
  • 需要捕获局部配置;
  • 需要创建带私有状态的函数;
  • 需要临时组合已有函数。
final minLength = 8;

final validate = (String value) {
  return value.length >= minLength;
};

如果闭包捕获的状态越来越多,或者需要暴露多个相关操作,应考虑改成普通类或 Callable 类。


13.3 何时使用 Callable 对象

使用 Callable 对象的情况:

  • 行为有持续状态;
  • 需要配置参数;
  • 需要实现接口;
  • 需要同时提供调用操作和其他方法;
  • 需要在测试中以对象身份管理其生命周期。

如果对象没有状态、接口或额外语义,Callable 类可能只是对简单闭包的过度封装。


13.4 API 类型应优先描述行为,而不是实现

不推荐:

void register(Function callback) {}

更推荐:

void register(Future<void> Function(Event event) callback) {}

后者明确了:

  • 回调接收一个 Event
  • 回调是异步的;
  • 回调完成时返回 Future<void>
  • API 可以选择等待它;
  • 调用者可以获得静态类型检查。

函数类型越准确,错误越早暴露在编译期,而不是在用户点击、网络失败或页面销毁后才以运行时异常出现。


结语

Dart 函数机制可以归纳为一条完整链路:

参数声明
  -> 确定调用形式
函数类型
  -> 确定可替代关系
函数值 / tear-off
  -> 允许传递和保存行为
闭包捕获
  -> 让行为携带词法环境
Callable 对象
  -> 将可调用行为与状态组织在一起
API 设计
  -> 明确同步、异步、错误和生命周期契约

参数语法解决“调用者如何传值”,函数类型解决“什么函数可以替代什么函数”,闭包解决“函数离开原作用域后如何继续访问状态”,Callable 解决“对象如何以函数形式暴露行为”。

在 Flutter 工程中,最需要警惕的不是匿名函数本身,而是函数值背后的生命周期:异步回调是否被等待,监听器是否使用同一引用注销,闭包是否继续持有已销毁的页面,以及 API 是否用类型准确表达了这些约束。


系列导航与关联阅读

官方资料

本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。