2012年6月16日土曜日

for文とforeach文の不思議な違い その2

こんにちは、matsuiです。

前回foreachの挙動を改めて確認しました。

確認用のサンプルプログラムを作って、確認した限りでは、

「foreachは配列自体を操作するわけではなく、配列のコピーを作成して処理します。foreachループ内で配列の要素を削除したり挿入したりしても、ループではその要素を反映しません。」

というプログラミングPHP(第2版)の解説を裏付けるような挙動をしていました。

今回のテーマ


今回は「配列のコピーを作成して処理する」という個所にフォーカスをあててみます。

前回はプリミティブ型の配列を用意して、サンプルプログラムとして動作させました。

今回はクラス型の配列で挙動を確認してみたいと思います。

目的は「配列のコピーは、どのようなレベルでコピーされるのか」を確認することです。

クラス型の配列ということはオブジェクトへの参照が含まれることになります。

参照型(オブジェクトへの参照)のコピーを考える際にディープコピー、シャローコピーという問題が出てきます。

foreachが内部で行っている「配列のコピー」はシャロー(浅い)なのでしょうか、ディープ(深い)なのでしょうか。


実験


まずは、配列の内部コピーが行われないfor文を使って配列内部ポインタがどの用に動作しているかを確認します。

for文バージョンサンプルプログラムソース

===========================================
class SampleObject
{
}
$obj1 = new SampleObject();
$obj2 = new SampleObject();
$obj3 = new SampleObject();

// パターン
$arrayList = array(
    $obj1,
    $obj2,
    $obj3,
);

echo "配列の全体を表示" . PHP_EOL;
var_dump($arrayList);
reset($arrayList);
echo PHP_EOL;

for ($i = 0;$i < count($arrayList);$i++) {
   
    echo "----- loop {$i} start ------" . PHP_EOL;
    echo "配列内部ポインタが指す要素の次の要素を取得" . PHP_EOL;
   
    $pointer = next($arrayList);
    var_dump($pointer);
   
    if ($pointer === false) {
        break;
    }
   
    echo "配列内部ポインタを最後にセット" . PHP_EOL;
    var_dump(end($arrayList));
    echo PHP_EOL;
   
}
===========================================

for文バージョン実行結果

===========================================
配列の全体を表示
array(3) {
  [0]=>
  object(SampleObject)#1 (0) {
  }
  [1]=>
  object(SampleObject)#2 (0) {
  }
  [2]=>
  object(SampleObject)#3 (0) {
  }
}

----- loop 0 start ------
配列内部ポインタが指す要素の次の要素を取得
object(SampleObject)#2 (0) {
}
配列内部ポインタを最後にセット
object(SampleObject)#3 (0) {
}

----- loop 1 start ------
配列内部ポインタが指す要素の次の要素を取得
bool(false)
===========================================
関数の説明はphpの公式サイトを見て頂くとして、サンプルプログラムとしては私の意図通り配列内部ポインタを移動させた結果ループ回数が3回から2回に変わっています。



次はforeachバージョンのサンプルプログラムです。
 

foreach文バージョンサンプルプログラム

===========================================
class SampleObject
{
    public $test = null;
}

$obj1 = new SampleObject();
$obj2 = new SampleObject();
$obj3 = new SampleObject();
$obj4 = new SampleObject();

// パターンA

$arrayList = array(
    $obj1,
    $obj2,
    $obj3,
    $obj4,
);

echo "配列の全体を表示" . PHP_EOL;
var_dump($arrayList);

echo "foreach 直前 current" . PHP_EOL;
var_dump(current($arrayList));

echo PHP_EOL;
echo "****** foreach start ******" . PHP_EOL;
echo PHP_EOL;

$count = 0;

foreach ($arrayList as $value) {
    $value->test = "sample string.";
  
    echo "------ loop ${count} start ------" . PHP_EOL;
    echo "foreach 内部直後 current" . PHP_EOL;
    var_dump(current($arrayList));
  
    echo "foreach 内部直後 next" . PHP_EOL;
    var_dump(next($arrayList));
    echo PHP_EOL;
  
    echo "foreach で処理している対象の配列の要素ポインタを配列の最後に動かす." . PHP_EOL;
    var_dump(end($arrayList));
    $count++;
    echo PHP_EOL;
}

echo "****** foreach end ******" . PHP_EOL;
echo PHP_EOL;
var_dump($arrayList);
===========================================

