[译][Perl 6] Object Orientation

About

  • Perl 6 document

  • 翻译

  • 本翻译意在帮助理解原文,因个人水平有限,难免会有翻译不当,如果疑惑请在评论中指出。

Translation

原文

Perl 6 中的面向对象

Perl 6 从本质上讲是一门面向对象语言,虽然它允许你编写代码时使用其他编程风格。
Perl 6 提供了丰富的预定义类型,它们可以分为两大类:普通类型以及原生类型。
原生类型被用作低级类型(比如uint16),它们并没有普通变量拥有的功能,但是如果你在他们身上调用方法,它们会包装成普通对象。
所有 你能存储在变量里面的东西除了原生类型就是对象,原生类型包括字面量,类型(类型对象),代码,以及容器。

使用对象

你可以通过调用方法来使用对象,在一个表达式中调用一个方法,在对象后添加一个点,紧跟着是方法名字:

say "abc".uc; ### 输出: ABC

上面的代码在"abc"上调用了方法uc"abc"是类型Str的一个对象。如果你想给方法提供参数,在方法之后添加一对括号,参数放在括号里面即可:

my $formatted-text = "Fourscore and seven years ago...".indent(8);

$formatted-text现在包含了上面的文本,但是缩进了8个空格。
如果想传递多个参数,只需要逗号隔开多个参数:

my @words = "Abe", "Lincoln";
@words.push("said", $formatted-text.comb(/\w+/));

另外一种调用方法的语法就是在方法名和参数列表之间用冒号隔开:

say @words.join: '--'; ### 输出: Abe--Lincoln--said--Fourscore--and--seven--years--ago

因此如果你想在传递参数的时候省略括号,你就需要在方法后面添加:,一个方法调用如果没有冒号或者括号被当作没有参数列表的方法调用:

say 4.log: ;     ### 1.38629436111989 ( 4的自然对数 )
say 4.log: +2;    ### 2                ( 4的以2为底的对数 )
say 4.log +2;    ### 3.38629436111989 ( 4的自然对数,然后加上2 )

许多看起来不像是方法调用的操作(比如智能匹配,或者在字符串里面内插变量)会导致方法调用。
方法可能会返回可修改的容器,在这种情况下你可以对方法调用的返回值进行赋值操作,这也是对象的可读写属性的使用方法:

$*IN.nl-in = "\r\n";

在上面代码中,我们调用了对象$*INnl-in无参方法,然后我们使用=运算符给返回的容器赋值。
所有的对象支持来自类Mu的方法,它是类型层次的根,换句话说,所有的类都继承自Mu。

类型对象

类型本身也是对象,你可以使用它们的名字获取类型对象

my $int-type-obj = Int;

你可以通过使用WHAT方法查询其他对象的类型(这实际上是一个宏方法形式):

my $int-type-obj = 1.WHAT;

类型对象(不同于Mu)可以使用===运算符比较是否相等:

sub f(Int $x) {
    if $x.WHAT === Int {
        say "you passed an Int";
    } else {
        say "you passed a subtype of Int";
    }
}

不过,在大多数情况下,.isa方法就足够了:

sub f($x) {
    if $x.isa(Int) {
        ...
    }
    ...
}

子类型检测可以通过使用智能匹配:

if $type ~~ Real {
    say '$type contains Real or subtype thereof';
}

类使用class关键字声明,通常跟着一个名字:

class Journey { }

(???)声明的结果就是一个类型对象被创建,名字Journey在当前lexical scope可用。你还可以声明一个词法作用域的类:

my class Journey { }

这强制类只在当前词法作用域可见,这在当某个类只是另一个类或者模块的实现细节的时候很有用。

属性

属性是存在于每一个类的实例中的变量,对象的状态存储即存储在它们那里。在 Perl 6 里面,所有的属性都是私有的,他们的声明通常使用has说明符和 twigil !

class Journey {
    has $!origin;
    has $!destination;
    has @!travellers;
    has $!notes;
}

同时并没有用来声明公共的(甚至是保护的)属性的那种东西,不过有一种方式可以自动的生成属性的 accessor 方法:使用 twigil . 替代!.可以提醒你生成了方法调用)。

class Journey {
    has $.origin;
    has $.destination;
    has @!travellers;
    has $.notes;
}

默认状态下是提供只读 accessor,为了允许对属性改变,加上is rw

class Journey {
    has $.origin;
    has $.destination;
    has @!travellers;
    has $.notes is rw;
}

