Desgin Perl 6: S12-Objects

标题


大纲 12: 对象(Objects)

版本


创建于: 2004-08-27

上次修改时间: 2014-8-26

版本:134

概述


这个大纲总结了第12个启示录, 它探讨关于面向对象的编程。

类 (Classes)


S12-class/lexical.t lines 12–61

S12-class/basic.t lines 13–50

S14-roles/lexical.t lines 12–47

类是使用关键字 class 声明的模块。 至于模块, 即公共存储, 接口, 并且类的名字通过包和它的名字来表示, 这总是(但不必须)一个全局的名字。 类是一个模块, 因此能导出东西, 但是类添加了更多的行为来支持 Perl 6 的标准的基于类的 OO。

作为类型对象(type object), 类名代表了它的类型的所有可能值, 因此在计算那种类型的普通对象能做什么时, 类型对象能用作任何属于该类型的"真实"对象的代理。 类对象是一个对象, 但是它不是一个类(Class), 因为 Perl 6 中没有强制性的 Class 类, 还因为在Perl 6 中类型对象被认为是未定义的。 我们想基于类的和基于原型的 OO 编程这两个都支持。所以, 所有的元编程是通过当前对象的 HOW 对象来完成的, 这可以把元编程代理给任何它喜欢的元模型上。 然而, 默认地, 从 Mu 派生的对象支持相当标准的基于类的模型。

有两种基本的类声明语法:

    unit class Foo; # 文件的剩余部分是类的定义
    has $.foo

    class Bar { has $.bar } # block 是类的定义

第一种形式只有当第一种声明是在一个编译单元(即文件或 EVAL 字符串)中时被允许。

如果类的主体以一个主操作符为单个prefix:<...>(yada)listop 开始的语句, 那么只引入类名而不定义, 并且在同一个作用域中第二次声明那个类不会抱怨重新定义。(语句修饰符允许在这样的 ... 操作符上。)因此你可以向前声明你的类:

calss A { ... } # 引入 A 作为类名而不定义
class B { ... } # 引入 B 作为类名而不定义

my A $root .= new(:a(B));

class A {
    has B $.a;
}

class B {
    has A $.b;
}

就像这个例子展示的那样, 这允许互相递归类的定义(但是它不允许递归继承)。

通过 augment 声明符来扩展类也是可以的, 但是那被认为有点不符合常规并且不应用于向前声明。

一个具名的类声明能作为表达式的一部分出现, 就像具名子例程声明那样。

类主要用于实例管理, 而非代码复用。 当你只是想提取共有的代码时考虑使用 roles。

Perl 6 支持多重继承, 匿名类和自动装箱。

S12-class/anonymous.t lines 5–81

所有的 public 方法调用在 C++ 里就是虚的。

你可能派生自任何内置类型, 但是像Int这样的低级别派生可能只增加行为, 而不改变表示。使用构成 and/or 代理来改变表示。

因为 Perl 6 中没有裸字, 裸的类名必须被预先声明好。你可以预先声明一个 stub 类并在之后填充它就像你在子例程中的那样。

S12-class/declaration-order.t lines 14–21

S12-class/stubs.t lines 4–40

你可以使用 :: 前缀来强制一个名字解释为类名或类型名。 在左值上下文中, :: 前缀是一个 no-op, 但是在声明式的上下文中, 它在声明的作用域中绑定了一个新的类型名, 伴随着任何其它通过声明声明的东西。

S12-class/literal.t lines 7–25

如果没有 my 或其它作用域声明符, 那么一个裸的 class 声明符声明了一个 our声明符, 即一个在当前包中的名字。 因为类文件开始解析于 GLOBAL 包中, 文件中第一个声明的类把它自己安装为一个全局的名字, 之后的声明随后把它们自己安装在当前类中而不是全局的包中。

因此, 要在当前的包(或模块, 或类)中声明一个内部的类, 那使用 our class 或仅仅 class。 要声明一个本地作用域的类, 使用 my class。 类的名字总是从最内的作用域开始搜索, 直到最外层的作用域。 至于起始的 ::, 类名中出现的 :: 不是暗示全局性(不像 Perl 5)。 所以外层的搜索能查看搜索过的名字空间的孩子。

