rakulang, dartlang, nimlang, golang, rustlang, lang lang no see

Raku 中的面向对象

焉知非鱼

Object Orientation in Raku

Raku 为面向对象编程(OOP)提供强大支持。尽管 Raku 允许程序员以多种范式进行编程, 但面向对象编程是该语言的核心。

Raku 带有丰富的预定义类型, 可分为两类:常规类型和原生类型。所有你能存储到变量中的东西要么是一个原生的 value, 要么是一个对象。这包括字面值、类型(类型对象)、code 和容器。

原生类型用于底层类型(例如 uint64)。尽管原生类型没有和对象同样的功能, 如果在其上调用方法, 它们也会自动装入普通对象。

一切不是原生值的东西都是一个对象。对象确实允许继承封装

使用对象 #

要在对象上调用方法, 请在对象名上添加一个点, 然后添加方法名称:

say "abc".uc;
# OUTPUT: «ABC␤» 

这将在 “abc” 上调用 uc 方法, 这是一个 Str 类型的对象。要为方法提供参数, 请在方法后面的括号内添加参数。

my $formatted-text = "Fourscore and seven years ago...".indent(8);
say $formatted-text;
# OUTPUT: «        Fourscore and seven years ago...» 

$formatted-text 现在包含上面的文本, 但缩进了8个空格。

多个参数由逗号分隔:

my @words = "Abe", "Lincoln";
@words.push("said", $formatted-text.comb(/\w+/));
say @words;
# OUTPUT: «[Abe Lincoln said (Fourscore and seven years ago)]␤» 

类似地, 可以通过在方法后放置冒号并使用逗号分隔参数列表来指定多个参数:

say @words.join('--').subst: 'years', 'DAYS';
# OUTPUT: «Abe--Lincoln--said--Fourscore and seven DAYS ago␤» 

如果要在没有括号的情况下传递参数, 则必须在方法之后添加一个 :, 因此没有冒号或括号的方法调用明确地是没有参数列表的方法调用:

say 4.log:   ; # OUTPUT: «1.38629436111989␤» ( natural logarithm of 4 ) 
say 4.log: +2; # OUTPUT: «2␤» ( base-2 logarithm of 4 ) 
say 4.log  +2; # OUTPUT: «3.38629436111989␤» ( natural logarithm of 4, plus 2 )

许多看起来不像方法调用的操作(例如, 智能匹配或将对象插值到字符串中)可能会导致方法调用。

方法可以返回可变容器, 在这种情况下, 您可以分配方法调用的返回值。这是使用对象的可读写属性的方式:

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

在这里, 我们在 $*IN 对象上调用 nl-in 方法, 不带参数, 并使用 = 运算符将其赋值给它返回的容器。

所有对象都支持类Mu的方法, 它是层次结构的根。所有对象都来自 Mu

类型对象 #

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

my $int-type-obj = Int;

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

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) {
        ...
    }
    ...
}

子类型可以使用 smartmatching 来检查:

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

类 #

使用 class 关键字进行类的声明, 通常后跟类名:

class Journey {

}

此声明会创建类型对象并将其安装在名字 Journey 下的当前包和当前词法作用域中。您还可以通过词法声明类

my class Journey {

}

这限制了它们对当前词法范围的可见性, 如果该类是嵌套在模块或另一个类中的实现细节, 这可能很有用。

属性 #

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

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

虽然没有公共(甚至保护属性)属性, 不过有一种方式可以自动生成访问器方法: 使用 . twigil 代替 ! 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;
}

现在, Journey对象创建之后, 它的 .origin, .destination.notes 都能从类的外部访问, 但只有 .notes 可以被修改。

如果在没有某些属性(例如 origindestination)的情况下实例化对象, 我们可能无法获得想要的结果。要防止这种情况, 请提供默认值或通过使用is required trait 来标记属性以确保在对象创建时设置属性。

