位元詩人 [Raku] 程式設計教學:繼承 (Inheritance)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

除了組合以外,繼承 (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 { }
關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。