内部的 class 或 role 在一般的上下文中必须被本地作用域化, 如果它依赖于任何一般的参数或类型的话; 并且这样的内部类或 role 也叫做泛型。

类的特性(Class traits)


类的特性使用 is 来设置:

    class MyStruct is rw { ... }

单继承


isa 也仅仅是一个特性, 碰巧是另一个类:

    class Dog is Mammal { ... }

多重继承


多重继承使用多个 is 来指定:

class Dog is Mammal is Pet { ... }
​```    
#### 合成
Roles 使用 `does` 代替 `is`:
​```perl
class Dog is Mammal does Pet { ... }

also 声明符


你也可以使用 also 声明符把这些都放在里面:

class Dog {
    also is Mammal;
    also does Pet;
}

(然而, also 声明符主要用在 roles 中)

元类(Metaclasses)


每个对象(包括任何基于类的对象)代理给它的元类的一个实例上。你能通过 HOW 方法来获取元类的任何对象, HOW 方法返回那个元类的实例。 在 Perl 6 中, 一个"类"对象仅仅被认为是一个"空的"实例, 更合适的叫法是 "原型" 或 "泛型" 对象, 或仅仅叫 "类型对象"。 Perl 6 真的没有任何名为 Class 的类。 各种各样的类型是通过这些未定义的类型对象来命名的, 这被认为是和他们自己的实例化版本拥有相同的类型。但是这样的类型对象是惰性的, 并且不能管理类实例的状态。

管理实例的实际对象是通过 HOW 语法所指向的元类对象。 所以当你说 "Dog"的时候, 你指的即是一个包又是一个类型对象, 后者指的是通过 HOW 来表示类的对象。 类型对象区别实例对象不是通过拥有不同的类型, 而是就谁被定义而言的。有些对象可能告诉你它们被定义了, 而其它对象可能告诉你它们没有被定义。 那归结于对象, 并取决于元对象如何选择去分发 .defined 方法。

闭合类(Closed classes)


类默认是开放和非最终(non-final) 的, 但是它们能很容易地被整个程序闭合或定型, 而非被它们自己。 然而使用动态加载或子程序的平台可能不想批量闭合或定型类。(这特么都是什么?)

私有类


私有类能使用 my 来声明; 在 Perl 6 中, 大部分隐私问题是使用词法作用域(my)来处理的。词法默认很重要的事实也意味着类导入的任何名字默认也是私有的。

在 grammars 中, 不能使用 grammars 属性, 所以你能从一个不相关的 grammar 中调用一个 grammar。这能通过在闭包中创建一个本地作用域的 grammars 来模仿那种行为。闭包捕获的词法变量就能用在像 grammars 属性那样的地方了。

类的成分


class声明(特别地, role 合成)是严格的编译时语句。特别地, 如果类声明出现在嵌套作用域里面, 那么类声明被约束为, 构成和任何可能的实现一样。所有的 roles 和 超类必须被限制为非重新装订的只读值; 任何 traits 的参数会只在非拷贝上下文中被求值。类声明限定的名字是非重新装订的并且是只读的, 所以它们能被用作超类。

匿名的类声明


在匿名的类声明中, 如果需要 :: 本身就代表了匿名类的名字:

class { ... }                    # ok
class is Mammal { ... }          # 错误
class :: is Mammal { ... }       # ok
class { also is Mammal; ... }    # also ok

方法


方法是类中使用 method 关键字声明的子例程:

method doit ($a, $b, $c) { ... }
meyhod doit ($self: $a, $b, $c) { ... }
method doit (MyName $self: $a, $b, $c) { ... }
method doit (::?CLASS $self: $a, $b, $c) { ... }

调用者


