ホームページ  >  記事  >  バックエンド開発  >  php_PHP チュートリアルでの foreach の問題の詳細な分析

php_PHP チュートリアルでの foreach の問題の詳細な分析

WBOY
WBOYオリジナル
2016-07-21 15:01:34778ブラウズ

前書き:
foreach 構造は php4 で導入され、配列を走査する簡単な方法です。従来の for ループと比較して、foreach はキーと値のペアをより簡単に取得できます。 php5 より前は、foreach は配列にのみ使用できましたが、php5 以降は、オブジェクトの走査にも使用できるようになりました (詳細については、「オブジェクトの走査」を参照)。この記事では、配列のトラバーサルについてのみ説明します。

foreach は単純ですが、特にコードに参照が含まれる場合、予期しない動作が発生する可能性があります。
foreach の本質をさらに理解するために、いくつかのケースを以下に示します。
質問 1:

コードをコピーします コードは次のとおりです:

$arr = array(1,2,3);
foreach($arr as $k => &$v ) {
v = $v * 2;
}
// 現在 $arr は array(2, 4, 6)
foreach($arr as $k => $v) {
echo "$k", " => ", "$v";
}

簡単に始めましょう。上記のコードを実行してみると、最終的な出力は 0=>2 1=>4 2=> であることがわかります。 4.
なぜ 0=>2 1=>4 2=>6 ではないのでしょうか?
実際、foreach($arr as $k => $v) 構造は、配列の現在の 'key' と現在の 'value' をそれぞれ変数 $k と $v に割り当てる次の操作を暗示していると考えることができます。具体的な展開は次のとおりです:
コードをコピーします コードは次のとおりです:

foreach($arr as $k => $v){
// 前に暗黙的に 2 つの代入演算があります。ユーザーコードが実行される
$ v = currentVal();
$k = currentKey();
//ユーザーコードを実行し続ける
……
}

上記の理論に従って、最初のコードを再分析しますforeach:
最初のループ、$v は参照なので、$v = &$arr[0]、$v=$v*2 は $arr[0]*2 と同等なので、$arr は 2,2 になります。 3
2 番目のループ、$v = &$arr[1]、$arr は 2,4,3 になります
3 番目のループ、$v = &$arr[2]、$arr は 2,4,6 になります
その後、コードが入力されます 2 番目の foreach:
最初のループ、暗黙的な操作 $v=$arr[0] がトリガーされます。これは、この時点では $v がまだ $arr[2] への参照であるためであり、これは $arr[ と同等です。 2] =$arr[0]、$arr は 2,4,2 になります
2 番目のループ、$v=$arr[1]、つまり、$arr[2]=$arr[1]、$arr は 2 になります, 4,4
3回目のループ、$v=$arr[2]、つまり$arr[2]=$arr[2]、$arrが2,4,4
となりOK、解析は完了です。
同様の問題を解決するにはどうすればよいですか? PHP マニュアルには注意事項があります:
警告: 配列の最後の要素の $value 参照は、foreach ループの後も保持されます。 unset() を使用して破棄することをお勧めします。
コードをコピーします コードは次のとおりです:

$arr = array(1,2,3);
foreach($arr as $k => &$v) {
$v = $v * 2;
}
unset($v);
foreach($arr as $k => $v) {
echo "$k", " => ", "$v";
}
// Output 0=>2 1=>4 2=>6

この質問から、参照には副作用が伴う可能性が高いことがわかります。意図しない変更によって配列の内容が変更されることを避ける場合は、時間内にこれらの参照の設定を解除することをお勧めします。
質問 2:
コードをコピーします コードは次のとおりです:

$arr = array('a','b','c');
foreach($arr as $k => $ v) {
echo key($arr), "=>", current($arr);
}
// print 1=>b 1=>b 1=>b

この問題はもっと奇妙です。マニュアルによると、keyとcurrentは配列内の現在の要素のキーの値です。
それでは、なぜ key($arr) が常に 1 で、current($arr) が常に b なのか?
まず vld を使用して、コンパイルされたオペコードを表示します。

配列 (' を表す 3 行目の ASSIGN 命令から開始します) a','b','c') は $arr に割り当てられます。
$arr は CV、array('a','b','c') は TMP であるため、見つかった ASSIGN 命令によって実際に実行される関数は ZEND_ASSIGN_SPEC_CV_TMP_HANDLER になります。ここで注意しなければならないのは、CV は PHP5.1 以降に追加された変数キャッシュであり、zval** を保存するために配列を使用します。キャッシュされた変数を再度使用する場合は、アクティブなシンボル テーブルを検索する必要はありません。配列からCVを取得するため、配列のアクセス速度がハッシュテーブルよりもはるかに速いため効率が向上します。
コードをコピーします コードは次のとおりです:

static int ZEND_FASTCALL ZEND_ASSIGN_SPEC_CV_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zend_free_op free_op2;
zval *value = _get_zval_ptr_tmp(& opline->op2 、EX(Ts)、&free_op2 TSRMLS_CC);

/ / CV 配列に $arr** ポインターを作成します
zval **variable_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
if (IS_CV == IS_VAR && !variable_ptr_ptr) {

}
else {
--'Assign-array------- value = zend_assign_to_variable(variable_ptr_ptr, value, 1 TSRMLS_CC); AI_SET_PTR(EX_T(opline->result .u.var).var, value);
PZVAL_LOCK(value);
}
}
ZEND_VM_NEXT_OPCODE();
}ASSIGN 命令が完了すると、zval** ポインタが CV 配列に追加され、ポインタは実際の配列を指します。これは、$arr がCVによってキャッシュされます。


次に、配列のループ操作を実行します。それに対応する実行関数は

です。コードをコピーします。

static int ZEND_ファストコールZEND_FE_RESET_SPEC _CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
… RMLS_CC);
… … } ... / // 配列へのポインタを ZEND_EXEC に保存UTE_DATA- & GT; (TSはコードの実行期間のTEMP_VARIABLEで使用されます) AI_SET_PTR (ex_t (OPLINE- & GT; Result.u.var) .var .end_hash_internal_pointer_reset(fe_ht) ; ..var).fe.fe_pos は、配列の内部ポインターを保存するために使用されます
zend_hash_get_pointer(fe_ht, &EX_T(opline- >result.u.var).fe.fe_pos);
zend_execute_data->Ts:

•EX_T(opline->result.u.var).var ---- 配列へのポインタ
•に格納されているポインタEX_T(opline->result.u.var).fe. fe_pos ---- 配列の内部要素へのポインタ

FE_RESET 命令の実行後のメモリ内の実際の状況は次のとおりです。



次に、引き続き FE_FETCH を見ていきます。対応する実行関数は ZEND_FE_FETCH_SPEC_VAR_HANDLER です:



コードをコピーします

コードは次のとおりです:

Tataticint zend_fastcall zend_fe_fetch_spec_var_handler(zend_opcode_handler_args)
{
zend_op *opline = ex(opline); * array = EX_T(opline->op1.u.var).var.ptr; ……

switch (zend_iterator_unwrap(array, &iter TSRMLS_CC)) {
デフォルト:
case ZEND_ITER_INVALID:
……
case ZEND_ITER_PLAIN_OBJECT: { // FE_RESET 命令では、配列の内部要素へのポインタが EX_T(opline->op1.u.var) に保存されます
ここでポインターを取得します
zend_hash_set_pointer(fe_ht, &EX_T( opline->op1.u.var).fe.fe_pos); If (zend_hash_get_current_data(fe_ht, (void **) &value)==FAILURE) {
key_type = zend_hash_get_current_key_ex(fe_ht, &str_key, &str_key_len, &int_key, 1, NULL );
移動されたポインタは EX_T(opline->op1.u .var).fe.fe_pos
zend_hash_get_pointer(fe_ht, &EX_T(opline->op1.u.var).fe.fe_pos に保存されます); ……
}

……
}


FE_FETCH の実装に基づいて、foreach($arr as $k => $v) が何をするのかを大まかに理解しました。 zend_execute_data->Ts のポインタに基づいて配列要素を取得します。取得が成功すると、ポインタを次の位置に移動して再度保存します。




簡単に言えば、最初のループで配列の内部ポインタが FE_FETCH の 2 番目の要素に移動されているため、foreach 内で key($arr) と current($arr) が呼び出されるとき、実際に取得されるのは 1 と ' b'.

では、なぜ1=>bが3回出力されるのでしょうか?

引き続き、9 行目と 13 行目の SEND_REF 命令を見てみましょう。これは、$arr パラメーターをスタックにプッシュすることを意味します。次に、通常は DO_FCALL 命令を使用してキー関数と現在の関数を呼び出します。 PHP はネイティブ マシン コードにコンパイルされないため、PHP はそのようなオペコード命令を使用して、実際の CPU とメモリがどのように動作するかをシミュレートします。

PHP ソース コードの SEND_REF を確認します:




コードをコピーします

コードは次のとおりです:

static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
// CV から $arr ポインタのポインタを取得
varptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX (Ts)、BP_VAR_W TSRMLS_CC);


// 変数の分離。これはキー関数専用の配列の新しいコピーです
SEPARATE_ZVAL_TO_MAKE_IS_REF(varptr_ptr);
varptr = *varptr_ptr;
Z_ADDREF_P(varptr);

// スタックをプッシュします
zend_ vm_stack_push(varptr) ;
ZEND_VM_NEXT_OPCODE();
}

SEPARATE_ZVAL_TO_MAKE_IS_REF はマクロです:
コードをコピーしますコードは次のとおりです:

#define SEPARATE_ZVAL_TO_MAKE_IS _REF(ppzv)
if (!PZVAL_IS_REF( *ppzv)) {
{ SEPARATE_ZVAL(ppzv);主な機能は、変数が参照でない場合にメモリ内に新しいコピーをコピーすることです。この例では、array('a','b','c') をコピーします。したがって、変数分離後のメモリは次のようになります:

変数分離が完了した後、CV 配列内のポインターは新しくコピーされたデータを指し、古いデータは引き続き zend_execute_data->Ts 内のポインターを通じて取得できることに注意してください。
以下のループについては、上の図と組み合わせて個別に説明しません:

•foreach 構造は、以下の青い配列を使用し、a、b、c を順番に走査します
•キーと現在の用途は黄色です。上記の配列では、その内部ポインタは常に b を指します
これで、key と current が常に配列の 2 番目の要素を返す理由がわかりました。コピーされた配列には外部コードが作用しないため、その内部ポインタは決して移動しません。
質問 3:


コードをコピーします
コードは次のとおりです:
$arr = array('a','b','c');foreach($arr as $k => & $v) { echo key($arr), '=>', current($arr);
}// print 1=>b 2=>c =>



この質問と質問 2 の違いは 1 つだけです。この質問の foreach では参照が使用されています。
VLD を使用してこの質問を確認し、質問 2 のコードからコンパイルされたオペコードが同じであることを確認します。したがって、質問 2 の追跡方法を使用して、対応するオペコードの実装を徐々にチェックします。
最初に foreach は FE_RESET を呼び出します:

コードをコピーします
コードは次のとおりです:

static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
……
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
_get_z = val_ptr_ptr_cv(&opline->op1, EX(Ts), TSRMLS_CC);
R _ptr ) == IS_OBJECT) {
/ 配列を走査する場合
使用する 使用する 使用する 使用する 使用する 使用する 使用する 使用する 使用する を通じて を通じて を通じて を通じて を通じて を通じて を通じて を通じて ‐ ‐ ‐ ‐ ‐ に) array_ptr_ptr );

FE_RESET
この例では、foreach が参照を使用して値を取得するため、実行中に FE_RESET は前の質問とは別の分岐に入ります。
最後に、FE_RESET は配列の is_ref を true に設定します。この時点では、メモリ内には配列データのコピーが 1 つだけあります。

次に SEND_REF を分析します:




コードをコピーします

コードは次のとおりです:


static int ZEND_FASTCALL ZEND_SEND_REF_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{

// CVから$arrポインタのポインタを取得する
varptr_ptr = _get_zval_ptr_ptr_cv (&opline->op1, EX(Ts), BP_VAR_W TSRMLS_CC);
……
// 変数の分離、この時点では CV 自体の変数が参照であるため、ここには新しい配列はコピーされません
SEPARATE_ZVAL_TO_MAKE_IS_REF (varptr_ptr); varptr = *varptr_ptr; Z_ADDREF_P(varptr);

// スタックをプッシュします zend_vm_stack_push(varptr TSRMLS_CC);
ZEND_VM_NEXT_OPCODE();
}
マクロ SEPARATE_ZVAL_TO_MAKE_IS_REF は is_ref で変数を区切るだけです=偽。配列は以前に is_ref=true に設定されているため、コピーされません。つまり、この時点ではまだメモリ内に配列データのコピーが 1 つだけ存在します。

上の図は、最初の 2 つのループが 1=>b 2=>C を出力する理由を説明しています。 FE_FETCH の 3 番目のサイクル中に、ポインタを前進させ続けます。



コードをコピーします

コードは次のとおりです:


ZEND_API int zend_hash_move_forward_ex(HashTable *ht, HashPosition *pos)
{
HashPosition *current = pos ? : &ht->pInternalPointer;
IS_CONSISTENT( ht);
if (*現在) {

この時点で内部ポインタはすでに配列の最後の要素を指しているため、先に進むと NULL を指すことになります。内部ポインタを NULL に指定した後、配列の key と current を呼び出すと、それぞれ NULL と false が返され、この時点では文字はエコーされません。
質問 4:
コードをコピーします コードは次のとおりです:

$arr = array(1, 2, 3);
$tmp = $arr;
foreach($tmp as $ k => &$v){
$v *= 2;
}
var_dump($arr, $tmp); // 何を出力するか?

この質問は foreach とはあまり関係ありませんが、foreach に関係するので一緒に議論しましょう:)
コードでは、最初に配列 $arr が作成され、次にその配列が $tmp に割り当てられます。 foreach ループ内で $v を変更すると、配列 $tmp に影響しますが、$arr には影響しません。
なぜですか?
これは、PHP では代入操作によって 1 つの変数の値が別の変数にコピーされるため、一方を変更しても他方には影響しないからです。
余談: これはオブジェクト型には当てはまりません。PHP5 以降、オブジェクトはデフォルトで常に参照によって割り当てられます。 例:
コードをコピーします コードは次のとおりです:

class A{
public $foo = 1;
}
$a1 = $a2 = new A;
$a1->foo=100;
echo $a2->foo; // 出力 100、$a1 および $a2 は実際には同じオブジェクト参照

質問のコードに戻ると、$tmp=$arr が実際には値のコピーであり、$arr 配列全体が $tmp にコピーされることが確認できます。理論的には、代入ステートメントが実行された後、メモリ内に同じ配列のコピーが 2 つ存在します。
配列が大きい場合、この操作は非常に遅くなるのではないかと疑問に思う生徒もいるかもしれません。
幸いなことに、php にはこれを処理するより賢い方法があります。実際、$tmp=$arr が実行された後も、メモリには配列が 1 つだけ存在します。 PHP ソース コード (php5.3.26 か​​ら抽出) の zend_assign_to_variable 実装を表示します:
コードをコピーします コードは次のとおりです:

static inline zval* zend_assign_to_variable(zval **variable_ptr_ptr, zval *value, int is_tmp_var TSRMLS_DC)
{
zval *variable_ptr = *variable_ptr_ptr;
zval Garage;

// 左の値はオブジェクト型
if (Z_TYPE_P (variable_ptr)== is_object && z_obj_handler_p(variable_ptr、set)){

}} else {
// refcount__gc = 1
)&& z_refcount_p(value)>
/ / $ TMP = $ ARR がここで実行されます。
// value は、$ Arr を指す実際の Array データへのポインタです。 Variable_ptr_ptr は、$ TMP のデータ ポインタを指すポインタです
// ポインタをコピーするだけです。配列
* variable_ptr_ptr = 値; variable_ptr_ptr);
}
return *variable_ptr_ptr;
}


$tmp = $arr の本質は配列のポインタをコピーすることであることがわかります。このときのメモリを図で表現すると、配列は 1 つだけです:


配列は 1 つだけなので、foreach で $tmp が変更されると、ループに応じて $arr が変更されないのはなぜですか?
引き続き PHP ソース コードの ZEND_FE_RESET_SPEC_CV_HANDLER 関数を見てください。これは OPCODE HANDLER であり、対応する OPCODE は FE_RESET です。この関数は、foreach が開始される前に、配列の内部ポインタを最初の要素に設定する役割を果たします。



コードをコピーします

コードは次のとおりです:

static int ZEND_FASTCALL ZEND_FE_RESET_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zval *array_ptr, **array_ptr_ptr;
HashTable *fe_ht;
zend_object_iterator *iter = NULL;
zend_class_entry *ce = NULL;
zend_bool is_empty = 0;
// 对变量行FE_RESET
if (opline->extended_value & ZEND_FE_RESET_VARIABLE) {
array_ptr_ptr = _get_zval_ptr_ptr_cv(&opline->op1, EX(T) s)、BP_VAR_R TSRMLS_CC);
if (array_ptr_ptr == NULL || array_ptr_ptr == &EG(uninitialized_zval_ptr)) {
……
}
// foreach一个オブジェクト
else if (Z_TYPE_PP(array_ptr_ptr) == IS_OBJECT) {
……
}
else {
// 本例会进入该分支
if (Z_TYPE_PP(array_ptr_ptr) == IS_ARRAY) {
// 注意此处的SEPARATE_ZVAL_IF_NOT_REF
// 它会重新复制一个数组出来
// 真正分离$tmpand$arr,变成了内存中的2个数组
SEPARATE_ZVAL_IF_NOT_REF(array_ptr_ptr);
if (opline->extended_value & ZEND_FE_FETCH_BYREF) {
Z _SET_ISREF_PP(array_ptr_ptr);
}
}
array_ptr = *array_ptr_ptr;
Z_ADDREF_P(array_ptr);
}
} else {
… …
}

// 重置数組内部指针
……
}

从代码中可以見出,真正行变量分离并不赋值语句行時候,而是了了使用推量時の、天気これも PHP での Copy On Write 機構の実現です。
FE_RESET 後の内部格納の変更は次のようになります。興味深い同学は、ZEND_FE_RESET_SPEC_CV_HANDLER と ZEND_SWITCH_FREE_SPEC_VAR_HANDLER の具体的な動作 (両方とも php-src/zend/zend_vm_execute.h にあります)、本文では説明しません:)



http://www.bkjia.com/PHPjc/327985.html

www.bkjia.com

http://www.bkjia.com/PHPjc/327985.html技術記事前文: php4 では、foreach 構造が導入されており、これは遍歴数の唯一の方法です。foreach の転送と比較して、foreach はより便利な取得値を追加できます。php5 より前では、foreach のみ...
声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。