深圳圆心GEO

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就想在每段循环里秀一把。

工具没有高低,只有合不合适。把这三把刀都摸一遍,下次写代码时手会自己做出选择——那时候,你就真的入门了。

最后留个小练习:写一个函数,接收一个文件路径,用生成器逐行读取并统计每行出现的单词数,要求不论文件多大,内存占用都保持稳定。写出来,说明迭代器这一课你过了。

php基础语法