LoginSignup
2
2

オブジェクトの単純代入はシャローコピーではない

Last updated at Posted at 2024-02-03

本記事は、「【上級資格取得に向けて】PHPを理解するために知っておくべきこと①」の記事に対して、代入とコピーの説明に誤りがあったので編集リクエストさせていただいた修正内容を掲載しています。
誤った内容が広まらないことを願って記事公開しておきます。
編集リクエストが採用されましたら本記事は削除いたします。 ストックしていただいた方がいらっしゃいますので残しておきます。

本記事に誤りや更に修正した方がいい内容がありましたら、コメントいただけると有難いです。

以下の意見交換記事も参考にしていただけましたら幸いです。

※以下、編集リクエストさせていただいた内容です※

言語が違えばルールや常識も変わるものです。

この記事ではPHP8.0技術者認定試験上級資格取得を目指すにあたり、PHPを理解するために知っておくべきことについてさらっとまとめてみました。

ぜひご参考までに読んでいただければと思います。

目次

  1. 代入とコピーの違い
    1.1 代入について
    1.2 コピーについて
    1.2.1 シャローコピーについて
    1.2.2 ディープコピーについて
    1.2.3 clone命令と__cloneメソッドについて
  2. コピーオンライト
  3. リファレンスカウントの原理
    3.1 zvalコンテナ
  4. まとめ

代入とコピーの違い

代入について

PHPにおいて、値を変数に代入すると値がコピーされるのが規定ですが、オブジェクトだけは例外的に値をコピーせずにオブジェクトを共有します。

<?php
class Person {
    public $lastname;
    public $firstname;

    public function __construct($lastname, $firstname){
        $this->lastname = $lastname;
        $this->firstname = $firstname;
    }
}

$p1 = new Person('太郎', 'PHP');
$p2 = $p1; // オブジェクトを共有

$p2->lastname = '花子';
echo $p2->lastname . PHP_EOL;  // 花子
echo $p1->lastname . PHP_EOL;  // $p1->lastnameも花子になってしまう

そのため、上記のように$1$p2に単純代入後に$p2lastnameを変更すると、その変更はオブジェクトを共有している$p1にも反映されてしまいます。
なので、オブジェクトはコピーされておらず、同じオブジェクトを参照する新しい変数$p2を作成しているだけになります。

image.png

単純な代入ではオブジェクトを共有します。オブジェクトはコピーしません。

コピーについて

オブジェクトを共有するのではなく、オブジェクトをコピーしたい場合があります。この時登場するのが「clone命令」および「__cloneメソッド」です。
オブジェクトのコピーには、シャロー(浅い)コピーとディープ(深い)コピーの2種類があります。
ここからは順を追って説明していきます。まずは、シャローコピーとディープコピーについて説明します。

シャローコピーについて

単純な代入とは異なり、

オブジェクトをコピーしてから新しい変数に代入します。ただし、オブジェクト内にあるオブジェクト(深い層のオブジェクト)はコピーせずに共有します。
オブジェクトを浅く(1階層だけ)コピーするのがシャローコピーです。

image.png

ディープコピーについて

オブジェクト内のオブジェクト、更にその中のオブジェクトの深い層まで再帰的に完全コピーすることを、ディープコピー といいます。

image.png

clone命令と__cloneメソッドについて

clone命令はオブジェクトのコピーを生成するための命令です。

clone命令の規定の挙動はシャローコピーです。

上述したコードで、$p2の内容を変更しても$p1に影響がないようにしたい場合は「clone命令」の規定のシャローコピーで実現できます。
オブジェクトの中に更にオブジェクトがある場合には「__cloneメソッド」の実装を変更してディープコピー処理を実装する必要があります。
__cloneメソッド」は、clone命令による複製が完了したタイミングで呼び出されるメソッド で、コピー先のプロパティ値を強制的に変更することができます。

__cloneメソッド」についてPHP公式リファレンスの例を用いて、解説していきます。

<?php
class SubObject
{
    static $instances = 0;
    public $instance;

    public function __construct() {
        $this->instance = ++self::$instances;
    }

    public function __clone() {
        $this->instance = ++self::$instances;
    }
}

class MyCloneable
{
    public $object1;
    public $object2;

    function __clone()
    {
        // this->object のコピーを作成します。こうしないと、
        // 同じオブジェクトを指すことになってしまいます。
        $this->object1 = clone $this->object1;
        $this->object2 = clone $this->object2;
    }
}

$obj = new MyCloneable();

$obj->object1 = new SubObject();
$obj->object2 = new SubObject();

$obj2 = clone $obj;


print("元のオブジェクト\n");
print_r($obj);

print("クローンオブジェクト\n");
print_r($obj2);

?>

__constructメソッドは、オブジェクトが作成されるたびに$instancesをインクリメントして、$instanceにその値を設定します。