现在,当一个Journey对象被创建时,它的.origin.destination,以及.notes属性都可以从类的外部访问,但是只有.notes是可以修改的。
上面的声明允许不指定.origin以及.destination的值创建Journey对象,对此你可以提供一个默认值或者把它标记为is required确保它被提供。

class Journey {
    has $.origin is required;    # 当origin没有提供值时会产生编译错误
    has $.destination = self.origin eq 'Orlando' ?? 'Kampala' !! 'Orlando';     # 把Orlando设置为默认值(除非出发点(origin)是Orlando)
    has @!travellers;
    has $.notes is rw;
}

因为类会从Mu继承到一个默认的构造方法,加上我们请求生成的 accessor 方法,我们的类已经可以用了。

# 创建一个类实例
my $vacation = Journey.new(
    origin        => 'Sweden',
    destination    => 'Switzerland',
    notes        => 'Pack hiking gear!'
);

# 使用存取器;这将会输出“Sweden”。
say $vacation.origin;

# 使用读写存取器改变属性的值
$vacation.notes = 'Pack hiking gear and sunglasses!';

注意默认的 constructor 只会设置拥有 accessor 方法的属性(私有的属性没有 accessor,设置值也没有用,但是这不会报错),但是可以初始化只读属性的值。

方法

使用method关键字在类的内部声明一个方法。

class Journey {
    has $.origin;
    has $.destination;
    has @!travellers;
    has $.notes is rw;

    method add_traveller($name) {
        if $name ne any(@!travellers) {
            push @!travellers, $name;
        } else {
            warn "$name is already going on the journey!";
        }
    }

    method describe() {
        "From $!origin to $!destination";
    }
}

一个方法可以拥有 signature,就像子例程一样。属性可以在方法中使用,并且总是可以用 twigil !来访问,即使它们是使用的 twigil . 。这其实是因为,twigil . 是 twigil ! 和额外生成的 accessor 方法的组合。
查看上面的代码,在方法describe使用$!origin$.origin之间有一些细微的差别,前者总是简单的查找属性,代价低廉,并且你能清楚它是这个类中声明的属性;后者其实是一个方法调用,而且有可能会被子类重写,所以,只有你想显式的允许重写时使用$.origin

self

在一个方法内,self是可用的,它将会绑定到 invocant,也就是说方法调用作用的对象。使用self可以进一步的调用 invocant 的方法,对于方法,$.originself.origin有着相同的效果。

私有方法

完全地在类内部使用,不会在其他任何地方调用的方法声明的时候在方法名前面加上一个感叹号标记!即可,调用它们时候使用感叹号替代点:

method !do-something-private($x) {
    ...
}
method public($x) {
    if self.precondition {
        self!do-something-private(2 * $x)
    }
}

私有方法将不会被子类继承。

子方法

一个子方法是不会被子类继承的公共方法,这个名字源于它们的语义类似与子例程的事实。
子方法对于对象 construction 以及 destruction 很有用,因为这些任务总是特定于特定类型,子类型一定要重写它们。
比如default method new继承层次上调用了子方法BUILD

class Point2D {
    has $.x;
    has $.y;

    submethod BUILD(:$!x, :$!y) {
        say "Initializing Point2D";
    }
}

class InvertiblePoint2D is Point2D {
    submethod BUILD() {
        sub "Initializing InvertiblePoint2D";
    }

    method invert {
        self.new(x => - $.x, y => - $.y);
    }
}

say InvertiblePoint2D.new(x => 1, y => 2);

这将会产生以下输出:

Initializing Point2D
Initializing InvertiblePoint2D
InvertiblePoint2D.new(x => 1, y => 2);

参照:Object Construction

继承

一个类可以有父类

class Child is Parent1 is Parent2 { }

如果在子类(的对象)上调用一个方法,但是子类却没有提供那个方法,如果其中一个父类中存在叫那个名字的方法,就会调用父类的方法。在这里,父类会被考虑的顺序被叫做method resolution order(MRO),Perl 6使用了C3 method resolution order,你可以通过调用一个类型的元类型的方法获取它的MRO:

say List<^mro>;  # 将会输出 List() Cool() Any() Mu()

如果一个类没有指定父类,那么默认就是Any, 所有的类直接或者间接的继承自类型层次的根类型Mu
所有的公共函数的调用都看起来像是c++中“虚函数”调用,这意味着对象的实际类型决定需要调用的方法,而不是声明类型。

class Parent {
    method frob {
        say "the parent class frobs"
    }
}

class Child is Parent {
    method frob {
        say "the child's somewhat more fancy frob is called"
    }
}