调用者的声明是可选的。你总是使用关键字 self来访问当前调用者。你不需要声明调用者的类型, 因为调用者的词法类是被任何事件知晓的, 因为方法必须声明在调用者的类中, 尽管真实的(虚拟的)类型可能是词法类型派生出来的类型。你可以声明一个限制性更强的类类型, 但是那对于多态可能是坏事儿。你可以显式地使用词法类型来type 调用者, 但是任何为此做的检查会被优化掉,(当前的词法导向的类总是可以命名为 ::?CLASS 即使在匿名类中或 roles 中)

S12-attributes/recursive.t lines 46–97

要标记一个显式的调用者, 在它后面放上一个冒号就好了:

method doit ($x: $a, $b, $c) { ... }

如果你使用数组变量为 Array 类型声明一个显式的调用者, 你可以在列表上下文中直接使用它来生成它的元素

method push3 (@x: $a, $b, $c) { ... any(@x) ... }

注意 self项直接指向了方法所调用的对象上, 因此:

class A is Array {
    method m() { .say for self }
}
A.new(1, 2, 3).m; # 1\n2\n\3

会打印3行输出。

私有方法


私有方法是使用 ! 声明的:

[S12-methods/private.t lines 6–44]

method !think (Brain $self: $thought)

(这样的方法对普通方法调用是完全不可见的, 实际上是使用不同的语法, 即使用 ! 代替 . 字符。 看下面。)

方法作用域


不像大部分的其它声明符, method声明符不是默认为 our语义, 或者甚至 my 语义, 而是 has语义。所以, 不是安装一个符号到词法的或包的符号表中, 它们只是在当前类或 role 中通过调用它的元对象来安装一个公共的或私有的方法。(同样适用于 submethod 声明符 — 查看下面的 "Submethod").

使用一个显式的 has声明符对声明没有影响。你可以在本地作用域中使用my或在当前包中使用 our来给方法安装额外的别名。这些别名使用 &foo别名来命名, 并返回一个能叫做子例程的 Routine对象, 这时你必须提供期望的调用者作为第一个参数。

方法调用


要使用普通的方法分发语义来调用普通的方法, 使用点语法记法或间接对象记法:

S12-methods/instance.t lines 13–243

$obj.doit(1,2,3)
doit $obj: 1, 2, 3

间接对象记法现在要求调用者后面要有一个冒号, 即使冒号后面没有参数:

S12-methods/indirect_notation.t lines 5–57

$handle.close;
close $handle:;

要拒绝方法调用并且只考虑 subs, 仅仅从调用行那儿省略冒号即可:

close($handle);
close $handle;

然而, 这儿内置IO类定义的方法 close ()是导出的, 它默认把 multi sub close (IO) 放在作用域中。因此, 如果 $handle对象是一个 IO 对象的话, 那么上面的两个子例程调用仍旧被转换成方法调用。

点调用记法可以省略调用者, 如果调用者是 $_:

.doit(1,2,3)

方法调用使用的是 C3 方法解析顺序。

花哨的方法调用


注意对于私有方法没有对应的记法。

!doit(1,2,3)     # 错, 会被解析为 not(doit(1,2,3))
self!doit(1,2,3) # ok

对于方法名有几种间接的形式。你可以使用引起的字符串替换标识符, 它会被求值为引起, 引起的结果用作方法名。

S12-methods/indirect_notation.t lines 58–76

$obj."$methodname"(1,2,3) # 使用 $methodname 的内容作为方法名
$obj.'$methodname'(1,2,3) # 没有插值; 调用名字中带有 $ 符号的方法

$obj!"$methodname"() # 间接调用私有方法名

在插值中, 双引号形式不可以包含空白。这在双引号中以点结尾的字符串中达到用户期望的那样:

S02-literals/misc-interpolation.t lines 96–120

say "Foo = $foo.";

如果你真的想调用带有空格的方法, 那你使用一个闭包插值来进行约束:

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

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • 允许的修饰符 有些修饰符能在所有允许的地方出现, 但并非所有的都这样. 通常, 影响 regex 编译的修饰符(...
    焉知非鱼阅读 1,318评论 0 1
  • 说来也有趣,大概五年前,那时候刚毕业不久,可能是想钱想疯了,也有个高大上点的理由:我想积累自己的未来,看了非常多理...
    阿拉希哥阅读 1,060评论 1 48