面向对象的 Perl 6

Object Orientation in Perl 6

Perl 6 有很多预先定义好的类型,这些类型可以归为 2 类:普通类型原生类型。原生类型用于底层类型(例如 uint 64)。原生类型没有和对象同样的功能,尽管你可以在它们身上调用方法, 它们还是被包装成普通的对象。所有你能存储到变量中的东西要么是一个原生的 value, 要么是一个对象。这包括字面值、类型(类型对象)、code 和容器。

使用对象


方法可以有参数, 但是方法名和参数列表之间不可以有空格:

say "abc".uc;                   
#        ^^^ 不带参数的方法调用
my @words = $string.comb(/\w+/);
#                  ^^^^^^^^^^^^ 带一个参数的方法调用

另外一种方法调用的语法将方法名和参数列表用一个冒号分开(冒号紧跟方法名, 中间不能有空格):

say @*INC.join: ':';

方法能返回一个可变容器, 这种情况下 你可以赋值给方法调用的返回值.

$*IN.input-line-separator = "\r\n";

类型对象


Types本身就是对象 ,你可以使用类型的名字获取 type object :

my $int-type-obj = Int;

你可以通过调用 WHAT 方法查看任何对象的 type object(它实际上是一个方法形式的macro):

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

使用 === 操作符可以比较 类型对象的相等性:

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

子类型可以使用 smart-matching来检查:

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


使用 class 关键字进行类的定义:

class Journey {
}

声明一个词法作用域的类:

my class Journey {
}

这在嵌套类中很有用。

属性


属性存在于每个类的实例中。属性中存储着对象的状态。在 Perl 6 中, 一切属性都是私有的. 它们一般使用 has 关键字和 ! twigil 进行声明.

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

然而, 没有像这样的公共(甚至保护属性)属性, 不过有一种方式能自动生成访问方法: 使用 .代替 ! twigil 。(那个 . 应该让你想起了方法调用).

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

这默认提供了一种只读的取值方法, 为了允许更改属性, 要添加 is rw 特性:

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

因为类默认继承于构造器 Mu, 我们也要求类为我们生成一些存取方法.

# 创建一个新的类的实例.
my $vacation = Journey.new(
    origin      => 'Sweden',
    destination => 'Switzerland',
    notes       => 'Pack hiking gear!'
);
# 使用存取器; 这打印出 Sweden.
say $vacation.origin;
# 使用 rw 存取器来更改属性的值.
$vacation.notes = 'Pack hiking gear and sunglasses!';

注意, 默认的构造器只会设置含有存取器方法的属性.

方法


使用 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"
    }
}

方法可以有签名, 就像子例程一样。 方法中能访问对象的属性, 并且总是能使用 ! twigil, 即使属性是用 . twigil 声明的. 这是因为, . twigil 是在那个位置上使用 ! twigil 声明了属性, 然后额外又添加了一个取值器方法.

has $.attribute 等价于:

    has $!attribute
    method attribute() { ... }
class A {    
    has $.attr is rw;
}

等价于:

class A {    
    has $!attr;    
    method attr() is rw {
        $!attr;
    }
}

在 describe 方法中使用 $!origin 和 $.origin ,这之间有一个微小但很重要的差别. $!origin 只是属性的简单查看. 它是廉价的, 并且你知道它是类中声明的属性. $.origin 真正的是一个方法调用, 因此能在子类中被覆写. 如果你真的显式地要覆写它才使用 $.origin 吧.

self


在方法内部, self 是可用的, 它被绑定到调用者, 例如方法调用的对象. self 能用于在调用者上调用深层的方法, 例如:

私有方法


在方法的名字前面引入一个感叹号, 这个方法就变为类的私有方法, 这个方法只在内的内部使用, 不能在其它任何地方调用.

私有方法的调用要使用感叹号而非点号:

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

私有方法不能被子类继承.

子方法


submethod 是不会被子类继承的公开方法。从词干名来看它们在语义上与子例程类似。

Submethods 对于对象构建和解构任务很有用。

继承


类可以有父类:

class Child is Parent1 is Parent2 { }