__cloneメソッドは、オブジェクトがクローンされるたびに$instancesをインクリメントして、$instance`にその値を設定します。

コードの処理としては以下のようになります。

  1. $obj = new MyCloneable();によって、MyCloneableクラスのインスタンスを新規作成

  2. $obj->object1 = new SubObject();によって、$object1にSubObjectクラスのインスタンスが生成・代入され、SubObjectクラスの__construct()メソッドによって$instancesがインクリメントされる($instances: 1)

  3. $obj->object2 = new SubObject();によって、$object2にSubObjectクラスのインスタンスが生成・代入され、SubObjectクラスの__construct()メソッドによって$instancesがインクリメントされる($instances: 2)

  4. $obj2 = clone $obj;によって、$obj2$objの複製(シャローコピー)が完了した段階で、MyCloneableクラスの__clone()が呼び出される

  5. 4.の__clone()メソッドによって、$object1(SubObjectのインスタンス)が$object1に複製(シャローコピー)された段階で、SubObjectクラスの__clone()メソッドが呼び出されて$instancesがインクリメントされ($instances: 3)、クローンされたオブジェクト($obj2)の$object1に代入される。

  6. print_r($obj);によって、元のオブジェクトが出力される

  7. print_r($obj2);クローンオブジェクトが出力される

上記の説明の通り、__clone()メソッドを用いて強制的に必要な変更を行うことでディープコピーを実現している訳です。

コピーオンライト

PHPでは コピーオンライト という仕組みが用いられています。

コンピュータ内部で、ある程度大きなデータを複製する必要が生じたとき、愚直な設計では、直ちに新たな空き領域を探して割り当て、コピーを実行してしまいます。

ところが、もし複製したデータに対する書き換えがなければその複製は無駄だったことになるでしょう。

そこで、複製を要求されても、コピーをした振りをして、とりあえず原本をそのまま指す参照させる方が無駄はありません。ただし、そのままで本当に書き換えては意図しない変更を加えてしまう可能性があります。

これは先ほど説明した「代入」のような、コピー先に変更を加えるとコピー元にも変更が反映されてしまう ような場合です。

原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。

これが「書き換え時にコピーする」、すなわち コピーオンライト (Copy-On-Write) の基本的な形態となります。
PHPの配列の代入で、コピーオンライトが用いられています。

リファレンスカウントの原理

PHPでは、「プログラムが動的に確保したメモリ領域のうち、不要になった領域を自動的に解放する」いわゆるガベージコレクションが機能として存在します。

PHPのガベージコレクションでは「参照カウント(リファレンスカウント)法」という方式で管理されています。

zvalコンテナ

PHPの変数は「zval」と呼ばれる特別なコンテナに値と型と共に保管されており、変数の型と値の他に、情報の追加ビットを2つ含んでいます。

<zvalコンテナに含まれる情報>

  1. 変数の型
  2. is_ref
  3. refcount

【is_ref】

「is_ref」は、変数が参照集合の一部かどうかを示すブール値の情報で、これによりPHPエンジンは通常の変数と参照を区別することができます。

PHPでは参照を使うことができるため、zvalコンテナは内部的なリファレンスカウント機構を持っており、メモリ使用状況を最適化します。

デフォルトでfalseにセットされます。

【refcount】

「refcount」は、この1つのzvalコンテナをどれだけ多くの変数名が指すかを表します。

具体的に挙動を確認していきます。

$a = "new string";
xdebug_debug_zval('a');

// 出力結果
// a: (refcount=1, is_ref=0)='new string'
  • $a = "new string"; というコードが実行されると、新しい変数名$aが現在のスコープで作成され、この$a変数には値が "new string" であり、型が string である新しい変数コンテナが作成されます。

  • この変数コンテナには、参照集合の一部かどうかを示す is_ref という情報のビットが含まれていますが、この時点ではユーザーランド参照(リファレンスによる代入:&$変数)が作成されていないため、is_refデフォルトでfalseにセット されます。

また、この変数コンテナを利用するシンボル(変数名:$a)が1つだけあるため、refcountは1に設定 されます。

refcountの減少でも同様に、以下のようにリンクが解除されることでrefcountの値は変化します。

$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );

// 出力結果
// a: (refcount=3, is_ref=0)='new string'
// a: (refcount=2, is_ref=0)='new string'
// a: (refcount=1, is_ref=0)='new string'

配列型 の場合は少し複雑で、それらのプロパティをそれら自身のシンボルテーブルに保管します。

$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );

/**    出力結果
*    a: (refcount=1, is_ref=0)=array (
*       'meaning' => (refcount=1, is_ref=0)='life',
*       'number' => (refcount=1, is_ref=0)=42
*    )
**/

イメージは以下のようになります。


引用元:PHP公式リファレンス

まとめ

PHPでは、変数への代入では値をコピーするのが規定ですが、オブジェクトだけは例外的にオブジェクトをコピーせずに共有するのが規定となっている。

代入

見ているオブジェクト同じです。オブジェクトをコピーしているわけではありません。

シャローコピー

オブジェクトを1階層だけ(浅く)コピーします。オブジェクト内のオブジェクト(深い層)はコピーせずに共有します。

ディープコピー

オブジェクトを深い層までコピーします。

clone

clone命令の規定の挙動はシャローコピーです。

コピーオンライト

原本またはコピーのどちらかを書き換えようとしたときに、それを検出し、その時点ではじめて新たな空き領域を探して割り当て、コピーを実行する。

リファレンスカウントの原理

<zvalコンテナに含まれる情報>

  1. 変数の型
  2. is_ref
  3. refcount
2
2
4

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
2
2