PHP8.5怎么实现生成器节省内存占用
前言先纠正一个版本事实生成器Generator不是 PHP 8.5 的特性。它是PHP 5.5引入的yield from委托语法是PHP 7.0加的此后语义基本稳定。标题里的PHP8.5有误——当然你用 PHP 8.5 跑生成器完全没问题本文的示例就按 PHP 8.x 写并且只用到 8.0 及以后确定存在的语法联合类型、构造函数属性提升等。所以准确的说法是在 PHP 8.5 上怎么用生成器省内存而不是PHP 8.5 引入了生成器。第二个要澄清的误解是生成器省的不只是内存更是峰值内存。file()一次性把整个文件读进数组内存占用与文件大小成正比生成器每轮只持有一行的状态进程的峰值内存与文件大小无关。这两者在平均内存上也许差得不多但峰值内存才决定你的进程会不会被 OOM Killer内存不足终止器干掉。本文讲清楚生成器为什么省内存原理层面而不是据说很快、怎么写才真的省、以及那些你以为省了其实没省的写法。文中所有测量代码都可以直接跑具体数字请在自己的机器上实测。一、生成器为什么省内存它把数据集换成了状态普通函数返回的是一个已完成的值要么是标量要么是一个装满了所有元素的数组。数组必须在函数返回之前就全部构造好所以它必然占用 O(元素个数) 的内存。生成器函数内部含yield的函数被调用时不执行函数体而是立刻返回一个Generator对象。这个对象只保存三样东西函数当前执行到的位置、局部变量的当前值、以及调用方的上下文。每次yield把值交给调用方后函数栈帧被挂起而不是销毁下一次需要值时再从挂起处恢复。所以内存模型从O(n) 的数据变成了O(1) 的状态 每次一个元素。这就是全部的秘密。对比项返回数组的函数生成器函数返回值类型array或声明类型Generator对象yield决定无法声明为array求值时机调用时立即全部求值eager首次取值时才开始lazy内存量级O(n)n 是元素个数O(1)与元素个数无关峰值出现时刻函数返回前迭代过程中的任意时刻且恒定能否遍历两次可以不可以取完即失效count()直接可用不可用会报TypeError一个关键推论如果调用方最终还是把生成器转成了数组省内存的努力就全废了。最常见的翻车写法就是iterator_to_array($gen)。二、从读一个大文件开始假设要处理一个几百 MB 的 CSV 日志。反面教材?php // ❌ 一次性读入内存与文件大小成正比 function readAllLines(string $path): array { return file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); } foreach (readAllLines(/var/log/big.csv) as $line) { // 处理 $line }file()会把整个文件读进内存里的一个数组数组每个元素还各有一个字符串 zvalPHP 内部变量的数据结构的开销。文件多大内存就吃多大。生成器版本?php // ✅ 逐行读取任意时刻内存里只有一行 function readLines(string $path): Generator { $handle fopen($path, rb); if ($handle false) { throw new RuntimeException(无法打开文件: {$path}); } try { while (($line fgets($handle)) ! false) { yield rtrim($line, \r\n); } } finally { // 无论正常结束、break 提前退出还是抛异常都会走到这里 fclose($handle); } } foreach (readLines(/var/log/big.csv) as $line) { // 处理 $line }这里finally里的fclose()很关键。生成器被break打断时并不会自动清理资源finally保证了句柄一定被释放——这也是生成器相对手写迭代器的一个便利之处可以直接用try/finally表达清理逻辑。三、真正的杀手场景从数据库流式取数读文件还不是最痛的地方。最痛的是导出百万行数据——fetchAll()把整张表塞进 PHP 数组然后json_encode()又要复制一份内存直接翻倍。正确做法是让 PDO 使用非缓冲查询unbuffered query配合生成器一条一条地取?php // stream-export.php —— 需要 PHP 8.0需要 pdo_mysql 扩展 // 用法php stream-export.php /** * 流式产出查询结果内存与结果集行数无关。 * * 注意非缓冲查询期间同一个连接上不能执行其它查询。 */ function streamRows(PDO $pdo, string $sql): Generator { // 关键关闭客户端缓冲让 MySQL 逐条下发结果 $pdo-setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); $stmt $pdo-query($sql); try { while (($row $stmt-fetch(PDO::FETCH_ASSOC)) ! false) { yield $row; } } finally { $stmt-closeCursor(); // 必须关闭游标否则连接会被占用 $pdo-setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); } } $pdo new PDO( mysql:host127.0.0.1;dbnameshop;charsetutf8mb4, root, , [PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION] ); // 一边查一边写 CSV全程不构造大数组 $out fopen(php://stdout, wb); if ($out false) { exit(无法打开输出流\n); } fputcsv($out, [订单号, 金额, 下单时间]); $count 0; foreach (streamRows($pdo, SELECT order_no, amount, created_at FROM orders ORDER BY id) as $row) { fputcsv($out, [$row[order_no], $row[amount], $row[created_at]]); $count; } fclose($out); fprintf(STDERR, 共导出 %d 行峰值内存 %d 字节\n, $count, memory_get_peak_usage(true));这段代码里有三个容易漏掉的点PDO::MYSQL_ATTR_USE_BUFFERED_QUERY false是必须的。默认是缓冲模式意味着query()执行完整个结果集已经在 PHP 的客户端缓冲里了——生成器这时候已经晚了内存早就吃满了。非缓冲查询期间不能在同一条连接上发别的 SQL。常见错误是在循环体里用同一个$pdo去写日志表会报Cannot execute queries while other unbuffered queries are active。要么另开一条连接要么先把数据攒够一批再写。closeCursor()必须调用。生成器被提前break时游标不会自己关闭连接会一直挂在那里。顺手测一下峰值内存想知道效果用memory_get_peak_usage()自己测。注意区分两个参数memory_get_usage()是当前用量memory_get_peak_usage(true)是 PHP 从系统申请到的峰值内存以 2MB 为粒度向上取整后者更能反映对进程的实际压力。?php // memory-probe.php —— 需要 PHP 8.0自造一个 20 万行的文件来对比 $path sys_get_temp_dir() . /gen-probe.csv; $fp fopen($path, wb); for ($i 0; $i 200000; $i) { fwrite($fp, row-{$i}, . str_repeat(x, 50) . \n); } fclose($fp); function report(string $label, string $path, callable $runner): void { $before memory_get_usage(); $peakBefore memory_get_peak_usage(true); $runner($path); printf( %-16s 当前增量%8d B 峰值%8d B\n, $label, memory_get_usage() - $before, memory_get_peak_usage(true) - $peakBefore ); } report(file() 全量读入, $path, function (string $p): void { $rows file($p, FILE_IGNORE_NEW_LINES); $n count($rows); }); report(生成器逐行, $path, function (string $p): void { $h fopen($p, rb); $n 0; while (fgets($h) ! false) { $n; } fclose($h); }); unlink($path);跑完后你会看到两种截然不同的增长形态file()那一行的当前增量会随文件行数线性上涨生成器那一行的增量基本是个常量。具体数值取决于机器与文件内容请以自己跑出来的结果为准本文不替你的机器下结论。真正有意义的是趋势对比。四、生成器的进阶用法yield from做委托。PHP 7.0 起可以用yield from把一个可迭代结构摊平进当前生成器不需要手写循环?php function inner(): Generator { yield 1; yield 2; } function outer(): Generator { yield 0; yield from inner(); // 委托等价于 foreach (inner() as $v) { yield $v; } yield from [3, 4]; // 数组也能委托 yield 5; } var_dump(iterator_to_array(outer())); // [0, 1, 2, 3, 4, 5]yield from还有一个细节上的好处它会把send()的值和异常透传给内层生成器而手写foreach做不到这一点。双向通信send()。生成器不只是生产值还能接收值?php function accumulator(): Generator { $sum 0; while (true) { // yield 表达式的值就是调用方 send() 进来的值 $value yield $sum; if ($value null) { return $sum; // 通过 getReturn() 取回 } $sum $value; } } $gen accumulator(); $gen-current(); // 启动跑到第一个 yield $gen-send(10); // 加 10 $gen-send(5); // 加 5 $gen-send(null); // 结束 echo $gen-getReturn(), PHP_EOL; // 15注意getReturn()的时机只有生成器完全迭代结束之后才能取到返回值中途调用会抛Exception。惰性求值带来的副作用时机变化。生成器函数体直到第一次取值才执行?php function lazy(): Generator { echo 函数体开始执行\n; yield 1; } $gen lazy(); // 什么都不会输出 echo 赋值完成\n; $gen-current(); // 这时才输出 函数体开始执行这个特性会让函数调用时就该发生的副作用打日志、校验参数、抛异常推迟到迭代时是排查时很容易看错方向的一类 bug。常见坑点1. 用iterator_to_array()把生成器转成数组// ❌ 一行代码把省下来的内存全花回去还多了一次全量复制 $rows iterator_to_array(streamRows($pdo, $sql)); foreach ($rows as $row) { /* ... */ }// ✅ 让生成器从头到尾保持流的形态 foreach (streamRows($pdo, $sql) as $row) { /* ... */ }2. 试图对生成器count()或$gen[0]// ❌ Generator 不是数组count() 直接 TypeError $n count(readLines($path)); $first readLines($path)[0];// ✅ 遍历中自己计数取首个元素用 current() 或先 foreach 一次 $n 0; foreach (readLines($path) as $_) { $n; }3. 想遍历两次同一个生成器// ❌ 第二次 foreach 什么都不输出生成器已经耗尽且无法 rewind $gen readLines($path); foreach ($gen as $line) { /* 第一遍 */ } foreach ($gen as $line) { /* 空的 */ }// ✅ 生成器函数是每次调用产生一个新的可迭代对象重复调用即可 foreach (readLines($path) as $line) { /* 第一遍 */ } foreach (readLines($path) as $line) { /* 第二遍拿到全新生成器 */ }对已经跑过一段的生成器调用rewind()会抛Exception: Cannot rewind a generator that was already run这个报错信息本身就说明问题。4. 声明了: array返回类型却写了yield// ❌ 致命错误含 yield 的函数返回类型只能是 Generator/Iterator/Traversable 等 function rows(): array { yield 1; }// ✅ 返回类型老老实实写 Generator function rows(): Generator { yield 1; }5. 在生成器里抛异常却在外面用try包错了位置// ❌ 异常发生在迭代那一刻包住创建语句是没用的 try { $gen readLines(/不存在的文件); } catch (RuntimeException $e) { // 永远不会进来 }// ✅ try 要包住迭代过程 try { foreach (readLines(/不存在的文件) as $line) { /* ... */ } } catch (RuntimeException $e) { error_log($e-getMessage()); }6. 生成器里的return被当成返回给调用方// ❌ 以为 $result 拿到了数组 function gen(): Generator { yield 1; return [1, 2]; } $result gen(); // 拿到的是 Generator 对象// ✅ 返回值只能在迭代结束后用 getReturn() 取 $g gen(); foreach ($g as $v) {} $result $g-getReturn(); // [1, 2]7. 用yield $k $v生成重复的键转数组时被悄悄覆盖// ❌ 两行 id 都是 1iterator_to_array 默认保留键只留下最后一个 function dup(): Generator { yield 1 a; yield 1 b; } var_dump(iterator_to_array(dup())); // [1 b]// ✅ 明确传 false让它按顺序追加不拿键做索引 var_dump(iterator_to_array(dup(), false)); // [a, b]8. 生成器被break打断后资源没释放// ❌ 循环只取前 10 行就退出文件句柄一直挂着直到脚本结束 foreach (readLines($hugeFile) as $line) { if ($i 10) { break; } }// ✅ 在生成器内部用 try/finally 保证 fclose/closeCursor 一定执行 // 见本文第二节 readLines() 的写法总结关注点说明真实版本生成器是PHP 5.5引入yield from是PHP 7.0标题里写 8.5 不准确省内存的原理把 O(n) 的数据换成 O(1) 的挂起状态逐次求值关键 APIyield、yield from、send()、getReturn()、current()必须配合的条件调用方不能转数组数据库查询要关掉客户端缓冲单向性生成器只能用一次不能rewind()不能count()清理逻辑用try/finally写在生成器内部break也能触发结论生成器节省峰值内存靠的不是魔法而是把一次性构造的数据集变成了逐个产生的流。真正要盯住的是两个地方——上游数据库/文件句柄是否真的流式MYSQL_ATTR_USE_BUFFERED_QUERY有没有关、下游调用方有没有用iterator_to_array()把它变回数组。这两处任何一处没做对生成器就只剩语法糖的价值内存一点都省不下来。建议在导出、批量处理、长列表遍历这三类场景里优先引入生成器并用memory_get_peak_usage(true)自己做前后对比来验证效果。

