PHPにおけるオブジェクトのコピーをちゃんと理解する。参照コピー/シャローコピー/ディープコピー【初心者向け】

PHP基礎 オブジェクトのコピー<解説> PHP
※当サイトはアフィリエイト広告を掲載しています。

オブジェクトのコピー。それはプログラミングにおいての基礎中の基礎です。

しかしながらあまり使わない(汗)。PHPの実務を長く続けていますが、個人的にはオブジェクトをコピーする機会がなかなかありません。

Lara
Lara

というか実務でclone、使ったこと無いかも!?参照渡し位は使うけど……

Webアプリケーションにおいてはあまり使わないのか!?そもそもおま環(私の環境)なのか!?それとも私のスキル不足?孤独なフリーランスには分かりません。。

そんな訳で、これまでコピーについてあまり考えたことがありませんでした。……が、何かの拍子で顕在化したため、改めて調査することに。

せっかくなので初心者の方にも分かるようにまとめてみました。参考にしていただければ幸いです。

PHPのコピーの種類

まず最初に、大枠を掴むためにPHPのオブジェクトコピーの概要からご紹介します。

以下の3つに分けられます。

  1. 参照のコピー
    参照=リファレンスをコピーする
  2. シャローコピー(浅いコピー)
    新しいオブジェクトを複製するが、その内部オブジェクトは参照のコピー
  3. ディープコピー(深いコピー)
    新しいオブジェクトを複製し、その内部オブジェクトも複製する

現時点では分からなくてもかまいません。これから一つずつご紹介していきます。

なお、シャローコピーについては定義の認識に相違がある方もいらっしゃるかもしれません。これについては諸説あるようで、最後のおまけで触れています。

本ページでは参照のコピーとシャローコピーは別のものという前提で記載しています。

それでは解説していきましょう。

参照のコピー(リファレンスのコピー)

変数の中身は、オブジェクトそのものが入っている訳ではありません。

参照のコピーを理解するためには、最低限、それを理解しておく必要があります。

参照のコピーは多くの人が知る概念ですので、ご存じの方は読み飛ばしてください。

参照とは!?

これを理解するためには、まずデータ種別ごとの内部的な扱いの違いを知る必要があります。

PHPでは、主に以下の2つに分類することが可能です。

  1. 基本的なデータ型 (プリミティブ型)
  2. オブジェクトや配列

まず1ですが、数値、真偽値、文字です。これらを変数に格納する場合、変数はその値自体を直接保持します。

 // この場合、$numは直接42という値を持ちます。
$num = 42; 

2はオブジェクトや配列です。これらを変数に格納する場合、その値自体を保持するわけではありません。変数はその値への「参照」をメモリ上に保持します。

つまり変数が直接値そのものを持っているわけではなく、値が実際に保存されているメモリのアドレスを保持しているということです。

// $objはこのオブジェクトのメモリアドレス(参照)を保持します。
$obj = new stdClass();  
Lara
Lara

本で例えれば「本棚の一番上の右から2番目にある」という情報自体を、変数に入れます。モノが大きい&動的に変更される等の理由からでしょう。

コード例

改めて具体的な参照のコピー方法を見てみましょう。

以下の様に、=で代入するだけで、参照のコピーとなります。

$hero1 = new stdClass();
$hero1->name = 'マリオ';

// これが参照のコピー!
$hero2 = $hero1;

$hero2->name = 'ルイージ';

echo $hero1->name; // 出力: ルイージ
echo $hero2->name; // 出力: ルイージ

$hero1$hero2は同じ参照を持っていますので、$hero2->nameの変更が、$hero1->nameにも影響を与えます。

きちんと参照を理解していれば、この動作はとても自然なことです。

関数・メソッドの参照渡しをご存じの方にとっては、それと同じようなもの……として考えると分かりやすいですね。

使い所は?

せっかくなので参照コピーの使い所を考えてみます。

……が、使うシーンがあまり思いつきません。

論理的には、以下のような場合があれば、時に使える可能性があるでしょうか。

  • リソースの無駄を防げる場合:
    →後述する複製ではないため、リソースの無駄を防ぐことができる場合
  • 複数の場所から使う場合:
    →対象となるオブジェクトを、複数の場所から変更する必要があるような場合

ただし私は実務でそういったためしがありませんでしたね。

関数やメソッドでの参照渡しのようなコピーであれば分かりますが、わざわざ=してまでとなると思い当たりません。

いずれにしても、不適切に使用するとコードの複雑さやバグの原因となる可能性があります。利用には注意が必要です。

シャローコピー(浅いコピー)

シャローコピーは、参照のコピーではなく、新しいオブジェクトとしてコピーされます。

しかし、オブジェクト内の他のオブジェクトへの参照はコピーされるだけです。だからシャロー(浅い)コピーというわけですね。

コード例:1

おそらく言葉で伝えるよりも、コードを見た方が早いです。

<?php
// Your code here!
$hero1 = new stdClass();
$hero1->name = 'マリオ';

// これがシャローコピー!
$hero2 = clone $hero1;

$hero2->name = 'ルイージ';

echo $hero1->name; // 出力: マリオ
echo $hero2->name; // 出力: ルイージ

14行目のcloneキーワードがシャローコピーを意味します。

別のオブジェクトとして複製されているので、9行目で$hero2->nameを変更しても、$hero1->nameの名前は変わりません。

もう1点の特徴を説明するために、もう一例ご紹介します。