class Journey {
    # error if origin is not provided 
    has $.origin is required;
    # set the destination to Orlando as default (unless that is the origin!) 
    has $.destination = self.origin eq 'Orlando' ?? 'Kampala' !! 'Orlando';
    has @!travelers;
    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!';

请注意, 虽然默认构造函数可以初始化只读属性, 但它只会设置具有访问器方法的属性。也就是说, 即使您传递 travelers => ["Alex", "Betty"] 给默认构造函数, @!travelers 也不会初始化该属性。

方法 #

使用 method 关键字在类的 body 中声明方法:

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 并生成一个访问方法。

看看上面的代码, 在方法 describe 中使用 $!origin$.origin 有一个微妙但重要的区别。$!origin 是一种廉价且明显的属性查找。$.origin 是一个方法调用, 因此可以在子类中重写。只在你想要允许覆盖时使用 $.origin

与子例程不同, 其他命名参数不会产生编译时或运行时错误。这允许通过重新分派来链接方法。

你可以编写自己的访问器来覆盖任何或所有自动生成的访问器。

my $ⲧ = " " xx 4; # A tab-like thing 
class Journey {
    has $.origin;
    has $.destination;
    has @.travelers;
    has Str $.notes is rw;
 
    multi method notes() { "$!notes\n" };
    multi method notes( Str $note ) { $!notes ~= "$note\n$ⲧ" };
 
    method Str { "$!origin\n$ⲧ" ~ self.notes() ~ "$!destination ⤶\n" };
}
 
my $trip = Journey.new( :origin<Here>, :destination<There>,
                        travelers => <þor Freya> );
 
$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;
 
# OUTPUT: 
#⤷ Here 
#       First steps 
#       Almost there 
# 
#There ⤶ 

声明的 multi 方法 notes 使用不同的签名进行读写, notes 覆盖了 $.notes 声明中隐含的自动生成的方法。

请注意, 在 notes $trip: "Almost there" 中我们使用了间接调用语法, 它首先放置方法名称, 然后放置对象, 然后用冒号分隔参数:method invocant: arguments。只要感觉比经典的句点和括号更自然, 我们就可以使用这种语法。它的工作原理完全相同。

可以在运行时使用 ."" 运算符解析方法名称。

class A { has $.b };
my $name = 'b';
A.new."$name"().say;
# OUTPUT: «(Any)␤» 

相对于先前的属性部分, 过去用于更新 $.notes 的语法在本节已改变。代替赋值:

$vacation.notes = 'Pack hiking gear and sunglasses!';

我们现在做一个方法调用:

$trip.notes("First steps");

覆盖默认的自动生成的访问器意味着它在返回赋值时提供可变容器不再可用。方法调用是将属性的更新添加到计算和逻辑的首选方法。许多现代语言可以通过使用 “setter” 方法重载赋值来更新属性。虽然 Raku 可以为此目的使用 Proxy 对象重载赋值运算符, 但是目前不鼓励使用复杂逻辑重载赋值运算符来设置属性作为弱面向对象设计

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

等价于:

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

类和实例方法 #

方法的签名可以有一个调用者(invocant)作为第一个参数, 后跟一个冒号, 这允许该方法引用它被调用的对象。

class Foo {
    method greet($me: $person) {
        say "Hi, I am $me.^name(), nice to meet you, $person";
    }
}
Foo.new.greet("Bob"); # OUTPUT: «Hi, I am Foo, nice to meet you, Bob␤» 

在方法签名中提供调用者还允许通过使用类型约束将方法定义为类方法或对象方法。::?CLASS 变量可用于在编译时提供类名, 与 :U(对于类方法)或 :D(对于实例方法)结合使用。

class Pizza {
    has $!radius = 42;
    has @.ingredients;
 
    # 类方法: construct from a list of ingredients 
    method from-ingredients(::?CLASS:U $pizza: @ingredients) {
        $pizza.new( ingredients => @ingredients );
    }
 
    # 实例方法 
    method get-radius(::?CLASS:D:) { $!radius }
}

my $p = Pizza.from-ingredients: <cheese pepperoni vegetables>;

say $p.ingredients;     # OUTPUT: «[cheese pepperoni vegetables]␤» 
say $p.get-radius;      # OUTPUT: «42␤» 
say Pizza.get-radius;   # This will fail. 

CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:␤ 
#          Invocant of method 'get-radius' must be 
#          an object instance of type 'Pizza', 
#          not a type object of type 'Pizza'. 
#          Did you forget a '.new'?» 

通过使用multi声明符, 方法既可以是类方法也可以是对象方法:

class C {
    multi method f(::?CLASS:U:) { say "class method"  }
    multi method f(::?CLASS:D:) { say "object method" }
}
C.f;       # OUTPUT: «class method␤» 
C.new.f;   # OUTPUT: «object method␤» 

self #

在方法内部, 术语 self 可用并绑定到调用对象。self 可以用来调用调用者的其他方法, 包括构造函数:

class Box {
    has $.data;
 