相关新闻

PHP8.5怎么实现策略模式切换不同算法

PHP8.5怎么实现策略模式切换不同算法

前言需要先纠正一个概念:策略模式不是 PHP 8.5 的特性。它是《设计模式》里提出的对象行为型模式,与语言版本无关,用 PHP 5 也能写,用 PHP 8.5 也能写。之所以标题里带着"PHP8.5",更合理的理解是"在 PH…

2026/10/3 3:15:11 阅读更多 →
fcitx5皮肤定制全攻略:主题配置、Rime联动与避坑指南

fcitx5皮肤定制全攻略:主题配置、Rime联动与避坑指南

如果你和我一样,日常在Linux桌面上靠fcitx5输入法过日子,那大概率早晚会走到“看默认皮肤不顺眼”这一步。默认的白底黑字候选框说不上哪里不好,但就是和精心调过的桌面壁纸、GTK主题放在一起时,怎么看怎么扎眼。今天这篇就专门聊…

2026/10/3 3:15:11 阅读更多 →
fcitx5皮肤定制全攻略:从原理到Rime搭配实战

fcitx5皮肤定制全攻略:从原理到Rime搭配实战

折腾Linux桌面这些年,输入法换来换去,最后在fcitx5上彻底安家。倒不是因为它打字多快,而是这套框架的皮肤系统实在太好用了:候选框、状态条、托盘图标全都能按自己喜好重画,装完一套顺手主题,整个桌面的完成…

