肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

オブジェクトの参照渡しと値渡しについて

唐突ですが、今日は昨日 id:kensuu に聞かれてちゃんとこたえられなくて id:Yudoufu に聞いてちゃんと理解したことをまとめようと思います(謎


というわけで、オブジェクトの参照渡しと値渡しについてです。(環境:PHP5.2.6)

関数に値を引き渡す(通常の変数)

まず、オブジェクトではなく通常の値を渡す場合を考えます。
以下のコードを見てください。

<?php

// 参照渡し
function test_r (&$a)
{
    $a = 2;
}

// 値渡し
function test_n ($a)
{
    $a = 3;
}

$b = 1;
echo $b; 
echo "\n";
test_r($b);
echo $b; 
echo "\n";
test_n($b);
echo $b;
echo "\n";

この結果は、

1
2
2

となります。
test_r() は、値を参照で渡しているため、関数内で変更された値は、 $b にも影響しますが、 test_n() は値渡しでコピーされるため、関数内で変更された値は $b には反映されません。

関数に値を引き渡す場合(オブジェクト)

次に、オブジェクトを関数に渡す場合を考えます。
まず、以下のコードを見てください。

<?php

class Test
{
    var $a = 1;
    var $b = 2;

    function write ($a, $b) 
    {   
        $this->a = $a; 
        $this->b = $b; 
    }   

    function print_var ()
    {   
        echo "a :" . $this->a . " ";
        echo "b :" . $this->b . "\n";
    }   
}

function test_write (&$t)
{
    $t->write(3, 4); 
    $t->print_var();
}

function test_write2 ($t)
{
    $t->write(3, 4); 
    $t->print_var();
}

Testという、2つのプロパティに値を書き込むメソッドと2つの値を出力するだけのメソッドを持つクラスを用意します。
そして、それに参照渡しをする関数 test_write() と、値渡しをする関数 test_write2() の2つを用意します。

そして、以下の2つが、テストの実行コードと結果です。関数に渡す前、関数内、関数に渡した後の3箇所で、出力のメソッドを呼び出しています。


test_write() のテスト

<?php
$test = new Test();
$test->print_var();
test_write($test);
$test->print_var();

実行結果

a :1 b :2
a :3 b :4
a :3 b :4

関数内でwriteメソッドを使って書き換えられた値は、関数の外のオブジェクト $test にも反映されています。

次に test_write2() です。

<?php
$test = new Test();
$test->print_var();
test_write2($test);
$test->print_var();

実行結果

a :1 b :2
a :3 b :4
a :3 b :4

ここが問題です。
参照渡し(&)で渡したわけではないのに、関数の外の $test のプロパティまで更新されています。

PHP5では、オブジェクトを渡すときは参照になる・・?

どこかでそんなことを聞いたことがあります。(そんな曖昧なことだからいざというとき困るのです

ということで、確かめるべく test_write() と test_write2() を以下のように書き換えます。具体的には、最後に $t = null; として、$tを亡き者にしようとしているだけです。

<?php

function test_write (&$t)
{
    $t->write(3, 4); 
    $t->print_var();

    $t = null;
}

function test_write2 ($t)
{
    $t->write(3, 4); 
    $t->print_var();
    
    $t = null;
}


参照で渡されているなら、関数実行後、$t が null になっているはずなので、print_varメソッドを呼べなくなるはずです。


test_write()

<?php
$test = new Test();
$test->print_var();
test_write($test);
$test->print_var();

実行結果

a :1 b :2
a :3 b :4
PHP Fatal error:  Call to a member function print_var() on a non-object

予想通り、関数の中でnullを代入されてしまったので、関数の外の $test にも影響をあたえ、オブジェクトが使えなくなっています。

で、問題が次。
test_write2()

<?php
$test = new Test();
$test->print_var();
test_write2($test);
$test->print_var();

実行結果

a :1 b :2
a :3 b :4
a :3 b :4

!!!!!
おい!!
と、なったわけです。
同じ場所を参照していて、その実体を null で置き換えたなら、3つ目の print_varメソッドは呼べないはずですが、正しく 3 4 の値を出力してくれるのです。

なんでなんで?
ということで、

カラクリを図にした *1

test_write()に値を渡したときの挙動。

test_write2()に値を渡したときの挙動。

もう、図の通りなのですがw、要するに、値渡しをしたつもりでも、ポインタを格納する場所は複製されても、その内容は渡しす前のものを参照しているので、その実体は同じものを見ている。逆に参照渡しは、同じポインタの格納場所を参照しているので、nullされたら関数の外でも使えなくなってしまうということです。

まとめ

わかったようで曖昧な理解だった参照渡しについてのお話でした。
意外とわかっていたようでわかっていなかった人も多いのではないでしょうか・・・?*2

あと、参照渡しという言い方が正しいのかどうかは定かではありません(ぉ

それから、SkypeのWhiteBoardMeetingというソフトを使って遠隔おえかき授業をしてもらったのですが、これがやたらと便利です。
Macでは使えないんだって!
ざまあ!(何



(なんか間違ってるかもしれないかもしれなかったら、こっそりコメントとかでDISってください>< )

*1:しれくれた、id:Yudoufu さんがw

*2:とか勝手に仲間を増やしたがる!