「オブジェクト内の他のオブジェクトへの参照はコピーになる」というのが分かる例を挙げます。

<?php

class Weapon {
    public $name;
    public $power;
    
    public function __construct($name, $power) {
        $this->name = $name;
        $this->power = $power;
    }
}

class Hero {
    public $name;
    public $hp;
    public $weapon;
    
    public function __construct($name, $hp, $weapon) {
        $this->name = $name;
        $this->hp = $hp;
        $this->weapon = $weapon;
    }
}

$sword = new Weapon('伝説の剣', 50);
$hero1 = new Hero('マリオ', 100, $sword);
$hero2 = clone $hero1; // これがシャローコピーです

echo $hero2->weapon->name;  // 出力: 伝説の剣

$hero2->weapon->name = '魔法の杖';
echo $hero1->weapon->name;  // 出力: 魔法の杖

この例では、Hero クラスが Weapon クラスのインスタンスを持つようにしています。

マリオに「伝説の剣」という武器を持たせ、その後シャローコピーを行った $hero2 の武器名を「魔法の杖」に変更すると、$hero1 の武器名も同時に「魔法の杖」に変わってしまいました。

つまりシャローコピーは、オブジェクトの内部に持つサブオブジェクトに対しては参照コピーとなることを意味します。

Lara
Lara

完全丸々の別物ではない、ということです。

ディープコピー(深いコピー)

シャローコピーは、オブジェクトの内部に持つサブオブジェクトに対しては参照コピーとなりました。ディープコピーは、それさえも複製するコピー方法です。

その名の通り、ディープ(深い)なコピーになります。

コード例

__clone()メソッドに注目して、コードを読んでみてください。

<?php

class Weapon {
    public $name;
    public $power;
    
    public function __construct($name, $power) {
        $this->name = $name;
        $this->power = $power;
    }
}

class Hero {
    public $name;
    public $hp;
    public $weapon;
    
    public function __construct($name, $hp, $weapon) {
        $this->name = $name;
        $this->hp = $hp;
        $this->weapon = $weapon;
    }

    // ここでディープコピーを行います
    public function __clone() {
        $this->weapon = clone $this->weapon;
    }
}

$sword = new Weapon('伝説の剣', 50);
$hero1 = new Hero('マリオ', 100, $sword);
$hero2 = clone $hero1; // これがディープコピーです

echo $hero2->weapon->name;  // 出力: 伝説の剣

$hero2->weapon->name = '魔法の杖';
echo $hero1->weapon->name;  // 出力: 伝説の剣
echo $hero2->weapon->name;  // 出力: 魔法の杖

この例では、Hero クラスの中で __clone メソッドをオーバーライドして、クローン作成時にWeapon サブオブジェクトもクローンするように指定しています。

このため、$hero1$hero2 はそれぞれ独立した Weapon オブジェクトを持っています。その結果$hero2 の武器名を変更しても、$hero1 の武器名は影響を受けません。

特別なPHPのディープコピー方法があるというよりは、自力で実装する……という感じです。全てのクローンが必要とは限らないので、任意のオブジェクトのみクローンして使います。

Lara
Lara

deep_cloneとかあれば簡単なんですけどね。

おまけ:用語の定義

ちょっと難しい話になるので、初心者の方はこの項目は読まなくても大丈夫です。

最後にちょっとしたオマケです。

本ページの分類に疑問を持った方もいるかもしれません。実際にはシャローコピー=参照のコピーという理解を持っている方も少なくないからです。

シャローコピーの定義はさまざま

私も曖昧だったので調べたところ、たしかにそういう解説が多く見られます。大手サイトでも、シャローコピー=参照のコピーと解説されていることがありました。

困った時はマニュアルを……ということで、以下はPHPマニュアルです。

オブジェクトのコピーは、clone キーワード (これは、そのオブジェクトの __clone() メソッドがある場合にこれをコールします)により作成されます。

$copy_of_object = clone $object;

オブジェクトのクローンが作成される際、PHP は、そのオブジェクトのプロパティを 全てシャローコピーします。他の変数へのリファレンスを保持する全てのプロパティは、 リファレンスのままとなります。

PHP: オブジェクトのクローン作成 – Manual

cloneキーワードがシャローコピーとありますし、最後の一文にある件もコピーするのがディープコピーと考えられます。

少なくとも、PHPの公式ではシャローコピーと参照のコピーは別である用に読めます。

ちなみにマニュアルを疑ったわけではありませんが、いろいろ調べたところこの説が濃厚なようです。

PHPに限った話ではなく、言語問わず曖昧に理解・解説されている模様。この辺の事情を知りたい方はこちらの他サイトの記事もご参考ください。

まとめ

以上、3つのオブジェクトのコピーについて解説しました。

再掲します。

  1. 参照のコピー
    参照=リファレンスをコピーする
  2. シャローコピー(浅いコピー)
    新しいオブジェクトを複製するが、その内部オブジェクトは参照のコピー
  3. ディープコピー(深いコピー)
    新しいオブジェクトを複製し、その内部オブジェクトも複製する

どの方法が優秀……ということではありません。大事なのは、適切なタイミングで、適切なコピー方法を採ることです。

そう多いことではないので、忘れてしまうことも多いかもしれませんが……(汗)。

迷った時には、本記事を思い出していただければ幸いです。

Lara
Lara

私もすぐ忘れるので、ことある毎に自分で調べた記事を参照しています。

コメント

タイトルとURLをコピーしました