Featured image of post 第5回-Storable::dclone()で深いコピー! - mass-producing-monsters

第5回-Storable::dclone()で深いコピー! - mass-producing-monsters

Storable::dclone()を使って「深いコピー」を実装。ネストしたオブジェクトも完全に独立したコピーになり、浅いコピーの問題を解決!

@nqounetです。

「PerlとMooでモンスター軍団を量産してみよう」シリーズの第5回です。

前回の振り返り

前回は、装備(武器オブジェクト)を持つドラゴンをclone()したとき、武器が共有されてしまう「浅いコピー」の問題を確認しました。

片方のドラゴンの武器を変更すると、もう片方のドラゴンの武器も変わってしまいましたね。今回は、この問題を「深いコピー」で解決します。

シリーズ全体の目次は以下をご覧ください。

深いコピーとは

「深いコピー(Deep Copy)」とは、オブジェクトとその中にネストしたすべてのオブジェクトを、再帰的にコピーすることです。

浅いコピーとの違いを図で見てみましょう。

1
2
3
4
5
6
7
8
【浅いコピー】
$dragon1 ─── weapon属性 ──┬──→ Weapon (炎の剣)
                          │      ↑ 共有されている!
$dragon2 ─── weapon属性 ──┘

【深いコピー】
$dragon1 ─── weapon属性 ────→ Weapon (炎の剣) ← 独立したコピー
$dragon2 ─── weapon属性 ────→ Weapon (炎の剣) ← 独立したコピー

深いコピーでは、ネストしたオブジェクトも含めてすべてがコピーされるため、片方を変更してももう片方には影響しません。

Storable::dclone()を使う

Perlには、深いコピーを行う標準モジュールStorableがあります。Storable::dclone()関数を使うと、データ構造を再帰的に複製できます。

1
2
3
use Storable qw(dclone);

my $copy = dclone($original);

dcloneは「deep clone」の略です。

clone()メソッドを改良する

MooX::Cloneの代わりに、Storable::dclone()を使った独自のclone()メソッドを実装してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, Storable(Perl標準モジュール)

use v5.36;
use Storable qw(dclone);

package Weapon {
    use Moo;

    has name  => (is => 'ro', required => 1);
    has power => (is => 'rw', required => 1);

    sub show ($self) {
        say "武器: " . $self->name . " (威力:" . $self->power . ")";
    }
}

package Monster {
    use Moo;
    use Storable qw(dclone);

    has name    => (is => 'ro', required => 1);
    has hp      => (is => 'rw', required => 1);
    has attack  => (is => 'rw', required => 1);
    has defense => (is => 'rw', required => 1);
    has weapon  => (is => 'rw');

    # 深いコピーを行うclone()メソッド
    sub clone ($self) {
        return dclone($self);
    }

    sub show_status ($self) {
        say "【" . $self->name . "】HP:" . $self->hp
            . " 攻撃:" . $self->attack
            . " 防御:" . $self->defense;
        if ($self->weapon) {
            $self->weapon->show;
        }
    }
}

# 武器を作成
my $fire_sword = Weapon->new(name => '炎の剣', power => 10);

# ドラゴンを作成
my $dragon1 = Monster->new(
    name    => 'ドラゴン',
    hp      => 100,
    attack  => 20,
    defense => 15,
    weapon  => $fire_sword,
);

# 深いコピーで複製
my $dragon2 = $dragon1->clone;

say "=== 変更前 ===";
$dragon1->show_status;
$dragon2->show_status;

# dragon2の武器の威力を変更
$dragon2->weapon->power(50);

say "\n=== dragon2の武器を変更した後 ===";
$dragon1->show_status;  # dragon1には影響なし!
$dragon2->show_status;

出力結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=== 変更前 ===
【ドラゴン】HP:100 攻撃:20 防御:15
武器: 炎の剣 (威力:10)
【ドラゴン】HP:100 攻撃:20 防御:15
武器: 炎の剣 (威力:10)

=== dragon2の武器を変更した後 ===
【ドラゴン】HP:100 攻撃:20 防御:15
武器: 炎の剣 (威力:10)    ← 変わっていない!
【ドラゴン】HP:100 攻撃:20 防御:15
武器: 炎の剣 (威力:50)    ← dragon2だけ変更された

