8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

PHP中foreach循环详解

IT171中文网 管理员组 2013-7-17 525

首先要说的是,其实我对foreach循环的用法并不是很精通,说详解,其实也只是我自己的理解,希望对你能有点帮助 。
先来看一下foreach的语法:
foreach ($array as $key=>$value)
{
……
}
为了便于理解,我们假定这里的$array是一个一维的相关数组,$key是数组的索引,$value是这个索引的值,它们的名字可以随意,之所以叫$key和$value是为了便于理解。为了能让你更好的理解foreach的工作过程,我们来创建一个数组:
$array = array('first'=>'ibm','second','hp');
现在我们模拟PHP服务用foreach对$array进行遍历:
foreach ($array as $key=>$value)
{
echo $key.'=>'$value;
}
第一次循环,$key = 'first',$value = 'ibm',这个时候,实际foreach对$array执行了一个我们看不见的操作:next($array),将数组指针向前(这里的“向前”跟PHP手册相同,不是我们通常所理解的“向前”)移一。然后echo语句输出"first=>ibm"。
第二次循环,首先会判断数组$array的当前指针是否已经到了数组的末尾,如果是,结束循环,否则进入第二次循环。当然这样的判断在进入第一次循环的时候也会有,如果$array是一个空数组,那么就会直接执行循环下面的语句。此时,$key = 'second','value' = 'hp',next($array)后,输出"second=>hp"。然后进行判断,数组指针已经到达末尾,执行下面的语句。
到这里你应该对foreach循环有所了解了吧?还有一点需要的是,foreach每次只是传递一个值,而不是真的对数组元素进行操作。具体到上面的例子,如果你想在每个数组元素的后面加上 'company' 这个字符串,那么$value .= 'company'这样的操作是不行的,它并不会改变数组元素的值,这个时候你应该这样用:$array[$key] .= 'company';





-------------------------------------------------------------------------


PHP foreach循环中出现的奇怪现象及其原因分析概述:主要介绍了foreach循环语句内,某些操作导致的一些不寻常的现象,以及猜想和分析这究竟是什么原因导致的。
关键词: PHP foreach 写时复制 Copy On Write 数组的内部指针
1)现象,foreach与current的奇怪互动
有下面这段代码,其每次循环结束之后打印的current($items)各不相同,这是很奇怪的现象。从表象上分析,在foreach循环过程中,中途调用了current函数,则$items的内部指针不再受foreach的循环影响。

$items = array( 'hello', 23, 45, 'how', 'hhhh' );
$i=0;
foreach ($items as $item) {

$i++;

}
var_dump(current($items));
//bool(false)
//循环过程中什么也没做,打印false,意味着内部指针指向了一个不存在的位置,这里应该是数组的末尾
$i=0;
foreach ($items as $item) {
if($i==0){ current($items); }
$i++;

}
var_dump(current($items));
//int(23)
//在第一次循环过程中调用current函数,打印了第二个元素。
$i=0;
foreach ($items as $item) {
if($i==2){ current($items); }
$i++;

}
var_dump(current($items));
//string(3) "how"
//在第三次循环过程中调用current函数,打印了第四个元素。
?>
2)进展,究竟是什么影响了数组的内部游标
把上面的代码中,foreach循环体中的current($items) 改成$items[0] = 'x'; 输出内容与上面的代码相同,没有变化。

bool(false)
int(23)
string(3) "how"

而把$items[0] = 'x';改成 var_dump($items[0]),输出的内容发生了变化。

bool(false)
bool(false)
bool(false)

从这点上来看,对数组的写操作,会对$items的内部指针产生影响,而读操作,则没有影响——这里,current函数被认为是一个写操作。

3)另外一个现象,内存占用情况的变化。
下面的代码考察了内存占用情况,可以发现,当foreach中有数组的写操作时,内存占用会上升,说明了此时有较大规模的内存拷贝发生。

$items = array( );
for($i=0;$i<100000;$i++){
$items[] = $i;
}
$i=0;
foreach ($items as $item) {
if($i ==90000){
var_dump(memory_get_usage());
//输出 int(8045308) ,循环过程中什么也没做
}
}
var_dump(memory_get_usage());
//输出 int(8045328)
$i=0;
foreach ($items as $item) {
if($i==0){ current($items); }
$i++;
if($i ==90000){
var_dump(memory_get_usage());
//输出 int(12969700) ,循环过程中调用了current方法,内存增大了 50%
}
}
var_dump(memory_get_usage());

