@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デザインパターンの一つであることを明かします。お楽しみに。