PHPのリファレンスを使ったコードを調べていたら、あまりにすごい現象があったので紹介したい。
https://www.programming-magic.com/20080307090613/
<?php $array = array(1,2,3); $ref = &$array[1]; //参照渡し $copy = $array; $copy[0] = 'a'; $copy[1] = 'b'; $copy[2] = 'c'; foreach( $array as $v ) print "$v"; //←この出力は ?>
実はこうなる
1
b
3
ついでに、PerlとRubyで同じコードを書いて動作を比較してみた。
Perl
@array = (1,2,3); $ref = \$array[1]; # リファレンス渡し @copy = @array; # 値コピー $copy[0] = 'a'; $copy[1] = 'b'; $copy[2] = 'c'; print "$_\n" for ( @array );
1
2
3
Perlの場合はリファレンスと言うよりは、ポインターに近い。配列のコピーは、基本的に値でコピーされるので、動作的におかしな動作はしない。
Ruby
array = [1,2,3] val = array[1] # 基本的に参照渡し copy = array # 基本的に参照渡し copy[0] = 'a' copy[1] = 'b' copy[2] = 'c' array.each{|x| print "#{x}\n";}
実行結果は、以下のようになる。
a
b
c
Rubyは全てのオブジェクトで扱われるので、実際にはcopyとarrayは同じ実体を指している。他の言語から見れば、戸惑うかもしれないが、首尾一貫している。
perlと同じように、値をコピーするには、cloneを使用して複製を作成する。
copy = array.clone # ここでは値を複製して渡す。
PHPのマニュアルには、こう書かれています。
<?php $a =& $b; ?>
この場合、$a と $b は同じ内容を指します。
注意: ここで、$a と $b は完全に 同じで、$a が $b を 指しているわけではなく、その逆でもありません。$a と $b は同じ場所を指しているのです。
Rubyを知っていた私は、基本的に変数はオブジェクトととして扱われており、普通に=だけで代入したときは、暗黙のうちに値のコピー作成して代入していると考えていた。だから、&演算子を使用すれば、このときだけ参照代入、値を複製しないで、同じ値を示すのだと思っていました。
そうならば、仕様としてはそんなにおかしくは、ありません。
さらに、マニュアルには、もう一つ注意事項が書かれています。はっきり言って、ここが異常!
注意: リファレンスを含む配列をコピーする際に、そのリファレンスが解消される ことはありません。配列を関数に値渡しする場合も同様です。
$copy = $array // $arrayが配列の場合の動作は、以下の意味だと普通は思うでしょう。 $copy = array(); foreach( $array as $i => $v ) $copy[$i] = $v; // $arrayが配列の場合の動作は、実際にはこんな動作になっています。 foreach( $array as $i => $v ){ if( is_ref($array[$i])){ // is_refはありません。 $copy[$i] = & $array[$i]; } else { $copy[$i] = $array[$i]; } }
なんだ?
以下の例を見て欲しい。関数fooで、配列の特定の条件の要素だけをリファレンスとして保存します。そして関数barで値渡しをした(と本人は思っている)、要素を変更して返します。なんと、リファレンスを保存した値だけが、リファレンスモードになっていてもとの配列を破壊してしまいます。
<?php $buffer = array(); function foo( &$ary ) { global $buffer; // 特定の条件のものだけ保存 for( $i = 0 ; $i < sizeof($ary) ; $i++ ){ if( $ary[$i] == 2 ) $buffer []= &$ary[$i]; } } function bar( $ary ) { for( $i = 0 ; $i < sizeof($ary) ; $i++ ){ $ary[$i] += 10; // 全て書き換え } return $ary ; } $array = array(1,2,3); foo( $array ); $copy = bar( $array ); foreach( $array as $v ) print "$v\n"; //←この出力は ?>
1
12
3
こんな状態になったら処理系のバグだと思うだろう。
原因はPHPの変数と値の保持の仕方にあるようです。
基本的に変数(配列の要素も含む)は、値を保持しています。これを値モードとします。リファレンス演算子を作成した場合は変数はリファレンスモードになって、別な場所に値をコピーし、その値を差すようにポインタに変換します。また、コピー先の変数にもそのポインタを渡します。そのときリファレンスカウンターを+1します。
一方リファレンスになって変数がスコープを外れて削除された場合や、unset命令で削除された場合は、値の持っているリファレンスカウンターを減らします。リファレンスカウンターが1になったとき、最後の変数に値モードになって変数が直接値を保持するようになります。
こんな現象が起きる仕様は、あまりにもお粗末というしかありません。言語デザインのセンスは酷いというか、、なんというか、
結論。はっきり言ってPHPの言語仕様は腐っている(-_-“)