dragon2の武器を変更しても、dragon1には影響がなくなりました。これが深いコピーの効果です。

独立しているか確認する

念のため、武器オブジェクトが別々になっているか確認してみましょう。

1
2
3
4
say "\n=== 別々の武器オブジェクトか確認 ===";
say "dragon1の武器: " . $dragon1->weapon;
say "dragon2の武器: " . $dragon2->weapon;
say "同じオブジェクト: " . ($dragon1->weapon == $dragon2->weapon ? 'はい' : 'いいえ');

出力結果:

1
2
3
4
=== 別々の武器オブジェクトか確認 ===
dragon1の武器: Weapon=HASH(0x7f8b1a...)
dragon2の武器: Weapon=HASH(0x7f8b1b...)    ← 異なるアドレス!
同じオブジェクト: いいえ

武器オブジェクトのアドレスが異なり、別々のオブジェクトになっていることが確認できました。

MooX::Cloneとの使い分け

方法特徴適用場面
MooX::Clone浅いコピー。高速。ネストしたオブジェクトは共有される単純な属性のみのクラス
Storable::dclone()深いコピー。やや低速。ネストしたオブジェクトも完全にコピーオブジェクトをネストしているクラス

シンプルなクラスであればMooX::Cloneで十分ですが、オブジェクトを属性に持つ複雑なクラスではStorable::dclone()を使うほうが安全です。

今回の完成コード

今回の最終的なコードです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, Storable(Perl標準モジュール)
#
# Storable::dclone()で深いコピーを実装
# ネストしたオブジェクトも完全に独立したコピーになる

use v5.36;
use Storable qw(dclone);

package Weapon {
    use Moo;

    has name  => (is => 'ro', required => 1);
    has power => (is => 'rw', required => 1);

    sub show ($self) {
        say "武器: " . $self->name . " (威力:" . $self->power . ")";
    }
}

package Monster {
    use Moo;
    use Storable qw(dclone);

    has name    => (is => 'ro', required => 1);
    has hp      => (is => 'rw', required => 1);
    has attack  => (is => 'rw', required => 1);
    has defense => (is => 'rw', required => 1);
    has weapon  => (is => 'rw');

    # 深いコピーを行うclone()メソッド
    sub clone ($self) {
        return dclone($self);
    }

    sub show_status ($self) {
        say "【" . $self->name . "】HP:" . $self->hp
            . " 攻撃:" . $self->attack
            . " 防御:" . $self->defense;
        if ($self->weapon) {
            $self->weapon->show;
        }
    }

    sub total_attack ($self) {
        my $base = $self->attack;
        my $weapon_power = $self->weapon ? $self->weapon->power : 0;
        return $base + $weapon_power;
    }
}

# 武器を作成
my $fire_sword = Weapon->new(name => '炎の剣', power => 10);

# ドラゴンを作成
my $dragon1 = Monster->new(
    name    => 'ドラゴン',
    hp      => 100,
    attack  => 20,
    defense => 15,
    weapon  => $fire_sword,
);

# 深いコピーで複製
my $dragon2 = $dragon1->clone;

say "=== 変更前 ===";
$dragon1->show_status;
$dragon2->show_status;

# dragon2の武器の威力を変更
$dragon2->weapon->power(50);

say "\n=== dragon2の武器を変更した後 ===";
$dragon1->show_status;
$dragon2->show_status;

# 同一性の確認
say "\n=== 別々の武器オブジェクトか確認 ===";
say "同じオブジェクト: " . ($dragon1->weapon == $dragon2->weapon ? 'はい' : 'いいえ');

まとめ

  • 浅いコピーはネストしたオブジェクトを共有するため、意図しない副作用が起きる
  • 深いコピーはネストしたオブジェクトも再帰的にコピーし、完全に独立した複製を作る
  • Perl標準のStorable::dclone()で深いコピーを簡単に実装できる
  • 複雑なオブジェクト構造を複製する場合は、深いコピーを使うべきである

次回予告

次回は最終回です。これまで作ってきた「既存オブジェクトを複製して新しいオブジェクトを作る」という手法が、実はPrototypeパターンというGoFデザインパターンの一つであることを明かします。お楽しみに。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。