Menu

PHP 生成器:yield、yield from 与内存占用

生成器是用 yield 逐个返回值的函数,foreach 可以遍历一百万个元素而无需先构建数组。本文讲解 yield、键、节省内存、无限序列、yield from、send() 和 getReturn()。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

生成器是包含 yield 的函数。调用它时并不会执行函数体,而是返回一个 Generator 对象,可以用 foreach 遍历。每个 yield $value 把一个值交给循环,并让函数暂停,直到循环请求下一个值,所以值是逐个产生的,而不是先全部收集到数组里。

输出在生成器和循环之间交替出现:每个值只有在循环请求时才会被生成。

生成器能节省内存

range(1, 1000000) 会在循环开始之前构建一个包含一百万个整数的数组。而 yield 同样这些数的生成器,任何时候都只持有其中一个。运行下面的代码,比较两种写法各自需要的内存:

两个循环输出的和相同,都是 500000500000,但数组版本报告 extra memory 16.0 MB,生成器版本是 extra memory 0.0 MB。数据越多,差距越大。正是同样的思路让生成器适合处理大文件、数据库查询结果和分页 API:处理一项,丢掉它,再取下一项。

yield 键和值

yield $key => $value 设置 foreach ($gen as $key => $value) 接收到的键。不指定键时,生成器会像列表一样从 0 开始给值编号。一个常见用法是带行号逐行读取文件:

内存中每次只有一行,所以同一个函数也能处理 2 GB 的日志文件。这里用到的文件函数见 fopen() 和 fgets()。

无限序列和惰性管道

生成器可以无限循环,因为它只会运行到使用方请求的地方为止。可以用 break 停止,也可以把它包在另一个只取前几个值的生成器里。把小生成器串联起来,就得到一个每一步都是惰性的管道:

fibonacci() 自己永远不会结束,但程序能正常结束:take() 取够数量后就不再请求,之后的值根本不会被计算。

yield from:委托给另一个生成器

yield from 会把数组、迭代器或另一个生成器的每个值依次传出去。用它可以把一个生成器拆成几个更小的生成器,或者递归遍历嵌套结构:

yield from 会保留内部的键,所以两个数组都会产生键 0 和 1。于是 iterator_to_array() 会覆盖掉 a 和 b。只要你只关心值,就把 false 作为第二个参数传入。

send() 和 getReturn()

生成器也可以接收值。$x = yield; 会暂停并等待;$gen->send($value) 让生成器继续运行,并把 $value 作为这个 yield 的结果。生成器还可以 return 一个最终值,在它结束后用 getReturn() 读取。

大多数代码都用不到 send();日常用法是用 foreach 惰性读取。它是协程库背后的基础构件。

生成器只能遍历一次

生成器只能向前运行,无法倒回。一旦某个 foreach 把它消费完,再次遍历同一个对象就会抛出异常:

解决办法是每次遍历都重新调用生成器函数,foreach (colors() as $c);如果确实需要用两次,就用 iterator_to_array(colors(), false) 一次性收集所有值。如果你发现自己经常这样做,说明这份数据更适合用普通数组。

常见问题

PHP 中的 yield 有什么作用?

yield $value 把一个值交给遍历该函数的代码,并让函数在这里暂停。循环进入下一次迭代时,函数从 yield 之后继续执行。任何包含 yield 的函数在调用时都不会直接运行,而是返回一个 Generator 对象。

为什么用生成器而不用数组?

数组会把所有元素同时放在内存里;生成器每次只产生一个元素。遍历 range(1, 1000000) 会分配全部一百万个整数,而逐个 yield 这些数的生成器内存占用基本不变。生成器还支持无限序列,并且循环 break 时会提前停止工作。

PHP 中的 yield from 是什么?

yield from $source 会把另一个生成器、数组或 Traversable 的每个值依次传出去,就好像当前生成器自己 yield 了它们一样。它的求值结果是内部生成器的返回值。

生成器可以遍历两次吗?

不可以。生成器只能向前运行。一次 foreach 结束后,再次遍历会抛出 Exception: Cannot traverse an already closed generator。要再遍历一次,就重新调用生成器函数得到一个新的生成器;如果确实需要用两次,就用 iterator_to_array() 把值保存下来。

怎么把生成器转换成数组?

iterator_to_array($gen) 会收集所有值,并保留生成器的键。如果键有重复(例如使用了 yield from),后面的值会覆盖前面的值,所以要把 false 作为第二个参数传入,得到一个普通列表:iterator_to_array($gen, false)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始