2026/10/3 3:15:11 阅读更多 →

最新新闻

Java 8 DoubleSummaryStatistics 实现员工工资统计与最高工资人数

Java 8 DoubleSummaryStatistics 实现员工工资统计与最高工资人数

后端开发里,统计员工工资这种需求太常见了——人数、工资总和、平均工资、最高、最低,挨个算。Java 8的DoubleSummaryStatistics就是专门为这类"汇总统计"准备的工具类,配合Stream一行就能把五个核心指标同时算出来。但实际接需求时…

2026/10/3 4:04:56 阅读更多 →
SpringBoot+Vue儿童英语娱教系统开发全记录

SpringBoot+Vue儿童英语娱教系统开发全记录

最近一直在忙活一个毕设级的项目——基于SpringBoot和Vue的儿童英语娱教软件。说实话,做之前我觉得这类“少儿教育系统”无非就是CRUD加几个页面,真正上手才发现,儿童用户和成人用户的设计逻辑完全是两码事。这个项目收到的一些反馈也很有意思…

2026/10/3 4:04:56 阅读更多 →
动力电池SOH与RUL深度学习预测系统:LSTM+Attention实战部署

动力电池SOH与RUL深度学习预测系统:LSTM+Attention实战部署

简介:本资源是一套基于Python实现的动力电池健康状态(SOH)评估与剩余寿命(RUL)预测系统,面向计算机、人工智能、自动化及电子工程等专业的学生、教师与研发人员,适用于课程实践、毕业设计与学术…