my Parent $test;

$test = Child.new;
$test.frob; # 这将会调用Child的frob方法而不是Parent

这将会产生下列输出:

the child's somewhat more fancy frob is called

对象构造

对象的创建通常都是通过方法调用,创建一个类型对象或者相同类型的另一个对象。
Mu提供了一个 constructor 方法叫做new,它接受命名参数用来初始化公共属性。

class Point {
    has $.x;
    has $.y = 2 * $!x;
}

my $p = Point.new(x = > 5, y => 2);
#              ^^^ 继承自Mu
say "x: ", $p.x;
say "y: ", $p.y;

这将会输出:

x:5
y:2
my $p2 = Point.new(x => 5); # y的值将会根据给定的x计算出来

say "x: ", $p.x;
say "y: ", $p.y;

这将会输出:

x:5
y:10

Mu.new调用 invocant 的bless方法,传递给它所有的命名参数,bless创建一个新的对象,然后调用BUILDALL函数。BUILDALL以MRO相反的顺序遍历所有的子类(也就是说从Mu到所有继承的类),并在每一个类中检查有没有一个叫做BUILD的方法,如果该方法存在则调用,再传递给它来自new方法的所有命名参数;如果该方法不存在,所有公共属性都会根据对应的(名字相同的命名参数)命名参数的值初始化。在这两种情况下,如果BUILD以及默认机制都没有初始化的属性,将会使用默认值(比如上面例子中的2 * $!x)。
得益于BUILDALL的默认行为和自定义的BUILD子方法,传递给从Mu继承而来的new方法的命名参数可以直接对应MRO上的任何类的公共属性,或者BUILD方法的任何命名参数。
这个对象构造方案对于自定义构造有这么几个影响。首先,自定义的BUILD方法必须总是子方法,否则它会打破子类属性初始化的顺序;其次,BUILD子方法可以用来在对象构造期间运行自己的代码,它们也可以用来创建用来初始化属性的别名:

class EncodedBuffer {
    has $.enc;
    has $.data;

    submethod BUILD(:encoding(:$enc), :$data) {
        $!enc     := $enc;
        $!data    := $data;
    }
}

my $eb1 = EncodedBuffer.new(encoding     => 'utf8', data => [64, 65]);
my $eb2 = EncodedBuffer.new(enc         => 'utf8', data => [64, 65]);
#                             ''' 现在enc、encoding你都可以使用

因为向例程传递参数就是把实参绑定在形参上面,用属性作为形参可以省略不必要的步骤,因此上面的例子可以被改写成:

submethod BUILD(:$encoding(:$!enc), :$!data) {
    # 这里不需要做多余的事情了
    # 函数签名绑定为我们做了所有的事情
}

第三,如果你想要 constructor 接受位置参数,你必须重写new方法:

class Point {
    has $.x;
    has $.y;

    method new($x, $y) {
        self.bless(:$x, :$y);
    }
}

然而,这是一个不恰当的做法,因为它使得初始化来自子类的对象变的更加困难。
你需要注意的另一件事情是new这个名字在 Perl 6 里面并不特别(比如关键字之类的东西就算特别),它仅仅是一个共同的约定,你可以在任何的方法中调用bless函数,或者玩弄低级的方法CREATE
另一种偷换对象创建(hooking into object creation)的方法是编写你自己的BUILDALL方法,为了确保父类的初始化正常工作,你还必须使用callsame调用父类的BUILDALL方法。

class MyClass {
    method BUILDALL(|) {
        # 在这里添加你的初始化

        callsame; # 调用父类的BUILDALL

        # 做进一步的检查
    }
}

对象克隆

所有类的父类Mu提供了一个奇妙的名为clone的方法,它可以通过拷贝实例的私有属性的值创建一个新的实例。拷贝是浅拷贝,因为它只是把源实例属性的值绑定到新的属性上,而没有进行值的拷贝。
正如new提供公共属性的初始值,clone使用源实例的值覆盖初始值。(参见文档中关于Mu的clone的例子)
需要注意的是clone并不是一个submethod,一个类的提供了自身了clone就会替换掉Mu的方法。这里也没有像BUILDALL一样的自动机制,比如,如果你想为一个特定的类做深拷贝,你可能需要调用callwith或者nextwith对父类进行深拷贝。

class A {
    has $.a;
    # ...
    method clone {
        nextwith(:a($a.clone));
    }
}

这在简单的类上能很好的工作,但是某些情况下可能需要效仿BUILDALL,在MRO上进行拷贝工作。

