除了組合以外,繼承 (inheritance) 也是重覆利用程式碼的一種方法。透過繼承,達到子類型 (subtyping) 的功能,也是實作多型 (polymorphism) 的一種手段;因此,繼承有時會被過度濫用。Go 和 Rust 不約而同拿掉繼承,就是對這個現象的一個反思。Perl 6 仍然保留繼承的特性,也是本文所要介紹的主題。
單一繼承
Perl 6 使用 is
表達繼承關係,如下例:
class Employee {
has Numeric $!_salary;
submethod BUILD(:salary($s)) {
self.salary($s);
}
multi method salary {
$!_salary;
}
multi method salary($s) {
if $s <= 0.0 {
die "Invalid salary";
}
$!_salary = $s;
}
}
class Programmer is Employee {
has Str @!_langs;
has Str @!_editors;
submethod BUILD(:salary($s), :@langs, :@editors) {
self.salary($s);
self.langs(@langs);
self.editors(@editors);
}
multi method langs {
@!_langs;
}
multi method langs(@l) {
@!_langs = @l;
}
multi method editors {
@!_editors;
}
multi method editors(@e) {
@!_editors = @e;
}
method solve(Str $problem) {
my $lang = @!_langs.roll;
my $editor = @!_editors.roll;
"The programmer solved {$problem} in {$lang} with {$editor}".say;
}
}
my $p = Programmer.new(
salary => 100.0,
langs => ("Perl", "Python", "Ruby", "Java", "Go"),
editors => ("Vim", "Emacs", "Atom", "Visual Studio Code"),
);
$p.salary == 100.0 or die "Wrong salary";
$p.solve("Sorting");
$p.solve("Tower of Hanoi");
在這個例子中,Programmer 繼承 Employee 的程式碼,我們不需要重新撰寫有關 salary 部分的代碼。
多重繼承
Perl 6 可以繼承多個物件,理論上,可以寫出類似以下代碼:
class GeometricRetangle is Retangle is Point {
# Implement it here.
}
但是,在 Perl 6 使用多重繼承不是好主意。因為在碰到方法衝突 (不同物件但同名稱的方法) 時,Perl 是依照特定的演算法自動處理,卻無法手動調整;而且,編譯器不會對此發出警告。因此,建議避開這項機制。
註:此演算法是 C3 linearization,有興趣的讀者可自行查閱相關資料。
以下節錄來自 Perl 6 官網的一個反例:
class Bull {
has Bool $.castrated = False;
method steer {
# Turn your bull into a steer
$!castrated = True;
return self;
}
}
class Automobile {
has $.direction;
method steer($!direction) { }
}
class Taurus is Bull is Automobile { }
my $t = Taurus.new;
$t.steer;
在這個例子裡,編譯器不會發出警告,但程式運行可能不如預期。
通常會誤用多重繼承,是對物件導向的思維有些誤解,或是想要達到多型的效果。Perl 6 提供另外一個更好的機制,就是我們下文要介紹的 role。
Role
Role 目前沒有直接對應的中文翻譯,在別的程式語言中,接近介面 (interface) 或是 mixin。Role 的作用在於提供一組公開方法,藉此約束類別的行為。
要繼承 roles,使用 does
。以下例子使用沒有實作程式碼的 role:
role Speak {
method speak { ... }
}
class Duck does Speak {
method speak {
"Pack pack".say;
}
}
class Dog does Speak {
method speak {
"Wow wow".say;
}
}
class Tiger does Speak {
method speak {
"Halum halum".say;
}
}
my Speak @animals = (
Duck.new,
Dog.new,
Tiger.new,
);
for @animals -> $a {
$a.speak;
}
在我們這個例子中,我們刻意在 role Speak
不提供實作,若繼承 Speak
的類別沒有自行實作相對應的 speak
方法,會引發錯誤。透過這種方法來約束類別。
Roles 在碰到方法衝突時,編譯器會引發錯誤,這裡節錄 Perl 6 官網的例子如下:
role Bull-Like {
has Bool $.castrated = False;
method steer {
# Turn your bull into a steer
$!castrated = True;
return self;
}
}
role Steerable {
has Real $.direction;
method steer(Real $d = 0) {
$!direction += $d;
}
}
class Taurus does Bull-Like does Steerable { }
這樣的程式會引發以下錯誤:
===SORRY!===
Method 'steer' must be resolved by class Taurus because it exists in
multiple roles (Steerable, Bull-Like)
可能的解決方法如下:
class Taurus does Bull-Like does Steerable {
method steer($direction?) {
self.Steerable::steer($direction?)
}
}
對照 roles 和繼承,筆者認為,Perl 6 的多重繼承,某種程度上是設計的失誤,應避免使用。
Role 也可以加入一些實作內容,以下節錄 Perl 6 的官網:
use MONKEY-SEE-NO-EVAL;
role Serializable {
method serialize() {
self.perl; # very primitive serialization
}
method deserialize($buf) {
EVAL $buf; # reverse operation to .perl
}
}
class Point does Serializable {
has $.x;
has $.y;
}
my $p = Point.new(:x(1), :y(2));
my $serialized = $p.serialize; # method provided by the role
my $clone-of-p = Point.deserialize($serialized);
say $clone-of-p.x;
EVAL
本身僅能解析字串,對於變數或其他複雜結構則無法處理;第一行的 MONKEY-SEE-NO-EVAL
可以解除此限制。
如果類別,roles 之間也可以繼承,如以下例子:
role R1 {
# methods here
}
role R2 does R1 {
# methods here
}
class C does R2 { }