状況がわかリ易いように要素数とvar_dumpによる出力を増やしましたが、
基本的にはfor文バージョンと同じように、配列内部ポインタを制御し、loop回数が本来よりも少なくなるようにしました。
合わせて配列内のオブジェクトにも変更を加え、foreachによるloop処理が終わった後にどうなっているかを確認します。

それでは実行結果を見てみましょう。

foreach文バージョン実行結果

===========================================
配列の全体を表示
array(4) {
  [0]=>
  object(SampleObject)#1 (1) {
    ["test"]=>
    NULL
  }
  [1]=>
  object(SampleObject)#2 (1) {
    ["test"]=>
    NULL
  }
  [2]=>
  object(SampleObject)#3 (1) {
    ["test"]=>
    NULL
  }
  [3]=>
  object(SampleObject)#4 (1) {
    ["test"]=>
    NULL
  }
}
foreach 直前 current
object(SampleObject)#1 (1) {
  ["test"]=>
  NULL
}

****** foreach start ******

------ loop 0 start ------
foreach 内部直後 current
object(SampleObject)#2 (1) {
  ["test"]=>
  NULL
}
foreach 内部直後 next
object(SampleObject)#3 (1) {
  ["test"]=>
  NULL
}

foreach で処理している対象の配列の要素ポインタを配列の最後に動かす.
object(SampleObject)#4 (1) {
  ["test"]=>
  NULL
}

------ loop 1 start ------
foreach 内部直後 current
object(SampleObject)#4 (1) {
  ["test"]=>
  NULL
}
foreach 内部直後 next
bool(false)

foreach で処理している対象の配列の要素ポインタを配列の最後に動かす.
object(SampleObject)#4 (1) {
  ["test"]=>
  NULL
}

------ loop 2 start ------
foreach 内部直後 current
object(SampleObject)#4 (1) {
  ["test"]=>
  NULL
}
foreach 内部直後 next
bool(false)

foreach で処理している対象の配列の要素ポインタを配列の最後に動かす.
object(SampleObject)#4 (1) {
  ["test"]=>
  NULL
}

------ loop 3 start ------
foreach 内部直後 current
object(SampleObject)#4 (1) {
  ["test"]=>
  string(14) "sample string."
}
foreach 内部直後 next
bool(false)

foreach で処理している対象の配列の要素ポインタを配列の最後に動かす.
object(SampleObject)#4 (1) {
  ["test"]=>
  string(14) "sample string."
}

****** foreach end ******

array(4) {
  [0]=>
  object(SampleObject)#1 (1) {
    ["test"]=>
    string(14) "sample string."
  }
  [1]=>
  object(SampleObject)#2 (1) {
    ["test"]=>
    string(14) "sample string."
  }
  [2]=>
  object(SampleObject)#3 (1) {
    ["test"]=>
    string(14) "sample string."
  }
  [3]=>
  object(SampleObject)#4 (1) {
    ["test"]=>
    string(14) "sample string."
  }
}
===========================================

どうでしょうか。

loop回数と要素数が同じにならないように配列内部ポインタを変更しているにも関わらず、要素数分loopしています。
ループ内での各要素については、配列定義時のオブジェクトへの参照が取得できています。
また、処理が終わった後の配列内の各要素には、loop内で行った処理が反映されているのがわかります。

結論


以上の結果から、foreach文内で行われている「配列のコピー」は、以下のようなものと捉えることができます。

・配列の参照コピーではなく、foreachが開始した段階で新たに別の配列が内部で作成されている(配列内部ポインタが共有されていないため)

・配列の各要素(この場合はオブジェクト)については、参照のコピーが行われている(var_dumpの出力結果をみると同じオブジェクトへの参照になっているため)

所見

このような(ある意味)不思議な挙動をするにもかかわらず、プログラムを作る際に問題にならないのは、その動き自体が「プログラムを作成する側が、foreach文を使う際にイメージする挙動に近い結果が得られるため」と考えられます。

作る側がイメージする挙動に近いということは、利用の際の敷居(難易度)を下げ、意図しない不具合を無くし、最終的に作る側に対してメリットという形で還元されます。

foreachの一見不可解な動きも、PHP言語がプログラマのために用意した便利な仕組みを実現するために必要なことだったのかもしれません。

後は、foreach文のデメリットも把握したうえで、必要に応じて使う側がコントロールすればよいわけです。

ちなみに、foreach文による配列のコピーが問題になる場合は、each、list関数を使ってwhile文でループさせるのが良いようです(もちろんfor文でもOK)。