    method make-new-box-from() {
        self.new: data => $!data;
  }
}

self 也可以用在类方法或实例方法中, 但要注意尝试从另一个方法调用一种类型的方法:

class C {
    method g()            { 42     }
    method f(::?CLASS:U:) { self.g }
    method d(::?CLASS:D:) { self.f }
}

C.f;        # OUTPUT: «42␤» 
C.new.d;    # This will fail. 

CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:␤ 
#          Invocant of method 'f' must be a type object of type 'C', 
#          not an object instance of type 'C'.  Did you forget a 'multi'?» 

self 也可以与属性一起使用, 只要它们具有访问器。self.a 将为声明为 has $.a 的属性调用访问器。然而, self.a$.a 之间存在差异, 因为后者将项化; $.a 将等同于 self.a.item$(self.a)

class A {
    has $.x = (1, 2, 3);
    method b() { .say for self.x; .say for $.x }
};

A.new.b; # OUTPUT: «1␤2␤3␤(1 2 3)␤» 

方法调用的冒号语法仅支持使用方法调用 self, 而不支持快捷方式。

请注意, 如果 Mu 中的相关方法 bless, CREATE 没有重载, self 将指向这些方法的类型对象。

在另一方面, 在初始化的不同阶段, 在实例上调用子方法 BUILDTWEAK。子类中同名的子方法尚未运行, 因此你不应该依赖这些方法中的潜在虚方法调用。

私有方法 #

方法名前带有感叹号的方法不能从定义类之外的任何地方调用; 这些方法是私有的, 因为它们在声明它们的类之外是不可见的。使用感叹号而不是点号调用私有方法:

class FunMath {
    has $.value is required;

    method !do-subtraction( $num ) {
        if $num ~~ Str {
            return $!value + (-1 * $num.chars);
        }
        return $!value + (-1 * $num);
    }

    method minus( $minuend: $subtrahend ) {
        # invoking the private method on the explicit invocant 
        $minuend!do-subtraction($subtrahend);
    }
}

my $five = FunMath.new(value => 5);
say $five.minus(6);         # OUTPUT: «-1␤» 
say $five.do-subtraction(6);

CATCH { default { put .^name ~ ":\n" ~ .Str } }
# OUTPUT: «X::Method::NotFound: 
# No such method 'do-subtraction' for invocant of type 
# 'FunMath'. Did you mean '!do-subtraction'?␤» 

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

子方法 #

子方法是子类不继承的公共方法。该名称源于它们在语义上与子例程类似的事实。

子方法对于对象构造和销毁任务以及特定于特定类型的任务非常有用, 因此子类型必须重写它们。

例如, 默认方法 new继承链中的每个类上调用 submethod BUILD:

class Point2D {
    has $.x;
    has $.y;
 
