Dart同步生成器惰性生成的含义及与普通函数的差异
Dart同步生成器的「惰性」到底指什么?
核心本质很简单:生成器的值计算逻辑,不会在函数调用时执行,只会延后到元素被实际访问时按需执行,且每次只计算当前需要的值,算完就暂停,和普通函数的执行逻辑有本质差异。
两者的执行行为差异
你写的两个函数最终拿到的列表结果一致,但执行时机、执行流程完全不一样:
普通返回List的函数(naturalsTo2)
只要你一调用函数,它的函数体就会从头到尾立刻执行完毕:初始化变量、循环跑完所有轮次、把所有元素塞进列表、最后把完整的列表返回。不管你之后会不会用这些元素、用几个,所有计算、内存分配的工作在调用瞬间就全部做完了。
sync*修饰的同步生成器(naturalsTo)
你调用它的时候,函数内部的代码一行都不会跑,只会立刻返回一个Iterable对象。只有当你实际迭代这个Iterable的时候(比如调用toList()、for-in遍历、取first/skip这类操作),函数体才会开始执行,而且是走一步停一步:
- 第一次取元素:函数执行到第一个
yield,算出当前值返回给迭代器,同时暂停执行,局部变量的值、当前执行到的代码位置全部保留 - 下一次取元素:函数从上次暂停的位置恢复,继续往下执行到下一个
yield,返回新值后再次暂停 - 直到函数执行到末尾,才会给迭代器发「遍历完成」的信号,整个流程结束
一眼看明白差异的测试代码
你可以跑下面这段加了日志的代码,执行顺序的区别会非常直观:
Iterable<int> naturalsTo(int n) sync* { print('生成器开始执行'); int k = 0; while (k < n) { print('即将产出$k'); yield k++; } print('生成器执行结束'); } Iterable<int> naturalsTo2(int n){ print('普通函数开始执行'); int k=0; List<int> resultList = []; for(k;k<n;k++){ print('即将添加$k到列表'); resultList.add(k); } print('普通函数执行结束'); return resultList; } void main() { print('--- 测试普通函数 ---'); final list2 = naturalsTo2(3); print('普通函数调用完成,准备转列表'); print(list2.toList()); print('\n--- 测试生成器函数 ---'); final list1 = naturalsTo(3); print('生成器调用完成,准备转列表'); print(list1.toList()); }
运行输出对比如下:
// 普通函数的执行顺序:调用就全跑完 --- 测试普通函数 --- 普通函数开始执行 即将添加0到列表 即将添加1到列表 即将添加2到列表 普通函数执行结束 普通函数调用完成,准备转列表 [0, 1, 2] // 生成器的执行顺序:调用时啥也不干,迭代时才按需跑 --- 测试生成器函数 --- 生成器调用完成,准备转列表 生成器开始执行 即将产出0 即将产出1 即将产出2 生成器执行结束 [0, 1, 2]
惰性设计的实际价值
- 省计算资源:如果你只需要迭代前N个元素,生成器根本不会计算N之后的所有值。比如写一个生成100万个数字的生成器,你只取
first拿第一个值,它只会执行到第一个yield就停,不会跑满100万次循环。 - 支持无限序列:你完全可以写一个
while(true)持续yield递增数字的生成器,调用时不会死循环,你取多少它就算多少,普通函数写无限循环加列表会直接卡死。 - 内存开销更低:不需要一次性把所有元素都存在内存里,迭代过的元素如果没有其他引用可以随时回收,处理超大规模序列的时候内存压力小很多。
内容的提问来源于stack exchange,提问作者森口万太郎
相关产品推荐
相关产品推荐

