OOP(3) 継承
パッケージ変数である @ISA 配列に、親となるクラス(パッケージ)の名前を入れておけば、そのクラスを継承した事になります。(is-a の関係)
メソッドが呼び出された時、そのメソッドが見つからなければ @ISA 配列の中の親クラスが持っていないか探してくれます。親クラスに見つからなければ、さらに親の親クラス……と再帰的にたどっていくので、継承のツリーができますね。
ちなみに、@ISA は配列なので複数のクラスを指定できます。すると、多重継承した事になります……が、僕はあまり多重継承が好きではないので、「できる」という事だけ覚えておきますね。
では、早速クラス継承のコードを書いてみます。
package Bird;
sub new {
my $class = shift;
return bless {}, $class;
}
sub sing {
print "Cheep cheep...\n";
}
package Cock;
our @ISA = ("Bird");
sub sing {
print "Cock-a-doodle-doo!\n";
}
package main;
$abird = Bird->new();
$abird->sing();
$acock = Cock->new();
$acock->sing();
以下が実行結果です。
Cheep cheep... Cock-a-doodle-doo!
@Cock::ISA の中に "Bird" を加えているので、Cock クラスは Bird クラスを継承しています(Cock is-a Bird)。
sing メソッドで鳴きますが、Cock 側で sing メソッドをオーバーライド(メソッドの上書き)しているので、鳴き声が変わっているのがわかります。
ポイントはコンストラクタ(new)です。クラスメソッドの場合、第一引数にはクラス名が渡されるので、そのクラス名を使って bless しています。
実は、最初は bless {}; と、クラス名を書かずに bless していました。
sub new { return bless {}; }
すると
Cheep cheep... Cheep cheep...
と $acook も Bird のオブジェクトになってしまうのです。(-_-;)
メソッド呼び出しの動きをよく考えると、原因がわかりました。
コンストラクタは Bird クラスにしか書いていないので、Cock->new() を呼び出すと、@ISA をたどって Bird クラスの new メソッド(Bird::new)が呼ばれる事になります。
そして Bird::new の中で bless {}; したリファレンスを返しているわけですが、bless する時にパッケージ名を省略したのが間違いでした。
bless のパッケージ名を省略すると、カレントパッケージが使われる事は昨日勉強しました。Bird::new の中ではカレントパッケージは Bird ですので、bless {}; と呼び出すと Bird パッケージが bless されてしまう事になります。
すると、Bird->new() しても Cock->new() しても、Bird オブジェクトが返ってきてしまうのです。これでは、鳴き声が同じになってしまいますね。
Cock->new(); Bird->new(); ↓(@ISAをたどる) ↓ Bird::new("Cock"); Bird::new("Bird"); ↓ ↓ bless {}; bless {}; ↓ ↓ Bird オブジェクト Bird オブジェクト
そこで、渡されたクラス名($class)を使って bless する事で、メソッドの呼び出しに使われたパッケージに従ったオブジェクトを生成する事ができるようになります。
Cock->new(); Bird->new(); ↓(@ISAをたどる) ↓ Bird::new("Cock"); Bird::new("Bird"); ↓ ↓ $class = "Cock"; $class = "Bird"; ↓ ↓ bless {}, $class; bless {}, $class; ↓ ↓ Cock オブジェクト Bird オブジェクト
こんな感じでしょうか。
失敗しましたが、継承の機構が理解できたので逆に良かったです。
それにしても、ただのクラス名の配列が動的な継承ツリーになるのは面白いです。スクリプト言語の Perl ならではですね。シンプルでわかりやすいです。