    submethod BUILD(:$!x, :$!y) {
        say "Initializing Point2D";
    }
}
 
class InvertiblePoint2D is Point2D {
    submethod BUILD() {
        say "Initializing InvertiblePoint2D";
    }

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

# OUTPUT: «Initializing Point2D␤» 
# OUTPUT: «Initializing InvertiblePoint2D␤» 
# OUTPUT: «InvertiblePoint2D.new(x => 1, y => 2)␤» 

另请参见:Object_construction

继承 #

类可以有父类:

class Child is Parent1 is Parent2 { }

如果在子类上调用一个方法, 但是子类没有提供该方法, 则调用其中一个父类中同名方法(如果存在)。父类被查询的顺序就叫做方法解析顺序(MRO)。Raku 使用 C3 方法解析顺序。你可以通过调用类型的元类型方法得知这个类型的 MRO.

如果一个类没有指定它的父类, 就默认使用 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; # calls the frob method of Child rather than Parent 
# OUTPUT: «the child's somewhat more fancy frob is called␤» 

对象构造 #

对象通常通过方法调用创建, 或者通过类型对象或者通过同类型的其它对象创建。

Mu 提供了一个名为 new 的构造函数方法, 它接受命名参数并使用它们来初始化公共属性。

class Point {
    has $.x;
    has $.y;
}
my $p = Point.new( x => 5, y => 2);
#             ^^^ inherited from class Mu

say "x: ", $p.x;
say "y: ", $p.y;
# OUTPUT: «x: 5␤» 
# OUTPUT: «y: 2␤» 

Mu.new 在调用者身上调用 bless 方法, 传递所有的命名参数bless 创建新的对象, 然后以反向方法解析顺序(即从Mu到大多数派生类)遍历所有子类, 并且在每个类中检查是否存在名为 BUILD 的方法。 如果该方法存在, 则使用该方法中的所有命名参数调用该 new 方法。如果不存在名为 BUILD 的方法, 这个类的公开属性就会用同名的命名参数进行初始化。 在任何一种情况下, 如果 BULID 方法和 默认构造函数 都没有对属性进行初始化, 则应用默认值。这意味着 BUILD 可以更改属性, 但它无权访问声明为其默认值的属性的内容; 这些只在 TWEAK(见下文)中可用, 它可以“看到”在类的声明中初始化的属性的内容。

BUILD 被调用之后, 名为 TWEAK 的方法会被调用, 如果它们存在, 传递给 new 的所有命名参数也会传递给 TWEAK。请参阅下面的使用示例。

由于 BUILDTWEAK 子方法的默认行为, 派生于 Munew 构造函数的命名参数可直接对应的任何方法解析顺序类的公共属性, 或对应于任何 BUILDTWEAK 子方法的任何命名参数。

此对象构造方案对自定义构造函数有几个含义。首先, 自定义 BUILD 方法应始终是子方法, 否则它们会破坏子类中的属性初始化。其次, 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] );
#  both enc and encoding are allowed now 

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

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

但是, 当属性可能具有特殊类型要求(例如 :$!id 必须为正整数)时, 请谨慎使用此属性的自动绑定。请记住, 除非您专门处理此属性, 否则将分配默认值, 并且该默认值将为 Any, 这会导致类型错误。

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

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

然而, 这不是最佳实践, 因为它使得从子类正确地初始化对象变得更难了。

另外需要注意的是, 名字 new 在 Raku 中并不特别。它只是一个常见的约定, 在大多数Raku类中都非常彻底。你可以从在调用的任何方法调用 bless, 或者使用 CREATE 来摆弄低级别的工作。

TWEAK 子方法允许你在对象构造后检查或修改属性:

class RectangleWithCachedArea {
    has ($.x1, $.x2, $.y1, $.y2);
    has $.area;

    submethod TWEAK() {
        $!area = abs( ($!x2 - $!x1) * ( $!y2 - $!y1) );
    }
}
 
say RectangleWithCachedArea.new( x2 => 5, x1 => 1, y2 => 1, y1 => 0).area;
# OUTPUT: «4␤» 

对象克隆 #

克隆是使用所有对象上可用的clone方法完成的, 这些克隆方法可以浅克隆公共和私有属性。公共属性的新值可以作为命名参数提供。

class Foo {
    has $.foo = 42;
    has $.bar = 100;
}
 
my $o1 = Foo.new;
my $o2 = $o1.clone: :bar(5000);

say $o1; # Foo.new(foo => 42, bar => 100) 
say $o2; # Foo.new(foo => 42, bar => 5000) 

有关如何克隆非标量属性的详细信息, 请参阅文档以获取克隆, 以及实现自己的自定义克隆方法的示例。

角色 #

角色是属性和方法的集合; 但是, 与类不同, 角色仅用于描述对象行为的一部分; 这就是为什么一般来说, 角色应该在类和对象中混合使用。通常, 类用于管理对象, 而角色用于管理对象内的行为和代码重用。

角色使用关键字 role 放在所声明的角色名称前面。角色使用 does 关键字 mixed in, does 关键字放在角色名之前。

constant  = " " xx 4; # Just a ⲧab 

role Notable {
    has Str $.notes is rw;
 
    multi method notes() { "$!notes\n" };
    multi method notes( Str $note ) { $!notes ~= "$note\n" ~  };
}
 
