Functions(函数)
函数把一段逻辑封装成可以重复调用的代码。Dart 中的函数也是对象,可以保存到变量中、作为参数传递,或从其他函数返回。
学习 Dart 函数时,重点需要掌握以下问题:
- 如何声明函数并指定参数与返回值?
- 位置参数、命名参数和可选参数分别如何使用?
=>简写适用于什么函数?- 如何把函数作为值传递?
- 闭包为什么能够记住外层作用域中的变量?
声明与调用函数
函数声明通常由返回类型、函数名、参数列表和函数体组成:
int add(int left, int right) {
return left + right;
}
void main() {
final result = add(2, 3);
print(result); // 5
}
上例中:
int表示add()返回整数。left和right是两个必需的位置参数,调用时必须按顺序传入。return结束当前函数,并把计算结果交给调用方。
函数参数和返回值都属于函数公开契约的一部分。即使分析器能够推断返回类型,公开函数通常也应明确写出返回类型,让调用方式更容易理解。
不返回结果的函数
只执行操作、不向调用方返回结果的函数使用 void:
void greet(String name) {
print('Hello, $name!');
}
void main() {
greet('Dart'); // Hello, Dart!
}
void 表示调用方不应使用该函数的返回结果,而不是要求函数体必须为空。
表达式函数体
函数体只有一个表达式时,可以使用 => 简写:
int square(int value) => value * value;
void main() {
print(square(4)); // 16
}
=> expression 等价于 { return expression; }。箭头后面只能写表达式,不能直接放 if、for 等语句。逻辑包含多条语句时,应使用普通函数体。
TypeScript 对比:两种语言都支持箭头写法,但 Dart 的
=>只是单表达式函数体的简写,不负责声明函数值。Dart 匿名函数的参数列表写在=>前,例如(value) => value * 2。
位置参数
普通参数是必需的位置参数。实参的含义由调用位置决定:
String describeUser(String name, int age) {
return '$name is $age years old.';
}
void main() {
print(describeUser('Ada', 36));
}
调用 describeUser() 时必须提供两个参数,而且顺序必须与声明一致。适合少量、含义直观的参数;参数较多或多个参数类型相同时,命名参数通常更易读。
命名参数
命名参数放在 {} 中,调用时通过名称传值,因此顺序可以改变:
String createLabel({required String text, required String color}) {
return '$text ($color)';
}
void main() {
print(createLabel(color: 'blue', text: 'Save')); // Save (blue)
}
命名参数默认是可选的。添加 required 后,调用方必须提供该参数。required 只要求调用时出现该名称;参数值是否允许为 null,仍由参数类型决定:
void showMessage({required String? message}) {
print(message ?? 'No message');
}
void main() {
showMessage(message: null); // No message
// showMessage(); // 编译错误:缺少 message
}
可选命名参数与默认值
可选命名参数必须允许“调用方没有传值”这种状态。可以把它声明为可空类型,也可以提供默认值:
String formatName({String? prefix, String separator = ' '}) {
final name = prefix ?? 'Guest';
return '$name${separator}User';
}
void main() {
print(formatName()); // Guest User
print(formatName(prefix: 'Dart', separator: '-')); // Dart-User
}
默认参数值必须是编译时常量。调用方省略 separator 时,函数使用 ' ';显式传入其他值时,则使用调用方提供的值。
TypeScript 对比:TypeScript 没有 Dart 这种内建的命名实参语法。常见替代方式是接收一个对象并进行解构;Dart 的命名参数则直接属于函数签名,分析器会检查参数名称和
required约束。
function createLabel(options: { text: string; color: string }): string {
return `${options.text} (${options.color})`
}
createLabel({ color: 'blue', text: 'Save' })
可选位置参数
放在 [] 中的位置参数可以省略,但仍然按照位置传递:
String joinWords(String first, [String second = '', String third = '']) {
return [first, second, third].where((word) => word.isNotEmpty).join(' ');
}
void main() {
print(joinWords('Learn')); // Learn
print(joinWords('Learn', 'Dart')); // Learn Dart
print(joinWords('Learn', 'Dart', 'Functions')); // Learn Dart Functions
}
必需位置参数必须写在可选位置参数之前。可选位置参数如果没有默认值,其类型通常需要允许 null:
void printScore(String name, [int? score]) {
print(score == null ? '$name: no score' : '$name: $score');
}
一个参数列表可以使用可选位置参数或命名参数,但不能同时使用两种可选参数形式。参数含义不容易从位置看出时,优先选择命名参数。
返回值与类型推断
Dart 可以根据函数体推断返回类型:
multiply(int left, int right) => left * right;
这里返回类型会被推断为 int,但省略返回类型也可能让接口意图变得不清楚。尤其是公开 API,推荐显式写成:
int multiply(int left, int right) => left * right;
声明了不可空返回类型的函数,所有正常结束的执行路径都必须返回相应类型的值:
String scoreLevel(int score) {
if (score >= 90) {
return 'A';
}
if (score >= 60) {
return 'B';
}
return 'C';
}
如果最后一个 return 'C'; 缺失,函数就可能在没有返回 String 的情况下结束,无法满足声明的返回类型。
函数是一等对象
Dart 把函数当作一等对象(first-class object):函数可以赋给变量,也可以作为参数和返回值使用。
int add(int left, int right) => left + right;
void main() {
final operation = add;
print(operation(2, 3)); // 5
}
这里的 operation 保存的是函数本身,operation(2, 3) 才是调用函数。直接引用已有函数而不调用它,称为函数撕取(function tear-off)。
明确声明函数类型
函数类型描述参数和返回值的契约:
int calculate(int left, int right, int Function(int, int) operation) {
return operation(left, right);
}
int add(int left, int right) => left + right;
int subtract(int left, int right) => left - right;
void main() {
print(calculate(8, 3, add)); // 11
print(calculate(8, 3, subtract)); // 5
}
int Function(int, int) 表示“接收两个 int 并返回 int 的函数”。相比只写宽泛的 Function,具体函数类型能够让分析器检查参数和返回值,也能让调用方直接看懂契约。
复杂或重复使用的函数类型可以通过 typedef 命名,详见 Typedefs(类型别名)。
TypeScript 对比:Dart 的
int Function(int, int)可以类比为 TypeScript 的(left: number, right: number) => number。两者都会约束函数的参数和返回值。
匿名函数与回调
没有名称的函数称为匿名函数。它常作为回调函数直接传给另一个函数:
void main() {
final numbers = [1, 2, 3];
final doubled = numbers.map((number) {
return number * 2;
});
print(doubled.toList()); // [2, 4, 6]
}
匿名函数也可以使用表达式函数体:
void main() {
final numbers = [1, 2, 3];
final doubled = numbers.map((number) => number * 2);
print(doubled.toList()); // [2, 4, 6]
}
map() 接收一个函数,并在遍历时调用它。这里的 (number) => number * 2 是回调;它描述“每个元素应该如何转换”,而 map() 负责遍历。
当已有函数的签名正好匹配时,可以直接传递函数撕取,避免再包一层匿名函数:
String normalize(String value) => value.trim().toLowerCase();
void main() {
final topics = [' Dart ', ' FUNCTIONS '];
print(topics.map(normalize).toList()); // [dart, functions]
}
词法作用域
Dart 使用词法作用域(lexical scope):变量是否可访问,由函数在源代码中的嵌套位置决定。
String language = 'Dart';
void main() {
const topic = 'Functions';
void printTopic() {
print('$language: $topic');
}
printTopic(); // Dart: Functions
}
内层函数 printTopic() 可以访问自己的作用域、外层 main() 的作用域以及顶层作用域。外层作用域不能反过来访问只在内层函数中声明的变量。
当内外层出现同名变量时,距离当前代码最近的声明会遮蔽外层声明:
const label = 'top level';
void main() {
const label = 'main';
void showLabel() {
const label = 'local';
print(label);
}
showLabel(); // local
}
过多的同名遮蔽会增加理解成本,实际代码中应优先使用能够表达含义的不同名称。
闭包
闭包(closure)是能够访问并保留其词法作用域中变量的函数。即使外层函数已经执行结束,返回的内层函数仍然可以使用被捕获的变量:
int Function() createCounter() {
var count = 0;
return () {
count++;
return count;
};
}
void main() {
final next = createCounter();
print(next()); // 1
print(next()); // 2
print(next()); // 3
}
createCounter() 返回后,局部变量 count 仍被返回的函数捕获。每次调用 next() 都会读取并更新同一个 count。
每次调用外层函数都会创建独立的闭包状态:
void main() {
final firstCounter = createCounter();
final secondCounter = createCounter();
print(firstCounter()); // 1
print(firstCounter()); // 2
print(secondCounter()); // 1
}
TypeScript 对比:Dart 和 TypeScript 的闭包都能捕获外层词法作用域中的变量。理解 Dart 闭包时可以沿用这一思路,但变量仍受 Dart 静态类型和空安全规则约束。
入口函数 main()
每个 Dart 程序都从顶层 main() 函数开始执行。最简单的入口不接收参数:
void main() {
print('Hello, Dart!');
}
命令行程序需要读取启动参数时,可以声明 List<String> 参数:
void main(List<String> arguments) {
print('Arguments: $arguments');
}
arguments 只包含传给程序的命令行参数,不包含可执行文件名称。
Dart 不按参数签名重载函数
Dart 不支持在同一作用域中仅凭参数数量或参数类型声明多个同名函数:
// 不能同时声明这两个同名函数。
// String format(int value) => '$value';
// String format(double value) => value.toStringAsFixed(2);
通常可以使用不同名称、共同的父类型、命名参数或可选参数来表达不同调用方式:
String formatNumber(num value, {int fractionDigits = 0}) {
return value.toStringAsFixed(fractionDigits);
}
void main() {
print(formatNumber(12)); // 12
print(formatNumber(12.345, fractionDigits: 2)); // 12.35
}
TypeScript 对比:TypeScript 可以为一个实现声明多个重载签名,Dart 没有直接对应的函数重载语法。迁移这类 API 时,通常需要重新设计名称或参数形式。
常见误区
误区一:忘记命名参数默认可选
不可空命名参数如果没有默认值,通常需要添加 required:
void openPage({required String path}) {
print(path);
}
required 表示调用方必须传入,不能只依赖不可空类型表达这一要求。
误区二:把函数传递写成函数调用
final values = [' Dart ', ' Functions '];
values.map(normalize); // 传递函数
// values.map(normalize()); // 错误:这里会先尝试调用 normalize
需要回调时传递函数本身;只有确实需要立即执行时才写括号。
误区三:使用宽泛的 Function 类型
void run(Function operation) {
operation();
}
Function 没有准确说明参数和返回值。已知签名时,应写出具体类型:
void run(void Function() operation) {
operation();
}
误区四:在 => 后书写语句
=> 后只能放表达式。需要条件分支、多条语句或局部变量时,改用 {} 函数体。
误区五:用过多位置参数降低可读性
当多个参数类型相同,createUser('Ada', 'admin', 'active') 很难直接看出每个值的含义。使用命名参数可以让调用处表达意图,并减少顺序错误。
如何选择参数形式
可以按照以下思路选择:
- 参数数量少、含义明确且每次调用都需要:使用必需位置参数。
- 参数较多、布尔值较多或调用处需要表达含义:使用命名参数。
- 命名参数每次都必须提供:添加
required。 - 参数可以省略:根据调用可读性选择可选命名参数或可选位置参数,并提供默认值或使用可空类型。
- 函数需要接收另一个函数:使用具体的
返回类型 Function(参数类型)声明回调契约。
小结
- Dart 函数可以显式声明参数类型和返回类型;不返回结果时使用
void。 =>是单表达式函数体的简写,不适合多条语句。- 普通参数按位置传递;
{}声明命名参数,[]声明可选位置参数。 - 命名参数默认可选,必须传入时使用
required;默认参数值必须是编译时常量。 - 函数是对象,可以保存、传递和返回;已知签名时应使用具体函数类型,而不是宽泛的
Function。 - 匿名函数常用于回调,已有函数签名匹配时可以直接传递函数撕取。
- 闭包能够捕获并保留外层词法作用域中的变量。
- Dart 不支持按参数签名重载函数,通常通过命名参数、可选参数或不同函数名表达不同调用方式。