LoginSignup
59
69

More than 3 years have passed since last update.

【PHP超入門】参照(リファレンス)の代入について

Last updated at Posted at 2017-08-23

はじめに

この記事を理解するには、変数、関数、クラスの基本的な知識が必要です。
PHP内部で参照の代入がどのように処理されているのか見ていくことで理解を深めることが目的です。
PHPマニュアルでは参照ではなくリファレンスという表記をしておりますが、リファレンスより参照と呼ぶ方が多いと思われるため、この記事ではリファレンスではなく参照と記載します。
私はPHPの勉強中で他言語の知識はありません。
おそらくですが、他言語の参照とPHPの参照は異なる部分があるのかと思います。
この記事の参照という言葉は、PHPマニュアルのリファレンスのことを指しています。

この記事は下記の記事の続きになります。

上記の記事で説明している内容は改めて説明しませんので、最初に上記の記事をご覧ください。

今回、下記の流れでまとめました。

参照の記述
スカラー型の参照代入
  サンプルケース1
  サンプルケース2
  サンプルケース3
配列型の参照代入
  サンプルケース1
  サンプルケース2
  サンプルケース3
foreachと参照
  サンプルケース1
  サンプルケース2
オブジェクト型の参照代入
  サンプルケース1

私自身未熟ですので説明に誤りがあるかもしれません。
誤りがあれば、ご指摘ください ((_ _ (´ω` )ペコ

参照の記述

変数名の前に & を記述すると参照になります。
参照による代入を行うには $a = &$b または $a =& $b のように記述します。
&= の前に記述して $a &= $b とした場合は、複合演算子のビット演算の代入になり、参照とは全くの別物です。
$a &= $b という記述は $a = $a & $b と同義ですので注意してください。

前回の記事の値の代入と同様に、スカラー型、配列型、オブジェクト型の参照代入について見ていきましょう。

スカラー型の参照代入

スカラー型(整数型)を参照代入すると、PHP内部ではどのように処理されるのか見ていきましょう。

サンプルケース1(スカラー型の参照代入)

スカラー型(整数型)の参照代入
$a = 1;
$b = &$a;
$b = 2;
echo $a // 結果:2
echo $b // 結果:2

2行目で &$a と記述していますので $a の参照を $b に代入しています。
参照代入すると $a$b は運命共同体になり、$a$b は全く同じ値を指し示します。
$a の値を変更すると、同じ値を指し示している $b にも影響します。
逆に $b の値を変更すると、同じ値を指し示している $a にも影響します。

PHP内部ではどのようになっているのか1行ずつ見ていきましょう。

1行目

1行目
$a = 1;

ref_assignment_scalar_pt1_1.png

$a(int)1 を指し示します。

2行目

2行目
$b = &$a;

ref_assignment_scalar_pt1_2.png

&$a と記述することで、参照代入しています。
$a$b は運命共同体になるため、全く同じ値を指し示します。
refcount2 になります。
参照のため is_ref1 になります。

3行目

3行目
$b = 2;

ref_assignment_scalar_pt1_3.png

$b2 を代入しました。
$b$a が指し示している値は (int)2 になります。

最後に echo$a$b を表示すると両方とも 2 と表示されます。
1行ずつPHP内部でどのように処理されているのか見ていくことで理解できたかと思います。

もう一つ別のケースを見てみましょう。

サンプルケース2(スカラー型の参照代入)

スカラー型(整数型)の参照代入
$a = 1;
$b = &$a;
$c = &$b;
$c = 2;
$a = 3;

2行目で $b$a を参照代入しています。
3行目で $c$b を参照代入しています。
先ほども説明したとおり、参照代入すると運命共同体になり、全く同じ値を指し示すようになります。
$a$b$c は全く同じ値を指し示していると言えます。

PHP内部ではどのようになっているのか1行ずつ見ていきましょう。

1行目

1行目
$a = 1;

ref_assignment_scalar_pt2_1.png

$a(int)1 を指し示します。

2行目

2行目
$b = &$a;

ref_assignment_scalar_pt2_2.png

&$a と記述することで、参照代入しています。
$a$b は運命共同体になるため、全く同じ値を指し示します。
refcount2 になります。
参照のため is_ref1 になります。

3行目

3行目
$c = &$b;

ref_assignment_scalar_pt2_3.png

&$b と記述することで、参照代入しています。
$b$c は運命共同体になるため、全く同じ値を指し示します。
$a$b はもともと同じ値を指し示していたので、$a$b$c は全く同じ値を指し示します。
(int)1refcount3 になります。

4行目

4行目
$c = 2;

ref_assignment_scalar_pt2_4.png

$c2 を代入しています。
$a$b$c が指し示している値は (int)2 になります。

5行目

5行目
$a = 3;

ref_assignment_scalar_pt2_5.png

$a3 を代入しています。
先ほどと同じく、$a$b$c が指し示している値は (int)3 になります。
1行ずつPHP内部でどのように処理されているのか見ていくことで理解できたかと思います。

もう一つ別のケースを見てみましょう。

サンプルケース3(スカラー型の参照代入)

スカラー型(整数型)の参照代入
$a = 1;
$b = $a;
$c = 'Hello';
$d = $c;
$e = &$d;

4行目まで普通に値渡しをしています。
5行目で $e$d を参照代入しています。
PHP内部ではどのようになっているのか1行ずつ見ていきましょう。

1行目

1行目
$a = 1;

ref_assignment_scalar_pt3_1.png

$a(int)1 を指し示します。

2行目

2行目
$b = $a;

ref_assignment_scalar_pt3_2.png

参照代入ではなく、値の代入のため $a の値が複製されて $b に代入されますが、コピーオンライトのため現時点ではまだ値の複製は行なわれず、同じ値を指し示します。
どちらかの値に変更があった時点で値の複製と書き換えが行われます。

3行目

3行目
$c = 'Hello';

ref_assignment_scalar_pt3_3.png

$c(string)'Hello' を指し示します。

4行目

4行目
$d = $c;

ref_assignment_scalar_pt3_4.png

参照代入ではなく、値の代入のため $c の値が複製されて $d に代入されますが、コピーオンライトのため現時点ではまだ値の複製は行なわれず、同じ値を指し示します。
どちらかの値に変更があった時点で値の複製と書き換えが行われます。

5行目

5行目
$e = &$d;

ref_assignment_scalar_pt3_5.png

$e$d を参照代入しています。
$e$d は運命共同体になり、全く同じ値を指し示します。

先ほどまで $d$c と同じ値を指し示していましたが、参照の代入をしたため $d$e は全く同じ値を指し示す必要があります。
つまり、この時点で値の複製が必要になったということです。
値の複製が行われ、$d$e は全く同じ値を指し示し、その値の refcount2 になり、is_ref1 になります。
それにあわせて $c が指し示している値の refcount1 になります。

今まで値自体に変更があったときに値の複製と書き換えが行われる例だけを見てきましたが、値自体に変更がなくても、参照によって値の複製が必要なときは、それにあわせて値の複製が行われます。

次は配列型の参照代入について見ていきましょう。

配列型の参照代入

配列型を参照代入すると、PHP内部ではどのように処理されるのか見ていきましょう。

サンプルケース1(配列型の参照代入)

配列型の参照代入
$a = 'a';
$b = 'b';
$c = 'c';
$arr = [$a, $b, &$c];
$a = 'd';
$b = 'e';
$c = 'f';

このコードは下記の回答からの引用です。

引用元:phpではarrayは参照型ではないのでしょうか?ある関数にarrayの変数を渡し... - Yahoo!知恵袋

最後に var_dump($arr) で表示すると下記のようになります。

$arrの表示結果
array(3) {
    [0] => string(1) "a"
    [1] => string(1) "b"
    [2] => &string(1) "f"
}

なぜ、このような結果になるのか1行ずつ見ていきましょう。

1〜3行目

1〜3行目
$a = 'a';
$b = 'b';
$c = 'c';

ref_assignment_array_pt1_1.png

変数名は各値を指し示します。

4行目

4行目
$arr = [$a, $b, &$c];

ref_assignment_array_pt1_2.png

$arr に配列を代入しています。
配列の各値は $a$b$c ですが、$c だけ参照代入です。
参照代入のため $c$arr[2] は運命共同体になり、全く同じ値を指し示します。
(string)'c'refcount2 になり、is_ref1 になります。
他の値は現時点で複製する必要がないので、$a$b と同じ値を指し示します。
どちらかの値に変更があった時点で値の複製と書き換えが行われます。

5〜7行目

5〜7行目
$a = 'd';
$b = 'e';
$c = 'f';

ref_assignment_array_pt1_3.png

$a$b$c に別の値を代入しています。
$c$arr[2] は参照で同じ値を指し示しています。
そのため、最終的に $arr の配列の値を出力すると下記のようになります。

$arrの表示結果
array(3) {
    [0] => string(1) "a"
    [1] => string(1) "b"
    [2] => &string(1) "f"
}

もう一つ別のケースを見てみましょう。

サンプルケース2(配列型の参照代入)

配列型の参照代入
$a = [1,2,3];
$b = &$a;
$a[0] = 'a';
$b[2] = 'c';

最後に var_dump($a) で表示すると下記のようになります。

$aの表示結果
array(3) {
    [0] => string(1) "a"
    [1] => int(2)
    [2] => string(1) "c"
}

なぜ、このような結果になるのか1行ずつ見ていきましょう。

1行目

1行目
$a = [1,2,3];

ref_assignment_array_pt2_1.png

変数名 $a(array) を指し示し、配列の各キーは各値を指し示します。

2行目

2行目
$b = &$a;

ref_assignment_array_pt2_2.png

$b$a を参照代入しています。
$b$a は運命共同体になり、全く同じ (array) を指し示します。
(array)refcount2 になり、is_ref1 になります。

3行目

3行目
$a[0] = 'a';

ref_assignment_array_pt2_3.png

$a[0]a を代入しています。
$a の配列キー 0 の値は (string)'a' になります。

4行目

4行目
$b[2] = 'c';

ref_assignment_array_pt2_4.png

$b の配列キー 2 の値は (string)'c' になります。

$a$b は全く同じ (array) を指し示しているため、最後に var_dump($a) で表示すると下記のようになります。

$aの表示結果
array(3) {
    [0] => string(1) "a"
    [1] => int(2)
    [2] => string(1) "c"
}

もう一つ別のケースを見てみましょう。

サンプルケース3(配列型の参照代入)

配列型の参照代入
$a = [1,2,3];
$b = &$a[1];
$c = $a;
$c[0] = 'a';
$c[1] = 'b';
$c[2] = 'c';

このコードは下記の記事からの引用です。

引用元:PHPとかいう糞言語|いんまのブログ

最後に var_dump($a) で表示すると下記のようになります。

$aの表示結果
array(3) {
    [0] => int(1)
    [1] => &string(1) "b"
    [2] => int(3)
}

なぜ、このような結果になるのか1行ずつ見ていきましょう。

1行目

1行目
$a = [1,2,3];

ref_assignment_array_pt3_1.png

変数名 $a(array) を指し示し、配列の各キーは各値を指し示します。

2行目

2行目
$b = &$a[1];

ref_assignment_array_pt3_2.png

$b$a の配列キー 1 の値を参照代入しています。
$b$a[1] は運命共同体になり、全く同じ値を指し示します。
(int)2refcount2 になり、is_ref1 になります。

3行目

3行目
$c = $a;

ref_assignment_array_pt3_3.png

$c$a の配列を代入しています。
配列は複製されて代入されますが、コピーオンライトのため、現時点では配列は複製されず、$c$a と同じ (array) を指し示します。
変数に別の値を代入したり、配列の値を変更するなど、配列の複製が必要になった時点で、複製されます。

4行目

4行目
$c[0] = 'a';

ref_assignment_array_pt3_4.png

$c の配列キー 0a を代入しました。
配列の複製が必要になったため、この時点で配列の複製が行なわれます。
$a$c は別々の配列を指し示します。
$a が指し示している (array)refcount1 になります。

配列を複製する前から (int)2is_ref1 でした。
この値は参照されています。
もともと (int)2 は参照されており、配列が複製されても、参照なのは変わりません。
$a[1]$b$c[1] は全く同じ (int)2 を指し示します。
それにあわせて refcount3 になります。

この行では $a[2]$c[2] の値は同じです。
$c[2]$a[2] は同じ値ですので、この時点では複製されず、同じ (int)3 を指し示します。
(int)3refcount2 になります。
コピーオンライトのため、同じ値を指し示していますが、どちらかの値に変更があった時点で複製されて書き換えが行われます。

5・6行目

5・6行目
$c[1] = 'b';
$c[2] = 'c';

ref_assignment_array_pt3_5.png

$c[1]b を代入しています。
$c[1]$a[1]$b が指し示している値は (string)'b' になります。
$c[2](string)'c' を指し示すようになります。
それにあわせて (int)3refcount1 になります。

最後に var_dump($a) で表示すると下記のようになります。

$aの表示結果
array(3) {
    [0] => int(1)
    [1] => &string(1) "b"
    [2] => int(3)
}

1行ずつ見てきたので、なぜこのような結果になるのか理解できたかと思います。
次は foreach と参照について見ていきましょう。

foreachと参照

既に foreach については理解しているかもしれませんが、簡単に foreach の使い方について説明します。
foreach は配列または連想配列のデータを取り出すのに便利な制御構文です。
値のみ取り出したいときは、下記のように記述します。

foreach構文(値のみ取り出す)
foreach(取り出したい配列または連想配列 as 値の仮変数) {

    // ループ内で実行する処理

}

配列にある名前を取り出して表示するなら、下記のように記述します。

foreachの記述例
$data = ['佐藤', '鈴木', '高橋'];

foreach($data as $value) {
    echo "名前は {$value} です。";
}

配列のキーも取り出したいときは、下記のように記述します。

foreach構文(キーも取り出す)
foreach(取り出したい配列または連想配列 as キーの仮変数 => 値の仮変数) {

    // ループ内で実行する処理

}

連想配列にある名前と性別を取り出して表示するなら、下記のように記述します。

foreachの記述例
$data = ['佐藤' => '男', '鈴木' => '女','高橋' => '男'];

foreach($data as $key => $value) {
    echo "{$key} さんの性別は {$value} です。";
}

foreach の使い方については理解できたかと思います。

基の配列の値を2倍にするような foreach を記述してみましょう。

基の配列の値を2倍にしたい
$arr = [1,2,3];
foreach ($arr as $a) {
    $a = $a * 2;
}
var_dump($arr);

最後に var_dump$arr を出力すると下記のようになります。

$arrの出力
array(3) {
    [0] => int(1)
    [1] => int(2)
    [2] => int(3)
}

foreach 内で2倍にしても基の配列は変わっていません。
なぜかというと foreach の仮変数 $a に値を代入しているためです。
foreach は配列の値の数だけ繰り返し処理をしますが、最初の処理では $a = $arr[0] として仮変数 $a に代入していると言えます。
前回の記事でも説明していますが、値を代入すると値は複製されて別々の値になります。
そのため、基の $arr には変化がありませんでした。
基の $arr の配列を値を foreach 内で変えるには参照を使う方法があります。

基の配列の値を2倍にする
$arr = [1,2,3];
foreach ($arr as &$a) {
    $a = $a * 2;
}
var_dump($arr);

最後に var_dump$arr を出力すると下記のようになります。

$arrの出力
array(3) {
    [0] => int(2)
    [1] => int(4)
    [2] => &int(6)
}

(参照を使うのは推奨されないやり方のようですが)期待通りの結果になりました。
基の配列の値が変わっているのがわかります。
ただ、foreach の仮変数を参照にするときに気をつけるべき点と言いますか、落とし穴があります。
どのような落とし穴があるのか foreach と参照を使った例を見ていきましょう。

サンプルケース1(foreachと参照)

foreachと参照
$arr = [1,2,3];
foreach ($arr as &$a) {
    $a = $a * 2;
}
$a = 10;
var_dump($arr);

このコードは下記の記事からの引用です。

引用元:Qiita - 初心者を戒めるPHP リファレンス(参照)はやめろ by @tadsan さん

最後に var_dump($arr) で表示すると下記のようになります。

$arrの表示結果
array(3) {
    [0] => int(2)
    [1] => int(4)
    [2] => &int(10)
}

foreach に慣れていない方には foreach がどのような処理をしているのかわからないかと思います。
今回の foreach は3回ループ処理をしており、仮変数で指定した $a を使ってループ処理をしています。
foreach を使わずに記述すると下記のようになります。

$arr = [1,2,3];

// foreach の1週目
$a = &$arr[0];
$a = $a * 2;

// foreach の2週目
$a = &$arr[1];
$a = $a * 2;

// foreach の3週目
$a = &$arr[2];
$a = $a * 2;

$a = 10;
var_dump($arr);

この記述を見るとわかりますが、foreach の仮変数 $a はグローバルスコープにあります。
$a がグローバルスコープにあるということは foreach が終わっても、$aforeach が終了した時点の状態を保っているということです。
foreach の仮変数 $a はグローバルスコープにあり、foreach が終了しても最後の状態を保っているというのがポイントになりますので、覚えておいてください。

それでは、1行ずつ見ていきましょう。

1行目

1行目
$arr = [1,2,3];

ref_assignment_array_foreach_pt1_1.png

変数名 $arr(array) を指し示し、配列の各キーは各値を指し示します。

2行目(foreachの1週目)

2行目(foreachの1週目)
$a = &$arr[0];

ref_assignment_array_foreach_pt1_2.png

ここからは foreach 内の1週目の処理です。
foreach の仮変数で記述した $a$arr[0] を参照代入しています。
$a$arr[0] は運命共同体になり、全く同じ値を指し示します。
(int)1refcount2 になり、is_ref1 になります。

3行目(foreachの1週目)

3行目(foreachの1週目)
$a = $a * 2;

ref_assignment_array_foreach_pt1_3.png

$a の値に $a * 2 の値を代入しています。
$a1 ですので、1 * 2 の値が代入されます。
$a$arr[0] が指し示す値は (int)2 になります。

4行目(foreachの2週目)

4行目(foreachの2週目)
$a = &$arr[1];

ref_assignment_array_foreach_pt1_4.png

ここからは、foreach 内の2週目の処理です。
$a$arr[1] は運命共同体になり、全く同じ値を指し示します。
(int)2refcount2 になり、is_ref1 になります。
もともと指し示していた (int)2refcount1 になり、is_ref0 になります。

$a に対して $a = &$arr[0] のあとに $a = &$arr[1] と記述しました。
同じ変数に2回参照代入すると、最後の参照代入のみ有効です。
そのことは上図からもわかりますが、2回参照代入しても $arr[0]$arr[1]$a の3つが全く同じ値を指し示すということはありません。

5行目(foreachの2週目)

5行目(foreachの2週目)
$a = $a * 2;

ref_assignment_array_foreach_pt1_5.png

$a の値に $a * 2 の値を代入しています。
$a2 ですので、2 * 2 の値が代入されます。
$a$arr[1] が指し示す値は (int)4 になります。

6行目(foreachの3週目)

6行目(foreachの3週目)
$a = &$arr[2];

ref_assignment_array_foreach_pt1_6.png

ここからは、foreach 内の3週目の処理です。
$a$arr[2] は運命共同体になり、全く同じ値を指し示します。
(int)3refcount2 になり、is_ref1 になります。
もともと指し示していた (int)4refcount1 になり、is_ref0 になります。

7行目(foreachの3週目)

7行目(foreachの3週目)
$a = $a * 2;

ref_assignment_array_foreach_pt1_7.png

$a の値に $a * 2 の値を代入しています。
$a3 ですので、3 * 2 の値が代入されます。
$a$arr[2] が指し示す値は (int)6 になります。

これで foreach の処理は終了です。

8行目

8行目
$a = 10;

ref_assignment_array_foreach_pt1_8.png

冒頭でも説明しましたが、foreach の仮変数 $a はグローバルスコープにあります。
foreach が終了しても $a は最後の状態を保っています。
$a$arr[2] は運命共同体で、全く同じ値を指し示しています。
この状態で $a = 10 としているので、$a$arr[2] が指し示している値は (int)10 になります。

最後に var_dump($arr) で表示すると下記のようになります。

$arrの表示結果
array(3) {
    [0] => int(2)
    [1] => int(4)
    [2] => &int(10)
}

今回は foreach の仮変数 $a を参照にしており、foreach が終了しても、その後の $a の記述にも影響しました。
foreach の後に unset() で変数を破棄すれば、その時点で参照ではなくなりますので、その後の記述には影響しません。

unsetで変数を破棄する例
$arr = [1,2,3];
foreach ($arr as &$a) {
    $a = $a * 2;
}
unset($a); // 変数 $a の破棄
$a = 10;
var_dump($arr);

最後に var_dump($arr) で表示すると下記のようになります。

$arrの表示結果
array(3) {
    [0] => int(2)
    [1] => int(4)
    [2] => int(6)
}

unset($a) で変数が破棄されて参照ではなくなったので、その後の $a = 10 は影響しません。
ちなみに、変数が破棄されるのは unset() 内に記述した変数のみになります。

unsetで変数を破棄する例
$a = 1;
$b = &$a;
unset($b);
echo $a; // 1
echo $b; // Notice: Undefined variable

$b$a を参照代入しており、$a$b は全く同じ値を指し示していますが、unset($b) と記述して変数が破棄されるのは $b のみです。
$a が破棄されることはありません。
$a は破棄されていないため、echo $a1 と表示されますが、$b は破棄されているため、echo $b とするとエラーが表示されます。

もう1つ foreach と参照のパターンを見てみましょう。

サンプルケース2(foreachと参照)

foreachと参照
$arr = [1,2,3];

foreach ($arr as &$a) {
}
var_dump($arr); // 1回目のvar_dump

foreach ($arr as $a) {
}
var_dump($arr); // 2回目のvar_dump

2つの foreach がありますが、foreach 内では何の処理もしていません。
このコードは下記の記事からの引用です。

引用元:foreachの$valueを参照で受けると思わぬバグを引き起こす - ぱせらんメモ

1回目の var_dump($arr) の結果です。

$arrの表示結果(1回目)
array(3) {
    [0] => int(1)
    [1] => int(2)
    [2] => &int(3)
}

2回目の var_dump($arr) の結果です。

$arrの表示結果(2回目)
array(3) {
    [0] => int(1)
    [1] => int(2)
    [2] => &int(2)
}

今回のコードを foreach を使わずに記述すると下記のようになります。

$arr = [1,2,3];

// 1回目の foreach の処理
$a = &$arr[0]; // 1週目
$a = &$arr[1]; // 2週目
$a = &$arr[2]; // 3週目

var_dump($arr);

// 2回目の foreach の処理
$a = $arr[0]; // 1週目
$a = $arr[1]; // 2週目
$a = $arr[2]; // 3週目

var_dump($arr);

それでは、1行ずつ見ていきましょう。

1行目

1行目
$arr = [1,2,3];

ref_assignment_array_foreach_pt2_1.png

変数名 $arr(array) を指し示し、配列の各キーは各値を指し示します。

2行目(最初のforeachの1週目)

2行目(最初のforeachの1週目)
$a = &$arr[0]; // 1週目

ref_assignment_array_foreach_pt2_2.png

$a$arr[0] を参照代入しています。
$a$arr[0] は運命共同体になり、全く同じ値を指し示します。
(int)1refcount2 になり、is_ref1 になります。

3行目(最初のforeachの2週目)

3行目(最初のforeachの2週目)
$a = &$arr[1]; // 2週目

ref_assignment_array_foreach_pt2_3.png

$a$arr[1] を参照代入しています。
$a$arr[1] は運命共同体になり、全く同じ値を指し示します。
(int)2refcount2 になり、is_ref1 になります。
それにあわせて (int)1refcount1 になり、is_ref0 になります。

4行目(最初のforeachの3週目)

4行目(最初のforeachの3週目)
$a = &$arr[2]; // 3週目

ref_assignment_array_foreach_pt2_4.png

$a$arr[2] を参照代入しています。
$a$arr[2] は運命共同体になり、全く同じ値を指し示します。
(int)3refcount2 になり、is_ref1 になります。
それにあわせて (int)2refcount1 になり、is_ref0 になります。

これで最初の foreach の処理は終了です。
foreach 内で値を変更することはしていないので、foreach の処理をしても refcountis_ref しか変わっていません。
注目すべきは、最後の状態です。
先ほども説明しましたが、foreach の仮変数 $a は、グローバルスコープにあり、foreach が終了しても最後の状態のままです。
$a$arr[2] は運命共同体であり、全く同じ値の (int)3 を指し示しています。
この状態で次の foreach の処理がされます。

5行目(次のforeachの1週目)

5行目(次のforeachの1週目)
$a = $arr[0]; // 1週目

ref_assignment_array_foreach_pt2_5.png

$a$arr[0] の値を代入しています。
$arr[0] の値は (int1) ですので、$a が指し示している値は (int)1 になります。

6行目(次のforeachの2週目)

6行目(次のforeachの2週目)
$a = $arr[1]; // 2週目

ref_assignment_array_foreach_pt2_6.png

$a$arr[1] の値を代入しています。
$arr[1] の値は (int2) ですので、$a が指し示している値は (int)2 になります。

7行目(次のforeachの3週目)

7行目(次のforeachの3週目)
$a = $arr[2]; // 3週目

ref_assignment_array_foreach_pt2_7.png

$a$arr[2] の値を代入しています。
$arr[2] の値は (int2) ですので、$a が指し示している値は (int)2 になります。

最後に var_dump($arr) を表示した結果です。

$arrの表示結果
array(3) {
    [0] => int(1)
    [1] => int(2)
    [2] => &int(2)
}

1行ずつ見ていくことで、なぜこのような結果になるのか理解できたかと思います。
foreach の仮変数はグローバルスコープにあり、参照した場合は foreach が終了した後も影響することに注意してください。
変数を破棄するには unset を使うようにしましょう。
そもそも、foreach に参照を使うのはあまり推奨されないやり方です。
そのことに関しては最後に参考になる記事を紹介します。

次はオブジェクト型の参照代入について見ていきましょう。

オブジェクト型の参照代入

オブジェクト型を参照代入すると、PHP内部ではどのように処理されるのか見ていきましょう。

サンプルケース1(オブジェクト型の参照代入)

オブジェクト型の参照代入
class foo {
  public $bar = 1;
  public $baz = "a";
}
$a = new foo;
$b = &$a;
$a->bar = 2;
$b->baz = "b";
$c = &$a->bar;
$c = 4;
$d = $b;
$d->bar = 5;
$d = 6;
$e = clone $b;
$e->bar = 7;

PHP内部ではどのようになっているのか1行ずつ見ていきましょう。

1行目

1行目
$a = new foo;

ref_assignment_object_pt1_1.png

$a(object)foo を指し示します。
(object)foo の各プロパティが各値を指し示します。

2行目

2行目
$b = &$a;

ref_assignment_object_pt1_2.png

$b$a を参照代入しています。
$b$a は運命共同体になり、全く同じオブジェクトを指し示すようになります。
(object)foorefcount2 になり、is_ref1 になります。

3行目

3行目
$a->bar = 2;

ref_assignment_object_pt1_3.png

$a のオブジェクトの bar プロパティは (int)2 を指し示すようになります。
それにあわえて (int)1refcount1 になります。

4行目

4行目
$b->baz = "b";

ref_assignment_object_pt1_4.png

$b のオブジェクトの baz プロパティは (string)'b' を指し示すようになります。
それにあわえて (string)'a'refcount1 になります。

5行目

5行目
$c = &$a->bar;

ref_assignment_object_pt1_5.png

$c$a が指し示しているオブジェクトの bar プロパティの値を参照代入しています。
$c$a->bar は運命共同体になり、全く同じ値を指し示すようになります。
(int)2refcount2 になり、is_ref1 になります。

6行目

6行目
$c = 4;

ref_assignment_object_pt1_6.png

$c が指し示してる値は (int)4 になります。

7行目

7行目
$d = $b;

ref_assignment_object_pt1_7.png

$d$b のオブジェクトを代入しています。
$d$b と同じオブジェクトを指し示します。
前回の記事でも説明しましたが、オブジェクトの各プロパティの値に変更があっても、オブジェクト自体が複製されることはありません。
オブジェクトを複製するには clone キーワードを使う必要があります。
$d(object)foo を直接指し示しており、$d = 1 のように別の値を代入するなど、変数名 $d が直接指し示すものに変更があれば、その時点で複製されて書き換えが行なわれます。

8行目

8行目
$d->bar = 5;

ref_assignment_object_pt1_8.png

$d が指し示しているオブジェクトの bar プロパティに 5 を代入しています。
(object)foobar プロパティが指し示している値は 5 になります。

9行目

9行目
$d = 6;

ref_assignment_object_pt1_9.png

$d6 を代入しています。
$d が直接指し示しているものに変更があったので、この時点で複製されて書き換えが行なわれます。
$d(int)6 を指し示します。
それにあわせて (object)foorefcount2 になります。

10行目

10行目
$e = clone $b;

ref_assignment_object_pt1_10.png

$b が指し示しているオブジェクトの複製を $e に代入しています。
$e は複製したオブジェクトを指し示します。
コピーオンライトのため、各プロパティが指し示している値に変更がなければ、既存の値を指し示します。
そのため、$e のオブジェクトの baz プロパティは、$b のオブジェクトの baz プロパティと同じ (string)'b' を指し示します。
どちらかの値に変更があった時点で、複製されて書き換えが行なわれます。
ここで注目すべきは、$e のオブジェクトの bar プロパティです。
$b のオブジェクトを複製して、$e に代入しましたが、もともと $bbar プロパティの値は参照されている状態です。
オブジェクトを複製しても参照の状態は変わりません。
$a$b が指し示している (objcet)foobar プロパティと $c$e が指し示している (objcet)foobar プロパティの3つは運命共同体であり、全く同じ値を指し示しています。
オブジェクトの複製にあわせて (int)5(string)'b'refcount の数も変わります。

11行目

11行目
$e->bar = 7;

ref_assignment_object_pt1_11.png

$e が指し示している (object)foobar プロパティが指し示している値は (int)7 になります。
つまり $a$b が指し示している (object)foobar プロパティと、$c と、$e が指し示している (object)foobar プロパティが指し示している値は (int)7 になるということです。

オブジェクトの参照代入がどのようになるか理解できたかと思います。

最後に

1行ずつPHP内部でどのように処理されているのか見てきたため、参照の代入について理解できたかと思います。
最後に参照について @tadsan さんの記事を抜粋して紹介します。

リファレンス (references, 参照とも呼ばれる)はPHPの言語仕様の中では珍しく、説明がめんどくさい方の文法です。用語がいっぱい出てきてめんどくさいってことは、 必要に迫られなければ利用しなくて良い 文法だってことです。

私の持論ですが、 リファレンス(参照)を利用しないで済むなら避けた方が賢明 です。

引用元:Qiita - PHPのリファレンス(参照&)の傾向と対策、あるいはさよなら

今まで見てきたのでわかると思いますが、参照を使ったときの処理は複雑です。
参照はなるべく避けた方が賢明なようです。
参照の注意点については @tadsan さんの記事をご覧ください。

意図的に参照は避けるにしても、参照についての理解は必要かと思いましたので、今回記事にまとめました。
参照については、参照代入の他に参照渡しがあります。
参照渡しについては別の記事で説明したいと思います。

参考サイト

この記事を書くにあたり、参考にしたサイトです。
記事内で紹介・引用したサイトは既に記載しているためここでは省略します。
とても参考になりました。ありがとうございます。

note

note でも記事を公開してるので、興味がある方はご覧ください。

【初学者向けコードリーディング】 PHP の TODO アプリのコードを一緒に読み解こう

59
69
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
69