class Journey does Notable {
    has $.origin;
    has $.destination;
    has @.travelers;
 
    method Str { "$!origin\n" ~  ~ self.notes() ~ "$!destination ⤶\n" };
}
 
my $trip = Journey.new( :origin<Here>, :destination<There>,
                        travelers => <þor Freya> );
 
$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;

# OUTPUT: 
#⤷ Here 
#       First steps 
#       Almost there 
# 
#There ⤶ 

一旦编译器解析角色声明的结束大括号, 角色就是不可变的。

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 就不可变了。

应用角色 #

角色应用程序与类继承有很大不同。将角色应用于类时, 该角色的方法将复制到类中。如果将多个角色应用于同一个类, 则冲突(例如, 同名的属性或 non-multi 方法)会导致编译时错误, 这可以通过在类中提供同名的方法来解决。

这比多重继承安全得多, 其中编译器从不检测冲突, 而是解析为方法解析顺序中较早出现的超类, 这可能不是程序员想要的。

例如, 如果你已经发现了一种有效的方法来骑牛, 并试图把它作为一种新的流行交通形式推销, 你可能会有一个 Bull 类, 对于你在房子周围的所有公牛, 以及一个 Automobile 类, 你可以驾驶的东西。

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;
say $t.steer;
# OUTPUT: «Taurus.new(castrated => Bool::True, direction => Any)␤» 

通过这种设置, 您的贫困客户将发现自己无法转动他们的金牛座, 您将无法生产更多的产品!在这种情况下, 使用角色可能更好:

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)
    }
}

将角色应用于第二个角色时, 实际应用程序将延迟, 直到第二个角色应用于类, 此时两个角色都将应用于该类。从而

role R1 {
    # methods here 
}

role R2 does R1 {
    # methods here 
}

class C does R2 { }

产生相同类 C

role R1 {
    # methods here 
}

role R2 {
    # methods here 
}

class C does R1 does R2 { }

Stubs #

当角色包含 stubbed 方法时, 必须在将角色应用于类时提供同名方法的 non-stubbed 版本。这允许您创建充当抽象接口的角色。

role AbstractSerializable {
    method serialize() { ... }        # literal ... here marks the 
                                      # method as a stub 
}
 
# the following is a compile time error, for example 
#        Method 'serialize' must be implemented by Point because 
#        it's required by a role 
 
class APoint does AbstractSerializable {
    has $.x;
    has $.y;
}
 
# this works: 
class SPoint does AbstractSerializable {
    has $.x;
    has $.y;
    method serialize() { "p($.x, $.y)" }
}

stubbed 方法的实现也可以由另一个角色提供。

继承 #

角色不能从类继承, 但它们可以携带类, 导致任何具有该角色的类从承载的类继承。所以, 如果你写:

role A is Exception { }
class X::Ouch does A { }
X::Ouch.^parents.say # OUTPUT: «((Exception))␤»

然后 X::Ouch 将直接从 Exception 继承, 我们可以通过列出其父项来看到。

由于它们不使用可能称为继承的东西, 因此角色不是类层次结构的一部分。使用 .^roles 元方法列出角色, 它使用 transitive 标记来包含所有级别或仅包含第一个级别。尽管如此, 仍然可以使用智能匹配或类型约束来测试类或实例, 以查看它是否起作用。

role F { }
class G does F { }
G.^roles.say;                    # OUTPUT: «((F))␤» 
role Ur {}
role Ar does Ur {}
class Whim does Ar {}; Whim.^roles(:!transitive).say;   # OUTPUT: «((Ar))␤» 
say G ~~ F;                      # OUTPUT: «True␤» 
multi a (F $a) { "F".say }
multi a ($a)   { "not F".say }
a(G);                            # OUTPUT: «F␤» 

主从秩序 #

role M {
  method f { say "I am in role M" }
}
 
class A {
  method f { say "I am in class A" }
}
 
class B is A does M {
  method f { say "I am in class B" }
}
 
class C is A does M { }
 
B.new.f; # OUTPUT «I am in class B␤» 
C.new.f; # OUTPUT «I am in role M␤» 

请注意, multi-method 的每个候选项都是它自己的方法。在这种情况下, 以上仅适用于两个这样的候选项具有相同签名的情况。否则, 没有冲突, 候选者只是添加到 multi-method 中。

