@nqounetです。
「PerlとMooでモンスター軍団を量産してみよう」シリーズの第4回です。
前回の振り返り
前回は、clone()で作ったオブジェクトの属性を変更して、色違いモンスターを量産する方法を学びました。
今回は、もう少し複雑なモンスターを扱います。装備(武器オブジェクト)を持つモンスターです。ここで思わぬ問題が発覚します。
シリーズ全体の目次は以下をご覧ください。
装備を持つドラゴンを作る
より強力なモンスター「ドラゴン」を作りましょう。ドラゴンは武器を装備しています。
まず、武器を表すWeaponクラスを作ります。
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
| #!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, MooX::Clone(cpanmでインストール)
use v5.36;
# 武器クラス
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 MooX::Clone;
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'); # Weaponオブジェクトを持つ
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 $dragon = Monster->new(
name => 'ドラゴン',
hp => 100,
attack => 20,
defense => 15,
weapon => $fire_sword,
);
$dragon->show_status;
say "総攻撃力: " . $dragon->total_attack;
|
ドラゴンが武器を持っていますね。ここまでは問題ありません。
ドラゴンを複製してみる
このドラゴンをclone()で複製して、それぞれに別の武器を持たせたいとしましょう。
1
2
3
4
5
6
7
8
9
10
11
12
| # ドラゴンを複製
my $dragon2 = $dragon->clone;
# dragon2の武器を「氷の剣」に変えたい!
$dragon2->weapon->name('氷の剣'); # ...あれ、nameはroだからエラー
# 威力だけ変えてみる
$dragon2->weapon->power(20);
say "=== ドラゴン1 ===";
$dragon->show_status;
say "=== ドラゴン2 ===";
$dragon2->show_status;
|
出力結果:
1
2
3
4
5
6
| === ドラゴン1 ===
【ドラゴン】HP:100 攻撃:20 防御:15
武器: 炎の剣 (威力:20) ← あれ?威力が20に変わってる!
=== ドラゴン2 ===
【ドラゴン】HP:100 攻撃:20 防御:15
武器: 炎の剣 (威力:20)
|
おかしい!$dragon2の武器の威力を変えただけなのに、元の$dragonの武器の威力も変わってしまいました。
これが「浅いコピー」の罠
この問題は「浅いコピー(Shallow Copy)」と呼ばれる現象が原因です。
clone()がコピーするのは属性値そのものです。武器属性にはWeaponオブジェクトへのリファレンス(参照)が入っています。浅いコピーでは、このリファレンスがそのままコピーされます。
つまり、$dragonと$dragon2は同じ武器オブジェクトを参照しているのです。
1
2
3
| $dragon ──── weapon属性 ──┬──→ Weapon (炎の剣)
│
$dragon2 ──── weapon属性 ──┘
|
片方から武器を変更すると、もう片方にも影響してしまいます。
問題を可視化する
この問題をより明確に見てみましょう。
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
| #!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, MooX::Clone(cpanmでインストール)
use v5.36;
package Weapon {
use Moo;
has name => (is => 'ro', required => 1);
has power => (is => 'rw', required => 1);
}
package Monster {
use Moo;
use MooX::Clone;
has name => (is => 'ro', required => 1);
has hp => (is => 'rw', required => 1);
has attack => (is => 'rw');
has weapon => (is => 'rw');
sub show_status ($self) {
say "【" . $self->name . "】HP:" . $self->hp;
if ($self->weapon) {
say " 武器: " . $self->weapon->name
. " (威力:" . $self->weapon->power . ")";
}
}
}
# 武器を作成
my $sword = Weapon->new(name => '炎の剣', power => 10);
# ドラゴンを作成
my $dragon1 = Monster->new(
name => 'ドラゴン1',
hp => 100,
attack => 20,
weapon => $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;
# 同じオブジェクトかどうか確認
say "\n=== 同じ武器オブジェクトを参照している? ===";
say "dragon1の武器: " . $dragon1->weapon;
say "dragon2の武器: " . $dragon2->weapon;
say "同じオブジェクト: " . ($dragon1->weapon == $dragon2->weapon ? 'はい' : 'いいえ');
|
出力結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| === 変更前 ===
【ドラゴン1】HP:100
武器: 炎の剣 (威力:10)
【ドラゴン1】HP:100
武器: 炎の剣 (威力:10)
=== dragon2の武器を変更した後 ===
【ドラゴン1】HP:100
武器: 炎の剣 (威力:50) ← 変更していないのに50に!
【ドラゴン1】HP:100
武器: 炎の剣 (威力:50)
=== 同じ武器オブジェクトを参照している? ===
dragon1の武器: Weapon=HASH(0x...)
dragon2の武器: Weapon=HASH(0x...) ← 同じアドレス
同じオブジェクト: はい
|
同じオブジェクトを参照していることが確認できました。これが浅いコピーの問題です。
なぜこうなるのか
MooX::Cloneが行うclone()は、以下のようなイメージです。
1
2
3
4
5
6
7
8
9
| # MooX::Cloneが内部でやっていること(イメージ)
sub clone ($self) {
return Monster->new(
name => $self->name, # 値をコピー(OK)
hp => $self->hp, # 値をコピー(OK)
attack => $self->attack, # 値をコピー(OK)
weapon => $self->weapon, # リファレンスをコピー(問題!)
);
}
|
weapon属性に入っているのはWeaponオブジェクトへのリファレンスです。リファレンスをコピーしても、参照先のオブジェクトは同じままです。
文字列や数値はそのまま値がコピーされるので問題ありませんが、オブジェクトや配列リファレンスは「参照がコピーされる」ため、元のデータを共有してしまうのです。
まとめ
clone()はデフォルトでは「浅いコピー」を行う- 浅いコピーでは、ネストしたオブジェクト(リファレンス)は共有されたまま
- 片方のオブジェクトからネストしたオブジェクトを変更すると、もう片方にも影響する
- これは意図しない副作用を生み、バグの原因になる
次回予告
次回は、この問題を解決する「深いコピー(Deep Copy)」を学びます。Perl標準モジュールのStorable::dclone()を使って、ネストしたオブジェクトも含めて完全に独立したコピーを作る方法を紹介します。お楽しみに。