class B is A {
    has $.b;
    # ...
    method clone {
        my $obj = callsame;
        $obj.b = $!b.clone(:seed($obj.a.generate_seed));
        $obj;
    }
}

Roles

Roles 在某些方面上类似与类,它们都是属性和方法的集合,不同的是 roles 也可以用来描述部分对象的行为,还有 roles 如何应用到类上,换句话说,类是用来管理实例的,roles 是用来管理行为以及代码重用的。

role Serializable {
    method serialize() {
        self.perl; # 非常原始的序列化
    }
    method deserialize($buf) {
        EVAL $buf; # 相对与.perl的反向操作
    }
}

class Point is Serializable {
    has $.x;
    has $.y;
}

my $p = Point.new(:x(1), :y(2));
my $serialized = $p.serialize;    # role 提供的方法
my $clone-of-p = Point.deserialize($serialized);

say $clone-of-p.x; # 将会输出 1

一旦编译器解析到了 role 声明的关闭括号,role 就不再是可变的了。

role运用

role 的运用完全不同与类的继承,当一个 role 应用于一个类的时候, role 的方法就会拷贝到类里面,如果有多个 role 应用到同一个类,冲突(比如 attribute 或者非重载方法重名)将会产生一个编译错误,解决方案是在类中提供一个相同名字的方法。
这比多继承安全的多,多继承编译器永远不会检测到冲突,只是简单的使用MRO出现最早的父类的方法,从而可能违背了程序员的本意。
比如,你发现了一种有效的方法骑牛,你尝试市场化它作为一种新的流行的交通工具,你可能有一个类Bull,代表你家里的牛,还有一个类Automobile,代表你可以驾驶的车。

class Bull {
    has Bool $.castrated = False;
    method steer {
        # 阉割你的牛,让它变的更加容易操控
        $!castrated = True;
        return self;
    }
}
class Automobile {
    has $.direction;
    method steer($!direction) {

    }
}

class Taurus is Bull is Automobile { }

my $t = Taurus.new();

$t.steer; # 阉割 $t

使用这种设置,你可怜的客户可能发现他们无法控制他们的金牛座,而你就不能出售更多的产品,这种情况下, 使用 role 是较好的:

role Bull-Like {
        has Bool $.castrated = False;
    method 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?);
    }
}

当一个 role 被应用到第二个 role ,实际的运用会推迟直到第二个类应用到类上面,此时 role 会应用到了类上面,因此:

role R1 {
    # ...
}
role R2 does R1 {
    # ...
}
class C does R2 { }

这其实相当于

role R1 {
    # ...
}
role R2 {
    # ...
}
class C does R1 does R2 { }

存根(Stubs)

当一个 role 包含一个方法存根的时候, role 应用的类必须提供一个名字相同的非存根版本的方法,这允许你创建行为像抽象接口的 role 。

role AbstractSerializable {
    method serialize() { ... } # 字面量 ... 标记这个方法为存根
}

# 下面的代码产生一个编译错误,比如
#        Method 'serialize' must be implemented by Point because
#        it is required by a role
class APoint does AbstractSerializable {
    has $.x;
    has $.y;
}

# 下面的代码是正常的
class SPoint does AbstractSerializable {
    has $.x;
    has $.y;
    method serialize() { "p($.x, $.y)" }
}

存根方法的实现可能由另一个 role 提供。

自动实例化 Roles(Auto Punning Roles)

任何对 role 实例化(或者其他类似的用法)的尝试都会自动的创建一个和 role 有着相同名字的类,使其可以透明的像类一样使用一个 role 。

role Point {
    has $.x;
    has $.y;
    method abs { sqrt($.x * $.x + $.y * $.y) }
}
say Point.new(x => 6, y => 8).abs;

我们管这个自动创建类过程叫做punning, 生成的类叫做pun

参数化Roles

Roles 可以使用参数,在方括号里面给出签名:

role BinaryTree[::Type] {
    has BinaryTree[Type] $.left;
    has BinaryTree[Type] $.right;
    has Type $.node;

    method visit-preorder(&cb) {
        cb $.node;
        for $.left, $.right -> $branch {
            $branch.visit-preorder(&cb) if defined $branch;
        }
    }

    method visit-postorder(&cb) {
        for $.left, $.right -> $branch {
            $branch.visit-postorder(&cb) if defined $branch;
        }
        cb $.node;
    }

    method new-from-list(::?CLASS:U: *@el) {
        my $middle-index     = @el.elems div 2;
        my @left            = @el[0 .. $middle-index - 1];
        my $middle            = @el[$middle-index];
        my @right            = @el[$middle-index + 1 .. * - 1];
        self.new(
            node     => $middle,
            left     => @left ?? self.new-from-list(@left) !! self,
            right    => @right ?? self.new-from-list(@right) !! self,
        );
    }
}