2026/10/3 4:04:56 阅读更多 →
Redis密码设置与验证:从requirepass到ACL的完整安全链路

Redis密码设置与验证:从requirepass到ACL的完整安全链路

“Redis 密码设置和验证技巧”在面试题里通常被归成基础题,但我做了这么多年后端和中间件相关的技术面试,发现十个候选人里至少有四五个会在这道题上翻车。不是他们不知道requirepass,而是不知道密码配完以后会发生什么:连主从复制…

2026/10/3 4:04:56 阅读更多 →
ACM寒假训练第一周指南:算法基础、STL与训练赛复盘

ACM寒假训练第一周指南:算法基础、STL与训练赛复盘

每年1月初,SMU机房里的键盘声会准时变得密集起来。作为带过两年寒假训练的“老学长”,我今年又回到训练营帮新队员做入门辅导。说句实话,第一周往往是整个训练营信息密度最高、也最容易劝退人的一周——不是因为题难,而是很多同学…

2026/10/3 4:04:56 阅读更多 →
Python 在 Linux 上连接 CNKI KBase 数据库:从驱动选型到连接池封装

Python 在 Linux 上连接 CNKI KBase 数据库:从驱动选型到连接池封装

简介:这份资源是面向Linux平台科研人员与学术数据检索开发者的CNKI KBase数据库连接包设计源码,基于Python语言实现,旨在解决Linux环境下访问中国知网KBase数据库、进行文献检索与数据处理时缺乏专用工具的问题。压缩包共50个文件&#xff0c…

2026/10/3 4:03:56 阅读更多 →

日新闻

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南 【免费下载链接】ex-skill 前任 skill 项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill 前任.skill 是一个运行在 Claude Code 上的开源 Skill:导入微信、iMessage、短信、…

2026/10/3 0:00:27 阅读更多 →
45个经典Linux面试题:从命令到网络排障的完整考点解析

45个经典Linux面试题:从命令到网络排障的完整考点解析

刚开始带应届生的时候,我最头疼的就是他们拿着一摞Linux面试题背得滚瓜烂熟,一上机全露馅。后来自己从被面的人变成面别人的人,才慢慢摸清楚:Linux面试题考的根本不是答案本身,而是你面对一个不确定的系统问题时&#…

2026/10/3 0:01:28 阅读更多 →
SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

简介:本资源是一份面向SAP ABAP开发人员、生产计划专员及ERP实施顾问的实操型操作指南,聚焦SAP生产预留核心业务场景,系统解决物料预留创建、查询、校验与批量处理等高频问题。文档以结构化方式覆盖预留背景原理、OMC2编码规则、工厂级参数配…

2026/10/3 0:01:28 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/1 19:40:48 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/1 19:41:40 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/10/1 20:05:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 5:26:06 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 6:09:11 阅读更多 →