自动角色双关 #

任何直接实例化角色或将其用作类型对象的尝试都将自动创建一个与角色同名的类, 从而可以透明地使用角色, 就好像它是一个类一样。

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

    method abs { sqrt($.x * $.x + $.y * $.y) }
    method dimensions { 2 }
}

say Point.new(x => 6, y => 8).abs; # OUTPUT «10␤» 
say Point.dimensions;              # OUTPUT «2␤» 

我们把这种自动创建的类叫双关, 并且将生成的类叫双关语。

但是, Punning 不是由大多数元编程构造引起的, 因为它们有时用于直接使用角色。

参数化角色 #

角色可以通过在方括号中给它们签名来参数化:

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 .. *];

        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);    # OUTPUT: «5␤4␤6␤» 
$t.visit-postorder(&say);   # OUTPUT: «4␤6␤5␤» 

这里, 签名只包含类型捕获, 但任何签名都可以:

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'); # OUTPUT: «[DEBUG] here we go␤» 

您可以拥有多个同名角色, 但签名不同; 多重分派的正常规则适用于选择多个候选者。

混合角色 #

角色可以混合到对象中。角色的给定属性和方法将添加到对象已有的方法和属性中。支持多个mixin和匿名角色。

role R { method Str() {'hidden!'} };
my $i = 2 but R;
sub f(\bound){ put bound };
f($i); # OUTPUT: «hidden!␤» 
my @positional := <a b> but R;
say @positional.^name; # OUTPUT: «List+{R}␤» 

请注意, 对象混合了角色, 而不是对象的类或容器。因此, @-sigiled 容器将需要绑定以使角色坚持, 如示例中的 @positional 所示。某些运算符将返回一个新值, 从而有效地从结果中删除 mixin。这就是为什么使用 does 在变量声明中混合角色可能更为清晰:

role R {};
my @positional does R = <a b>;
say @positional.^name; # OUTPUT: «Array+{R}␤» 

运算符 infix:<but> 比列表构造函数窄。提供要混合的角色列表时, 请始终使用括号。

role R1 { method m {} }
role R2 { method n {} }
my $a = 1 but R1,R2; # R2 is in sink context, issues a WARNING 
say $a.^name;
# OUTPUT: «Int+{R1}␤» 
my $all-roles = 1 but (R1,R2);
say $all-roles.^name; # OUTPUT: «Int+{R1,R2}␤» 

Mixins 可用于对象生命中的任何一点。

# A counter for Table of Contents 
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;     # don't do math with Not A Number 
say $toc-counter;              # OUTPUT: «NaN␤» 

$toc-counter does TOC-Counter; # now we mix the role in 
$toc-counter.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);
put $toc-counter / 1;          # OUTPUT: «NaN␤» (because that's numerical context) 
put $toc-counter;              # OUTPUT: «2.2.2␤» (put will call TOC-Counter::Str) 

角色可以是匿名的。

my %seen of Int is default(0 but role :: { method Str() {'NULL'} });
say %seen<not-there>;          # OUTPUT: «NULL␤» 
say %seen<not-there>.defined;  # OUTPUT: «True␤» (0 may be False but is well defined) 
say Int.new(%seen<not-there>); # OUTPUT: «0␤» 

元对象编程和自省 #

Raku 有一个元对象系统, 这意味着对象、类、角色、Grammars、enums 等行为本身由其它对象控制; 这些对象叫做元对象(想想元操作符, 它操作的对象是普通操作符). 像普通对象一样, 元对象是类的实例, 这时我们称它们为元类。

对每个对象或类, 你能通过调用 .HOW 方法获取元对象. 注意, 尽管这看起来像是一个方法调用, 但它更像宏。

所以, 你能用元对象干些什么呢? 你可以通过比较元类的相等性来检查两个对象是否具有同样的元类:

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

Raku 使用单词 HOW, Higher Order Workings, 来引用元对象系统。因此, 在 Rakudo 中不必对此吃惊, 控制类行为的元类的类名叫做 Raku::Metamodel::ClassHow。每个类都有一个 Raku::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

内省 #

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

can #

给定一个方法名, can 返回可用的方法名

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]]]]],)

https://docs.raku.org/language/objects