如果在子类中调用一个方法, 但是子类没有提供那个方法, 就会调用父类中同名的方法, 如果父类中存在那个方法的话. 父类被询问的顺序就叫做方法解析顺序(MRO). Perl 6 使用 C3 方法解析顺序. 你可以通过调用一个类型的元类型方法得知这个类型的 MRO.

say Parcel.^mro;    # Parcel() Cool() Any() Mu()

如果一个类没有指定它的父类, 就假定默认为 Any. 所有的类都直接或间接的派生于 Mu-类型层级的根.

对象构造


对象通常通过方法调用创建, 或者通过类型对象或者通过同类型的其它对象创建. 类 Mu 提供了一个叫做 new 的构造器方法, 这个方法接收命名参数然后使用它们来初始化公共属性.

class Point {
    has $.x;
    has $.y = 2 * $!x;
}
my $p = Point.new( x => 1, y => 2);
#             ^^^ 继承自类 Mu

Mu.new 在调用者身上调用 bless 方法, 传递所有的具名参数. bless 创建新的对象, 然后调用该对象的 BUILDALL 方法. BUILDALL相反的方法解析顺序(继承层级树自上而下)遍历所有子类(例如, 从 Mu 到 派生类), 并且在每个类中检查名为 BUILD 的方法是否存在。 如果存在就调用它, 再把传递给 new 方法的所有具名参数传递给这个 BUILD 方法。 如果没有, 这个类的公开属性就会用同名的具名参数进行初始化. 这两种情况下, 如果 BULID 方法和 默认构造函数 都没有对属性进行初始化, 就会应用默认值 (上面例子中的 2 * $!x)。

这种构造模式对于自定义构造器有几处暗示. 首先, 自定义 BUILD 方法应该总是子方法(submethod), 否则它们会中断子类中的属性初始化. 第二, BUILD 子方法能用于在对象构造时执行自定义代码. 它们也能用于为属性初始化创建别名:

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

    submethod BUILD(:encoding(:$enc), :$data) {
        $!enc  := $enc;
        $!data := $data;
    }
}
my $b1 = EncodedBuffer.new( encoding => 'UTF-8', data => [64, 65] );
my $b2 = EncodedBuffer.new( enc      => 'UTF-8', data => [64, 65] );
#  现在 enc 和 encoding 都被允许

因为传递实参给子例程把实参绑定给了形参, 如果把属性用作形参,单独绑定那一步就不需要了. 所以上面的例子可以写为:

submethod BUILD(:encoding(:$!enc), :$!data) {
    # nothing to do here anymore, the signature binding
    # does all the work for us.
}

第三个暗示是如果你想要一个接收位置参数的构造函数, 你必须自己写 new 方法:

class Point {
    has $.x;
    has $.y;
    method new($x, $y) {
        self.bless(*, :$x, :$y);
    }
}

然而, 这不是最佳实践, 因为这让来自子类的对象的初始化正确更难了.

Roles


Roles 在某种程度上和类相似, 它们都是属性和方法的集合. 不同之处在于, roles 是用来描述对象行为的某一部分的, 和 roles 怎样应用于类中. 或怎样解析。 类用于管理对象实例, 而 roles 用于管理行为代码复用

role Serializable {
    method serialize() {
        self.perl; # 很粗超的序列化
    }
    method deserialization-code($buf) {
        EVAL $buf; #  反转 .perl 操作
    }
}

class Point does Serializable {
    has $.x;
    has $.y;
}
my $p = Point.new(:x(1), :y(2));
my $serialized = $p.serialize;      # 由 role 提供的方法
my $clone-of-p = Point.deserialization-code($serialized);
say $clone-of-p.x;      # 1

编译器一解析到 role 声明的闭合花括号, roles 就不可变了。

Role Application


Role 应用和类继承有重大不同。 当 role 应用到类中时, 那个 role 的方法被复制到类中。如果多个 roles 被应用到同一个类中, 冲突( 例如同名的非 multi 方法(s) )会导致编译时错误, 这可以通过在类中提供一个同名的方法来解决冲突。
这比多重继承更安全, 在冲突从来不会被编译器检测到的地方, 但是代替的是借助于在 MRO 中出现更早的父类, 这可能是也可能不是程序员想要的。