$i=0;
foreach ($items as $item) {
if($i==2){ $items[0] = 'x'; }
$i++;

if($i ==90000){
var_dump(memory_get_usage());
//输出 int(12969744) ,循环过程中修改了某个元素的值,内存增大了50%
}
}
var_dump(memory_get_usage());
//输出 int(8045344) ,循环结束的时候,内存恢复了循环之前的值。
?>

4)瞎懵一般的分析,不着调的分析,黑盒分析
很遗憾的是,在下愚钝,仔细阅读php的源代码多遍,也没有发现php引擎是如何把current操作认为是一个写操作的;也没有搞明白,php引擎是如何判断循环体中是否有写操作的;看来要好好学习,继续努力。
不过幸运的是,我们从php的手册中看到了下面这段说明:

注:当foreach开始执行时,数组内部的指针会自动指向第一个单元。这意味着不需要在foreach循环之前调用 reset() 。

注:除非数组是被引用,foreach所操作的是指定数组的一个拷贝,而不是该数组本身。因此数组指针不会被each()结构改变,对返回的数组单元的修改也不会影响原数组。不过原数组的内部指针的确在处理数组的过程中向前移动了。假定 foreach 循环运行到结束,原数组的内部指针将指向数组的结尾。

根据这段描述, 和前面的几段测试代码,我们可以猜测,php引擎在foreach循环中,首先在原数组上使用内部指针循环;在循环过程中,如果有写操作,则对原数组做一个拷贝,在拷贝数组上继续进行循环操作。这里拷贝的可能并不是整个数组。
这样要达到的目的是,foreach循环和each、next、current的内部指针操作互不干扰,同时又尽可能得减少数组拷贝操作。
但是,foreach循环为什么要与each、next、current操作隔离?貌似没有必要。
5)自嘲
哎。还是继续去阅读php源代码吧,如果把源代码搞透了,何必猜来猜去呢?等我研究透了,这篇文章才像个样子,而不是现在这种鬼样子:)
6) 学习的成果
有个悲喜交加的消息,最近的学习有了成果,了解了PHP引擎中一个叫做写时复制(Copy On Write,COW)的特性。简单的说,该特性是指,复制表量的时候,并不是真正立刻创建了一快新的内存,并把原变量的数据拷贝过来;而是先创建一个到原变量的引用,只要新变量或者原变量发生写入操作时,才真正去申请内存拷贝变量。这样的好处很明显,就是参数传值和大数组复制的时候,可能会省却很多无谓的内存申请、复制、销毁的工作,能提高PHP引擎的效率。
在上面的例子中,current函数和$items[0] = 'x'赋值操作后,能明显的看到Copy On Write的影子。之前,所有的操作都是在原数组上进行的,所以foreach循环改变了$items的内部指针;然后,current函数和赋值操作引发COW,foreach在新数组上进行,后继的current在原数组上进行,两者分离,不再干涉。因为数组的复制实际的发生了,所以使用的内存也就增多了。
这种解释在一定程序上能说得通上面的问题;不过,有两个问题可能不好解释,一个问题是,current是标准的读操作,为什么会触发COW?另外一个问题是,为什么发生了数组的复制,内存的增长不是之前的倍数,却仅仅是原来内存的一半左右?
7)内存增长的解释
对于数组复制来说,其真正复制的时候,会复制HashTable本身,包括HashTable中各个Bucket;每个Buckets内的对象,$items[12] = 26; 其中26是Buckets->pData指向的zval对象,这里也是Copy On Write的。所以,在上面的情况,只复制了数组的Key,没有复制数组的值,所以内存占用要小一些。不过,只要修改下程序,让数组的每一个值都触发COW,使用的内存就会上升。

foreach ($items as $item) {
//修改数组的每一个值,触发COW。

$items[$i] = $i+1;

$i++;

if($i ==90000){
var_dump(memory_get_usage());
//这里输出的值是15489692,比之前的12969744大了很多,接近8045328的2倍了。
}
}

上面的代码片断很清楚的说明了这一点。
8)current函数触发Copy On Write的解释
要想了解current这样一个标准的只读函数为什么会触发Copy On Write,就需要去php引擎的源代码中去查找原因了。
这里我就把追查的步骤略过,看下面current函数的变量声明:

ZEND_BEGIN_ARG_INFO(arginfo_current, ZEND_SEND_PREFER_REF)
ZEND_ARG_INFO(ZEND_SEND_PREFER_REF, arg)
ZEND_END_ARG_INFO()
从这段代码中可以看到,这里使用了ZEND_SEND_PREFER_REF,这个参数会告知引擎进行真实的拷贝操作,所以就触发了COW。
9)foreach过程中对引用和内部指针的处理
看下面这段代码,注意循环过程中声明的是引用
foreach ($items as &$item) {
$ret = next($items);

if($i ==90000){
var_dump($ret);
//这里返回的结果是90001,说明next函数确实修改了HashTable的内部指针

var_dump(memory_get_usage());
//这里输出的值是8041804,与之前相差不大,说明没有进行数组拷贝,这与前文中提到的php文档中的说明部分相符:如果是
//引用,则在原数组上进行循环,否则拷贝一个数组,在拷贝数组中循环

}
}
上面这段代码说明了两个问题,一是,如果foreach中使用了引用,则循环操作直接在原数组上进行,不再进行Copy On Write,所以没有内存的大幅变化。二是foreach循环过程中,next等函数对HashTable内部指针的操作是临时性的,foreach循环中,下一次循环会将内部指针重置回上一次循环结束后的位置。
PHP引擎中,获取foreach循环的key/value数据的opcode是ZEND_FE_FETCH,此opcode的处理函数是ZEND_FE_FETCH_SPEC_VAR_HANDLER,该函数中有如下代码,可以看到循环内部确实保存了HashTable的内部指针,并且每次获取Key/Value对之前,都要将内部指针恢复,避免受next,prev,end的函数的影响:
ZEND_FE_FETCH_SPEC_VAR_HANDLE:
fe_ht = HASH_OF(array);
//从foreach相关结构体中拿到本次循环需要的内部指针,并设置给HashTable
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
//获取该内部指针指向的元素的value
if (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {
/* reached end of iteration */
ZEND_VM_JMP(EX(op_array)->opcodes+opline->op2.u.opline_num);
}
//获取该内部指针指向的元素得key
if (use_key) {
key_type = zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len,
&int_key, 1, NULL);
}
//内部指针后移一位,产生新的内部指针
zend_hash_move_forward(fe_ht);
//把新的内部指针保存在foreach的相关结构中。
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos);
//然后就是循坏内部对key和value的各种操作,处理。这里面就算对内部指针有改动也会被
zend_hash_set_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos)给恢复回去的。
10)结论
正如php文档中中所说,foreach($items as $item) 与foreach($items as & $item)的不同之处在于,前者会对数组进行拷贝操作,后者不会。因为php中写时复制策略(Copy On Write, COW)的存在,数组拷贝会延迟到对$items有改动的时候才会真实地发生,否则只是增加了一个对$items的引用。在两种foreach循环内部,next、current、prev、end、reset等函数的行为,其结果与影响并不相同,可以认为其行为是不可预期的,在后续的php版本中,很可能发生变化。
下面是针对上面问题的两个建议:
a)foreach循环内部禁用next、current、prev、end、reset五个array内部指针操作函数。
b)在foreach内部可能有对$items进行的写操作时,推荐使用foreach($items as & $item),避免大数组拷贝。
感谢newsmth.org的myhere2011、Orpherus、cadiny等朋友讨论,看了他们的讨论后我才去琢磨这个问题。




-------------------------------------------------------




PHP经常需要循环显示大量内容,尤其是在进行数组循环显示操作时,然而php循环函数主要有两种方式,一种是foreach,另一种是while,到底哪种性能好呢,为什么要有两个循环函数呢?,然后我就到网上找了些关于这些的资料,下面有总结:
在循环里进行的是数组“读”操作,则foreach比while快:

foreach ($array as $value) {
echo $value;
}
while (list($key) = each($array)) {
echo $array[$key];
}
foreach ($array as $value) {
echo $value;
}
while (list($key) = each($array)) {
echo $array[$key];
}
在循环里进行的是数组“写”操作,则while比foreach快:

foreach ($array as $key => $value) {
echo $array[$key] = $value . '...';
}
while (list($key) = each($array)) {
$array[$key] = $array[$key] . '...';
}
foreach ($array as $key => $value) {
echo $array[$key] = $value . '...';
}
while (list($key) = each($array)) {
$array[$key] = $array[$key] . '...';
}
总结:通常认为,foreach涉及到值复制,一定会比while慢,但实际上,如果仅仅是在循环里进行数组的读操作,那么foreach是很快的,这是因为PHP采用的复制机制是“引用复制,写时拷贝”,这样看来,foreach的高效读操作就不难理解了。
另外,既然foreach不适合处理数组写操作,那么我们可以得出一个结论,多数情况下,类似foreach ($array as $key => $value)形式的代码都应该被替换成while (list($key) = each($array))。
这些技巧产生的速度差异在小项目里可能并不明显,但是在类似框架这样的大项目中,一次请求动辄便会涉及到几百几千几万次数组循环操作,差异就会明显放大。



-------------------------------------------------------------



来源:网络转载[hr]
最新回复 (8)
全部楼主
返回
发新帖
我也是有底线哒~