【在主画面加入捷径】
       
【选择语系】
繁中 简中

Perl 6 程序设计教学:继承 (Inheritance)

【赞助商连结】

    除了组合以外,继承 (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 { }
    
    【赞助商连结】