当一个 role 被应用到第二个 role上, 实际的程序被延迟直到第二个 role 被应用到类, 这时两个 roles 才都被应用到那个类中。 因此:

role R1 {
    # methods here
}
role R2 does R1 {
    # methods here
}
class C does R2 { }

等价于:

role R1 {
    # methods here
}
role R2 {
    # methods here
}
class C does R2 does R1 { }

Stubs


当 role 中包含了一个 stubbed 方法, 在这个 role 被应用到类中时, 必须提供一个同名的非 stubbed 版本的方法。这允许你创建如抽象接口那样的 roles。这有点像 Swift 中的 Protocol 协议。

role AbstractSerializable {
    method serialize() { ... }  # 字面的三个点 ... 把方法标记为 stub
}

#  下面是一个编译时错误, 例如
#        Method 'serialize' must be implemented by APoint 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)" }
}

那个 stubbed 方法的实现也可能由另外一个 role 提供。

TODO: 参数化的 roles

元对象编程和自省


Perl 6 有一个元对象系统, 这意味着对象,类,roles,grammars,enums 它们自身的行为都被其它对象控制; 那些对象叫做元对象(想想元操作符, 它操作的对象是普通操作符). 元对象, 像普通对象一样, 是类的实例, 这时我们称它们为元类.

对每个对象或类, 你能通过调用 .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
# or shorter:
say 1.HOW.name(1);                  # Int

为了避免使用同一个对象两次, 有一个便捷写法:

say 1.^name;                        # Int
# same as
say 1.HOW.name(1);                  # Int

内省


内省就是在运行时获取对象或类的信息的过程. 在 Perl 6 中, 所有的内省都会搜查原对象. 标准的基于类对象的 ClassHow 提供了这些工具:

can


给定一个方法名, 它返回一个Parcel, 这个 Parcel 里面是可用的方法名

class A      { method x($a) {} };
class B is A { method x()   {} };
say B.^can('x').elems;              # 2
for B.^can('x') {
    say .arity;                     # 1, 2
}

在这个例子中, 类 B 中有两个名为 x 的方法可能可用(尽管一个正常的方法调用仅仅会直接调用安置在 B 中那个方法). B 中的那个方法有一个参数(例如, 它期望一个参数, 一个调用者(self)), 而 A 中的 x 方法期望 2 个参数( self 和 $a).

methods


返回类中可用公共方法的列表( 这包括父类和 roles 中的方法). 默认它会停在类 Cool, Any 或 Mu 那儿; 若真要获取所有的方法, 使用副词 :all.

class A {
    method x() { };
}
say A.^methods();                   # x
say A.^methods(:all);               # x infinite defined ...

mro


按方法解析顺序返回类自身的列表和它们的父类. 当方法被调用时, 类和它的父类按那个顺序被访问.(仅仅是概念上; 实际上方法列表在类构建是就创建了).

say 1.^mro;                         # (Int) (Cool) (Any) (Mu)

name


返回类的名字:

say 'a string'.^name;               # Str

parents


返回一个父类的列表. 默认它会停在 Cool, Any 或者 Mu 那儿, 但你可以提供一个副词 :all来压制它. 使用副词 :tree 会返回一个嵌套列表.

class D             { };
class C1 is D       { };
class C2 is D       { };
class B is C1 is C2 { };
class A is B        { };
say A.^parents(:all).perl;          # (B, C1, C2, D, Any, Mu)
say A.^parents(:all, :tree).perl;
    # ([B, [C1, [D, [Any, [Mu]]]], [C2, [D, [Any, [Mu]]]]],)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,849评论 6 13
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 标题 大纲 12: 对象(Objects) 版本 创建于: 2004-08-27 上次修改时间: 2014-8-2...
    焉知非鱼阅读 239评论 0 0
  • 自省和 Perl 6 的对象系统 Perl 6 是构建在元对象层上面的。那意味着有些对象(元对象)控制着各种面向对...
    焉知非鱼阅读 482评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139