函数类型、匿名函数与闭包
在Dart里,函数像普通变量一样,可作为参数传递,或从一个函数返回,这便是所谓的函数乃一等公民(first-class object)。在Dart里可以定义匿名函数 ,也就是没有名称的函数,这在实际应用中极为方便。闭包(Closure)是与之相关的另一个重要的概念,详见后文。
函数类型
首先通过一个示例,来看一看什么是函数类型(funtion type)。
// ex261.dart
void main() {
int increase(int value, {int increment = 1}) => value + increment; // 3
var increaseFunc = increase; // 4
print(addFunc); // 5
print(increaseFunc); // 5a
//Output:
//Closure: (int, int) => int from Function 'add': static.
//Closure: (int, {int increment}) => int
}
int add(int a, int b) => a + b; // 1
int Function(int a, int b) addFunc = add; // 2
int Function(int, int) addFunc2 = add; // 2a
var addFunc3 = add; // 2b
- 声明函数
add; - 声明变量
addFunc并赋值为add,它的类型是一个函数类型int Function(int a, int b), 可见函数类型的写法与声明一个函数类似,去掉函数体,然后将函数名称换成关键字Function即可,同时可省略位置参数的名称(2a); - 在
main函数的内部,声明函数increase; - 声明变量
increaseFunc并赋值为increase; - 打印
addFunc与increaseFunc,注意观察其输出。
increase 函数定义在main函数的内部,这便是嵌套函数的概念。Closure: (int, int) => int from Function 'add': static.,这表明addFunc的值是一个闭包(Closure),并且它来自全局(static)的add函数。这里的全局讲的是作用域(Lexical Scope)。
使用 typedef
使用 typedef 关键字给函数类型取一个别名:
typedef AddFunction = int Function(int a, int b);
AddFunction addFunc4 = add;
typedef VoidCallback = void Function();
typedef 也用于给数据类型取别名:
typedef IntList = List<int>;
词法范围(Lexical Scope)
从语法上看,一对花括号({})便定义了一个词法范围,即作用域。作用域嵌套而成,外层作用域内的变量对内层可见,反之不然。下面是来自官方文档的示例:
// ex262.dart
bool topLevel = true;
void main() {
var insideMain = true;
void myFunction() {
var insideFunction = true;
void nestedFunction() {
var insideNestedFunction = true;
assert(topLevel);
assert(insideMain);
assert(insideFunction);
assert(insideNestedFunction);
}
}
}
匿名函数
像main 、print这些是命名(具名)函数。我们也可以创建没有名称的函数,称之为匿名函数。
匿名函数经常作为其他函数的参数,它与命名函数类似,特别之处在于:
- 没有函数名称;
- 也无需声明返回值的类型;
- 参数的类型是可选的。
语法格式:
(参数列表) {
函数体
}
下面的示例程序分析一句英文,然后打印其中的单词及其长度。
// ex263.dart
void main() {
var sentence = 'Its quaint events were hammered out'; // 1
var stats = sentence.split(' ').map((word) => (word, word.length)); // 2
stats.forEach(print); // 3
// Output:
// (Its, 3)
// (quaint, 6)
// (events, 6)
// (were, 4)
// (hammered, 8)
// (out, 3)
}
sentence是一句英文;sentence.split(' ')使用空格符(' ')分割sentence,得到一个单词(word)的列表; 然后将每个单词映射(map)为一个记录,内容为单词及其长度((word, word.length)); 这一句结束时将得到统计结果stats,它是一个Iterable对象(可将Iterable简单理解为一个更为泛化的列表,通过特定的方法访问其元素);- 使用
Iterable.forEach方法,打印stats中的每个元素,这里将print函数作为参数传入forEach。
第2句中的(word) => (word, word.length) 是一个匿名函数,它作为参数传入map函数。
闭包(Closure)
闭包是一个容易让人迷糊的概念。简言之,闭包是一个函数(或函数值、函数对象),很多时候它就是一个匿名函数。闭包这个概念之所以会诞生,是因为它的实用性与特殊性:
闭包即使脱离了定义它的作用域(原始作用域),依然可以访问原始作用域内的变量。
换言之,当一个函数引用了其外部作用域的变量时,它就形成了一个闭包。闭包就像一个带有‘记忆背包’的函数,即使离开了出生的家,包里依然装着它从家里带出来的变量。
下面这个示例来自官方文档:
// ex264.dart
Function makeAdder(int addBy) {// 1
return (int i) => addBy + i; // funcA
}
void main() {
var add2 = makeAdder(2); // 2
var add4 = makeAdder(4); // 2a
assert(add2(3) == 5); // 3
assert(add4(3) == 7); // 3a
}
makeAdder是一个函数,它返回一个匿名函数(int i) => addBy + i;,为方便描述,称之为funcA;- 分别调用
makeAdder(2)、makeAdder(4)得到函数add2、add4; - 断言
add2(3)等于5, 断言add4(3)等于7。
add2、add4 都记住了makeAdder被调用时的addBy(makeAdder的参数)。从数学的角度来看funcA:
\( funcA( i ) = addBy + i \)
这个函数里的自变量是i, addBy是一个常量。 add2 = makeAdder(2), 相当于指定常量 addBy = 2,add4类似。add2、add4 等都是闭包。
深度解析
内存中的“生存转移”
当 Dart 编译器发现一个函数引用了外部变量时,它会做一件特殊的事情:将这些变量从“栈”提升到“堆”中。
-
普通局部变量:存储在栈上,函数退出,内存立即回收。
-
被捕获的变量:存储在一个特殊的“环境对象”(Context Object)中,存放在堆上。只要匿名函数(闭包)还被引用,这个环境对象就不会被 GC(垃圾回收)。
生命周期延长现象
闭包使得变量的生命周期(Lifetime)脱离了它原本的作用域(Scope)。
“捕获的是变量,而不是值”
闭包捕获的是对变量引用的持有,而不是该变量在捕获那一刻的快照。这意味着:
- 共享状态:如果有多个闭包捕获了同一个变量,它们共享同一个状态。
- 副作用:如果在外部修改了该变量,闭包内看到的值也会变。
// ex265.dart
void main() {
var x = 10;
void showX() => print(x);
x = 20;
showX(); // Output: 20
}
最佳实践
匿名函数及其形成的闭包是极其强大的工具,但若误用很容易导致内存泄漏、内存占用过高或GC抖动(短时间内频繁触发GC)。 编程时要注意避免 “过度捕获”陷阱 与 长生命周期持有。
Dart 的闭包捕获是基于整个作用域对象的,而不仅仅是捕获那个被用到的变量。
class MyWidget {
String name = "Widget";
List<int> massiveData = List.generate(1000000, (i) => i);
void doSomething() {
// 这个匿名函数虽然只用了 name,但由于它捕获了隐式的 'this'
// 导致整个 MyWidget 实例(包括 massiveData)都无法被释放
button.onTap = () => print(name);
}
}
实践中,我们要及时“切断”闭包与外部作用域变量的持有关系。
a. 及时置空
如果一个长生命周期的对象持有了一个回调,在不需要时将其设为 null。
button.onTap = null; // 切断匿名函数及其捕获作用域的链接
b. 局部变量转换(避免捕获 this)
void doSomething() {
final nameSnapshot = this.name; // 将属性存为局部变量
button.onTap = () => print(nameSnapshot); // 只捕获字符串,不捕获 'this' 对象
}
c. 生命周期对齐
在 Flutter 开发中,闭包的寿命应严格受限于 Widget 的生命周期。利用 dispose 是切断关系的黄金时机。
- 监听器清理:使用
controller.removeListener(myListener)。 - 流订阅清理:调用
streamSubscription.cancel()。
d. 避免在异步间隙中持有(Async Gap)
在 await 之后的闭包操作要格外小心,因为 await 会挂起当前函数,此时所有的局部变量都会保持存活。
Future<void> process(MassiveObject huge) async {
await someLongRunningTask(); // 挂起期间,huge 对象一直无法被回收
print(huge.id);
}
如果 huge 对象很大,且 await 之后只需要它的一个 id,请先提取 id,然后让 huge 脱离作用域或设为 null。
练习
修改示例程序ex263,将统计结果按单词的长度降序排序。提示:使用List.sort方法。