my $t = BinaryTree[Int].new-from-list(4, 5, 6);

$t.visit-preorder(&say); # 输出 5 \n 4 \n 6
$t.visit-postorder(&say); # 输出 4 \n 5 \n 6

上面的签名只是由类型捕获组成的,其实可以是任何参数:

use v6;

enum Severity < debug info warn error critical >;

role Logging[$filehandle = $*ERR] {
    method log(Severity $sev, $message) {
        $filehandle.print("[{uc $sev}] $message\n");
    }
}

Logging[$*OUT].log(debug, "here we go!"); # 输出 [DEBUG] here we go!

你可以定义多个相同名字的 roles ,他们拥有不同的签名,这也是重载的分派时选择重载对象的正常规则。

Roles的混入

Roles 可以混入到对象中, roles 将自身的属性和方法添加为对象的属性和方法,多混入和匿名 roles 也是支持的。

role R { method Str() { 'hidden'} };
my $i = 2 but R;
sub f(\bound) { put bound };

f($i); # hidden!

注意,是对象被 role 混入,而不是对象的类或者容器,(???)因此 @-sigil 容器需要绑定到 role ,有些操作会返回一个新的值,导致混入被剥夺。
混入可以发生在对象生命期的任何时候:

# 一个目录计数器
role TOC-Counter {
    has Int @!counters is default(0);
    method Str() { @!counters.join('.') }
    method inc($level) {
        @!counters[$level - 1]++;
        @!counters.splice($level);
        self
    }
}

my Num $toc-counter = NaN; # 不要做数学运算
say $toc-counter; # 输出 NaN
$toc-counter does TOC-Counter; # 现在我们将 role 混入
$toc-counter.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);
put $toc-counter / 1; # 输出 NaN (因为这里是数值上下文)
put $toc-counter; # 输出 2.2.2 (put 将会调用TOC-Counter::Str)

Roles 可以是匿名的:

my %seen of Int is default(0 but role :: { method Str() { 'NULL' } });
say %seen<not-there>; # 输出 NULL
say %seen<not-there>.defined; # 输出 True (0有可能是False但是却定义了)
say Int.new(%seen<not-there>); # 输出 0

元对象编程以及内省

Perl 6 拥有一个元对象系统,它的意思是对象、类、 role 、grammars、枚举等的行为均是通过其它对象控制的,这些对象称为元对象。元对象,就像普通的对象,也是类的实例,不过在这种情况下类叫做元类
对于每一个对象或者类,你都可以通过调用.HOW方法获取它们的元对象,注意虽然这看起来像是一个方法调用,但其实对编译器来说是特殊情况,所以它其实更像 macro。
所以,你该怎么样使用元对象呢?一个用法就是你可以通过比较元对象是否相等检测它们是否有用相同的元类。

say 1.HOW === 2.HOW;    # 输出 True
say 1.HOW === Int.HOW;    # 输出 True
say 1.HOW === Num.HOW;     # 输出 False

Perl 6 使用这个单词HOW, Higher Order Workings,指代元对象系统。因此,不必惊讶于Rakudo里面,控制类行为的元类的类名被叫做Perl6::Metamodel::ClassHow,对于每一个类都有一个Perl6::Metamodel::ClassHOW的实例。
当然元模型可以为你做更多,比如它支持对象以及类的内省,元对象的调用约定是,调用元对象的方法,将在意的对象作为第一个参数传入。所以要获取实例的类的名字,你可以这么写:

my $object = 1;
my $metaobject = 1.HOW;
say $metaobject.name($object); # 输出 Int

# 简短的方式
say 1.HOW.name(1); # 输出 Int

(Perl 6 这么做的动机是想拥有一个基于原型的对象系统(prototype-based object system),这样没有必要为每一个类创建一个新的元对象)。
可以使用下面的方式摆脱使用两次相同对象的麻烦,现在代码更简短了:

say 1.^name; # 输出 Int
# 相当与
say 1.HOW.name(1); # 输出 Int

参见关于类的元类的文档,以及关于元对象协议的文档

    原文作者:araraloren
    原文地址: https://segmentfault.com/a/1190000004679283
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