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の言語仕様は腐っている(-_-“)