PHP数组遍历方法对比:foreach、for与迭代器使用场景
PHP数组遍历方法对比:foreach、for与迭代器使用场景 先说个扎心的事实:你写PHP,十有八九有一半时间在跟数组打交道。而跟数组打交道,一半时间又在 遍历 它。 所以问题来了——PHP给了你三把刀: foreach 、 for 、迭代器。很多新手学完就只用 foreach 一把砍到底,能跑就行。但真到了某些场景…
PHP数组遍历方法对比:foreach、for与迭代器使用场景
先说个扎心的事实:你写PHP,十有八九有一半时间在跟数组打交道。而跟数组打交道,一半时间又在遍历它。
所以问题来了——PHP给了你三把刀:foreach、for、迭代器。很多新手学完就只用foreach一把砍到底,能跑就行。但真到了某些场景,你会发现“能跑”和“跑得漂亮”是两码事。
今天咱们就把这三把刀摆桌上,一个一个看清楚:谁适合切菜,谁适合剁骨头,谁适合表演花式飞刀。
一、先搞明白:遍历到底在干嘛
数组就像一个快递柜,每个格子里塞了点东西。
- 有的柜子是连号的:1号、2号、3号……你按号取就行。
- 有的柜子是乱号的:
"name"号、"age"号、"city"号……你压根不知道下一个是几号。
遍历就是:把柜子从第一个走到最后一个,每打开一个格子,把里面的东西拿出来用一下。
三种方法的区别,本质上是“你怎么找下一个柜子”的区别。
$user = [
'name' => '张三',
'age' => 18,
'city' => '杭州',
];
上面这就是个乱号柜子(关联数组)。下面正式开始。
二、foreach:PHP的亲儿子,闭眼用不亏
1. 基本用法
$fruits = ['苹果', '香蕉', '西瓜'];
foreach ($fruits as $fruit) {
echo $fruit . PHP_EOL;
}
就这四行,完事。不用管数组长度,不用管下标是不是连续的,不用管是关联数组还是索引数组——foreach全给你兜住。
这是新手最该先掌握的写法,没有之一。
2. 想同时拿键?加个箭头
$user = ['name' => '张三', 'age' => 18];
foreach ($user as $key => $value) {
echo "{$key} 的值是 {$value}" . PHP_EOL;
}
输出:
name 的值是 张三
age 的值是 18
$key => $value 这个写法建议刻进DNA里,后面写业务代码天天用。
3. 想在遍历时改数组?用引用
这是新手第一个大坑。看这段:
$prices =;
foreach ($prices as $price) {
$price = $price * 2; // 注意:这里改的是副本!
}
print_r($prices); // 还是
为什么没变?因为foreach默认把每个元素复制一份给你玩,你改的是复制品,原数组纹丝不动。
想真改,加个 &:
$prices =;
foreach ($prices as &$price) {
$price = $price * 2;
}
unset($price); // 这行一定要写!
print_r($prices); //
4. 那个必须写的 unset
重点说下上面那行 unset($price);。
用引用遍历完之后,$price 这个变量还指向数组的最后一个元素。如果你后面不小心又给 $price 赋了个值,数组最后一个元素就被你莫名其妙改掉了:
$nums =;
foreach ($nums as &$n) {}
// 忘了 unset
$n = 999;
print_r($nums); // —— 啊?
这种bug极其隐蔽,找一下午都找不到。记住口诀:引用遍历完,立刻 unset。
5. foreach 的小脾气
foreach 遍历的是数组的一个“快照式”视图(准确说是内部指针的副本)。所以:
$arr =;
foreach ($arr as $v) {
if ($v === 1) {
$arr[] = 4; // 往原数组追加
}
echo $v . ' ';
}
// 输出:1 2 3 —— 新加的 4 不会被这次遍历到
这个行为其实挺贴心的:不会因为你边遍历边加元素就陷入死循环。
6. foreach 也能遍历对象
class Point {
public $x = 1;
public $y = 2;
protected $z = 3;
private $w = 4;
}
$p = new Point();
foreach ($p as $key => $value) {
echo "$key = $value" . PHP_EOL;
}
// 只输出 x 和 y(protected/private 在类外拿不到)
这个特性在调试、或者做简单的对象转数组时挺方便。
小结: foreach 是万金油,90%的场景用它就够了。唯一要小心的是引用遍历记得 unset。
三、for:老派硬汉,专治“我要下标”
for 是C语言传下来的老古董,PHP里当然也有:
$fruits = ['苹果', '香蕉', '西瓜'];
for ($i = 0; $i < count($fruits); $i++) {
echo $fruits[$i] . PHP_EOL;
}
看起来比foreach啰嗦,那它到底什么时候有用?
场景1:你需要下标本身
比如“第几个”这类需求:
$students = ['小明', '小红', '小刚'];
for ($i = 0; $i < count($students); $i++) {
echo "第 " . ($i + 1) . " 位同学:" . $students[$i] . PHP_EOL;
}
当然foreach也能做到,加个计数器就行,但for写起来更直白。
场景2:倒着遍历 / 跳着走
$nums =;
// 倒序
for ($i = count($nums) - 1; $i >= 0; $i--) {
echo $nums[$i] . ' ';
}
// 6 5 4 3 2 1
// 每次跳两个
for ($i = 0; $i < count($nums); $i += 2) {
echo $nums[$i] . ' ';
}
// 1 3 5
这种“我要自己控制步伐”的需求,for 是最顺手的。
场景3:固定次数的循环
有时候你压根不是在遍历数组,就是想跑固定次数:
for ($i = 0; $i < 5; $i++) {
echo "打卡第 " . ($i + 1) . " 天" . PHP_EOL;
}
这种时候别硬套foreach,for才是本命。
大坑警告:for 只认连续下标
这是新手最容易翻车的地方。
$user = ['name' => '张三', 'age' => 18];
for ($i = 0; $i < count($user); $i++) {
echo $user[$i]; // 报错:Undefined array key 0
}
为啥?因为$user根本没有 0、1 这两个键!它有的是 'name' 和 'age'。
for 只能用在下标从 0 开始、连续递增的索引数组上。
更阴险的是这种情况:
$nums =;
unset($nums); // 删掉中间的
print_r($nums); // [0 => 10, 2 => 30]
for ($i = 0; $i < count($nums); $i++) {
echo $nums[$i]; // $i=1 时会报错,因为1号键没了
}
count($nums) 是 2,但键是 0 和 2。循环到 $i=1 就炸了。
所以:拿不准数组结构时,别用 for,用 foreach。 这句请写便利贴上。
小优化:count 提到循环外
$count = count($nums);
for ($i = 0; $i < $count; $i++) {
// ...
}
虽然在现代PHP里count()对数组是O(1)操作,性能上差别不大,但把长度缓存下来是个好习惯,代码也更清晰——尤其是循环体很复杂的时候,读者一眼就知道长度不会变。
四、迭代器:高级玩家的瑞士军刀
先说结论:新手阶段,你几乎不需要手写迭代器。 但你得知道它存在,知道它解决什么问题。
1. 它是来解决什么问题的?
想象你要处理一个10GB的日志文件,每行一条记录。
用file()一把读进来?内存直接爆了。
这时候就需要“迭代器”的思路:不要一次性把所有东西都拿到手,而是一个一个地要。 这叫“惰性求值”或者“按需拉取”。
2. SPL 里现成的:ArrayIterator
PHP标准库(SPL)自带一批迭代器,ArrayIterator 是最常用的一个:
$arr = new ArrayIterator(['a', 'b', 'c']);
foreach ($arr as $key => $value) {
echo "$key => $value" . PHP_EOL;
}
你可能会说:这不跟foreach数组一模一样吗?
对,foreach的语法是通用的——只要能迭代的东西,foreach都能遍历。区别在于ArrayIterator对象额外带了很多方法:
$it = new ArrayIterator;
echo $it->count(); // 5,元素个数
$it->seek(2); // 跳到第3个
echo $it->current(); // 30,当前值
$it->asort(); // 就地排序
seek() 这种“我想直接跳到第N个”的能力,普通数组是做不到的(普通数组你只能$arr,但你不能在“遍历过程”中随意跳)。
3. Generator:写起来最爽的迭代器
手写Iterator接口要实现5个方法(current、key、next、rewind、valid),啰嗦得要命。PHP 5.5 给了个语法糖:yield。
function readLines(string $file) {
$handle = fopen($file, 'r');
if (!$handle) {
return;
}
while (($line = fgets($handle)) !== false) {
yield trim($line);
}
fclose($handle);
}
// 用起来就像普通数组一样
foreach (readLines('/path/to/big.log') as $line) {
echo $line . PHP_EOL;
}
看到没?函数里用了yield,它就自动变成一个迭代器。
关键点:整个过程中,内存里只保留当前这一行。 文件多大都不怕。这就是迭代器最大的价值。
4. 无限序列
迭代器还能表达“无限”这种东西——数组永远做不到:
function fibonacci() {
$a = 0;
$b = 1;
while (true) {
yield $a;
[$a, $b] = [$b, $a + $b];
}
}
$count = 0;
foreach (fibonacci() as $n) {
echo $n . ' ';
if (++$count >= 10) {
break; // 你要几个拿几个
}
}
// 0 1 1 2 3 5 8 13 21 34
数组必须有尽头,迭代器可以没有。这是维度上的差异。
5. 自定义迭代器让对象“可遍历”
如果你的类实现了Iterator或IteratorAggregate,这个对象就能直接被foreach遍历:
class Team implements IteratorAggregate {
private array $members = [];
public function add(string $name): void {
$this->members[] = $name;
}
public function getIterator(): Iterator {
return new ArrayIterator($this->members);
}
}
$team = new Team();
$team->add('小明');
$team->add('小红');
foreach ($team as $name) {
echo $name . PHP_EOL;
}
这样你的对象用起来就跟数组一样自然,内部结构却可以随便藏。这在写类库、写SDK时非常常见。
五、三者对比:一张表看清楚
| 对比项 | foreach |
for |
迭代器(Iterator / Generator) |
|---|---|---|---|
| 上手难度 | 极低 | 低 | 中到高 |
| 支持关联数组 | 原生支持 | 不支持(会炸) | 支持 |
| 支持键名 | 支持($k => $v) |
需自己维护 | 支持 |
| 需要下标 | 需额外计数器 | 天然有 | 视实现 |
| 倒序 / 跳步 | 麻烦 | 非常方便 | 需自己实现 |
| 大数据内存友好 | 一般(数组本身还是全量加载) | 一般 | 非常好(惰性) |
| 能否表达无限序列 | 不能 | 不能 | 能 |
| 能否边遍历边安全修改 | 引用方式可行(记得 unset) | 容易出下标错乱 | 视实现 |
| 典型场景 | 业务代码90%的遍历 | 索引数组、固定次数、倒序 | 大文件、数据库游标、惰性计算 |
六、到底怎么选?给你一份决策清单
不用记理论,遇到场景对号入座就行:
选 foreach,如果:
- 数组是关联数组(有字符串键)
- 你只想拿到每个值,或者键值都要
- 数组长度不固定,或者下标不连续
- 你在写日常业务逻辑(这几乎是默认答案)
选 for,如果:
- 明确是索引数组,键从0连续排
- 你需要下标参与计算(比如“每隔3个取一个”)
- 你要倒序遍历
- 其实你只是想让某段代码跑N次,跟数组没半毛钱关系
选迭代器,如果:
- 数据量大到不能一次性读进内存(大文件、大结果集)
- 数据源是流式的(数据库游标、网络流、生成器算法)
- 你想让自定义对象支持
foreach - 你需要“要多少拿多少”的惰性行为
一句话总结:能用 foreach 就用 foreach;需要下标和步长就换 for;数据大到装不下,再请迭代器出山。
七、几个必须记住的坑
坑1:foreach 引用后忘记 unset
已经讲过,再说一次,因为它真的太容易犯了。改完数组立刻 unset($value);。
坑2:for 用在关联数组或“有洞”的索引数组上
Undefined array key 十有八九是这个原因。拿不准就foreach。
坑3:在 foreach 里删元素
$nums =;
foreach ($nums as $k => $v) {
if ($v % 2 === 0) {
unset($nums[$k]); // 这个是可以的,因为遍历的是副本
}
}
print_r($nums); // [0 => 1, 2 => 3, 4 => 5]
这个能跑,但要注意:删完之后键不连续了。如果后面还要用for或者json_encode成数组(而不是对象),记得array_values()重排一下:
$nums = array_values($nums);
坑4:count() 放在 for 条件里
for ($i = 0; $i < count($arr); $i++) // 每次循环都调用一次
虽然对数组是O(1),但养成缓存到变量的习惯没坏处,尤其在循环体复杂、或者未来$arr变成Countable对象时(那时count()可能是O(n)),你就庆幸自己写对了。
坑5:以为迭代器一定比数组快
不是的。迭代器的优势是省内存,不是省CPU。小数组用迭代器反而多了一层对象开销,纯属自找麻烦。用对场景,别为了高级而高级。
八、收个尾
数组遍历是PHP基础语法里最朴素、也最容易被忽视的一块。
foreach是你的主力,写业务代码时它几乎永远是对的答案;for是你的备用工具,处理索引、步长、倒序时比foreach顺手;- 迭代器是你的进阶装备,数据大到内存扛不住的时候,它会救你一命。
新手最容易犯的错,不是不会用迭代器,而是只会用foreach却硬要拿它去解决所有问题,或者反过来,刚学会yield就想在每段循环里秀一把。
工具没有高低,只有合不合适。把这三把刀都摸一遍,下次写代码时手会自己做出选择——那时候,你就真的入门了。
最后留个小练习:写一个函数,接收一个文件路径,用生成器逐行读取并统计每行出现的单词数,要求不论文件多大,内存占用都保持稳定。写出来,说明迭代器这一课你过了。