Raku 提供了丰富的内置语法来定义和使用类。它使编写类在大多数情况下变得富有表现力和简洁,但也提供了机制来涵盖罕见的边缘情况。

快速概述§

让我们从一个例子开始,以概述

class Rectangle {
    has Int $.length = 1;
    has Int $.width = 1;
 
    method area(--> Int{
        return $!length * $!width;
    }
}
 
my $r1 = Rectangle.new(length => 2width => 3);
say $r1.area(); # OUTPUT: «6␤» 

我们使用 class 关键字定义一个新的 Rectangle 类。它有两个 属性$!length$!width,使用 has 关键字引入。两者都默认为 1。只读访问器方法会自动生成。(注意声明中的 . 而不是 !,它会触发生成。助记符:! 类似于一扇关闭的门,. 类似于一扇打开的门。)

名为 area方法 将返回矩形的面积。

很少需要显式地编写构造函数。一个自动继承的默认构造函数,称为 new,将自动从传递给构造函数的命名参数初始化属性。

任务示例§

作为更详细的示例,以下代码片段实现了一个依赖项处理程序。它展示了自定义构造函数、私有和公共属性、方法以及签名的各个方面。代码并不多,但结果却很有趣且有用。它将在以下各节中用作示例。

class Task {
    has      &!callback     is built;
    has Task @!dependencies is built;
    has Bool $.done;
 
    method new(&callback*@dependencies{
        return self.bless(:&callback:@dependencies);
    }
 
    method add-dependency(Task $dependency{
        push @!dependencies$dependency;
    }
 
    method perform() {
        unless $!done {
            .perform() for @!dependencies;
            &!callback();
            $!done = True;
        }
    }
}
 
my $eat =
    Task.new({ say 'eating dinner. NOM!' },
        Task.new({ say 'making dinner' },
            Task.new({ say 'buying food' },
                Task.new({ say 'making some money' }),
                Task.new({ say 'going to the store' })
            ),
            Task.new({ say 'cleaning kitchen' })
        )
    );
 
$eat.perform();

§

Raku,与许多其他语言一样,使用 class 关键字来定义一个类。后面的代码块可以包含任意代码,就像任何其他代码块一样,但类通常包含状态和行为声明。示例代码包括属性(状态),通过 has 关键字引入,以及行为,通过 method 关键字引入。

属性§

Task 类中,代码块中的前三行都使用 has 声明器声明属性(在其他语言中称为 字段实例存储)。就像 my 变量无法从其声明范围之外访问一样,属性也永远无法从类之外直接访问(这与许多其他语言形成对比)。这种 封装 是面向对象设计的主要原则之一。

Twigil $!§

第一个声明指定了回调的实例存储(即要调用的代码片段,以执行对象表示的任务)

has &!callback is built;

& 符号表示此属性代表可调用内容。! 字符是一个 twigil 或辅助符号。twigil 是变量名称的一部分。在本例中,! twigil 强调此属性对类是私有的。该属性被 封装。私有属性默认情况下不会被默认构造函数设置,这就是我们添加 is built 特性以允许这样做的原因。助记符:! 看起来像一扇关闭的门。

第二个声明也使用了私有 twigil

has Task @!dependencies is built;

但是,此属性表示一个项目数组,因此它需要 @ 符号。这些项目分别指定了在完成当前任务之前必须完成的任务。此外,此属性上的类型声明表明该数组只能包含 Task 类(或其某个子类)的实例。

Twigil $.§

第三个属性表示任务的完成状态

has Bool $.done;

这个标量属性(带有 $ 符号)的类型为 Bool。它使用 . 符号而不是 ! 符号。虽然 Raku 对属性强制封装,但它也免去了你编写访问器方法的麻烦。用 . 替换 ! 既声明了一个私有属性,也声明了一个以属性命名的访问器方法。在本例中,属性 $!done 和访问器方法 done 都已声明。就好像你写了

has Bool $!done;
method done() { return $!done }

请注意,这与某些语言允许的声明公共属性不同;你实际上得到了一个私有属性和一个方法,而无需手动编写方法。如果你将来需要执行比返回值更复杂的操作,可以自由地编写自己的访问器方法。

is rw 特性§

请注意,使用 . 符号创建了一个方法,该方法将提供对属性的只读访问。如果对象的用户应该能够重置任务的完成状态(也许是为了再次执行它),你可以更改属性声明

has Bool $.done is rw;

is rw 特性会导致生成的访问器方法返回一个容器,以便外部代码可以修改属性的值。

is built 特性§

has &!callback is built;

默认情况下,私有属性不会被默认构造函数自动设置。(毕竟它们是私有的。)在上面的例子中,我们希望允许用户提供初始值,但保持属性的私有性。is built 特性允许这样做。

也可以用它对公共属性做相反的事情,即阻止它们被用户提供的 value 自动初始化,但仍然生成访问器方法

has $.done is built(False);

上面的声明确保你无法构建已完成的任务,但仍然允许用户查看任务是否已完成。

is built 特性是在 Rakudo 2020.01 版本中引入的。

is required 特性§

默认情况下,在初始化期间为属性提供值是可选的。在任务示例中,这对于所有三个属性都是有意义的,即 &!callback@!dependencies$.done 属性。但假设我们想添加另一个属性 $.name,它保存任务名称,并且我们希望强制用户在初始化时提供值。我们可以按如下方式做到这一点

has $.name is required;

默认值§

你也可以为属性提供默认值(这对于有和没有访问器的属性都适用)

has Bool $.done = False;

赋值在对象构建时执行。右侧在此时进行评估,甚至可以引用之前的属性

has Task @!dependencies;
has $.ready = not @!dependencies;

可写属性可以通过可写容器访问

class a-class {
    has $.an-attribute is rw;
}
say (a-class.new.an-attribute = "hey"); # OUTPUT: «hey␤» 

此属性也可以使用 .an-attribute.an-attribute() 语法访问。另请参阅 类上的 is rw 特性,了解有关如何在整个类上使用它的示例。

类变量§

类声明还可以包含类变量,用 myour 声明,这些变量的值由所有实例共享,可用于诸如计算实例数量或任何其他共享状态等操作。因此,类变量的作用类似于其他编程语言中已知的静态变量。它们看起来与普通(非类)词法变量相同(实际上它们是相同的)

class Str-with-ID is Str {
    my  $counter = 0;
    our $our-counter = 0;
    has Str $.string;
    has Int $.ID is built(False);
 
    submethod TWEAK() {
        $counter++;
        $our-counter++;
        $!ID = $counter;
    }
 
}
 
class Str-with-ID-and-tag is Str-with-ID {
    has Str $.tag;
}
 
say Str-with-ID.new(string => 'First').ID;  # OUTPUT: «1␤» 
say Str-with-ID.new(string => 'Second').ID# OUTPUT: «2␤» 
say Str-with-ID-and-tag.newstring => 'Third'tag => 'Ordinal' ).ID# OUTPUT: «3␤» 
say $Str-with-ID::our-counter;        # OUTPUT: «3␤» 

类变量由所有子类共享,在本例中为Str-with-ID-and-tag。此外,当使用包范围的our声明符时,变量可以通过其完全限定名 (FQN) 访问,而词法范围的my变量是“私有”的。这与myour在非类上下文中表现出的行为完全相同。

类变量的行为类似于许多其他编程语言中的静态变量。

class Singleton {
    my Singleton $instance;
    method new {!!!}
    submethod instance {
        $instance = Singleton.bless unless $instance;
        $instance;
    }
}

在本例中,单例模式的实现使用类变量来保存实例。

class HaveStaticAttr {
    my Int $.foo = 5;
}

类属性也可以使用一个辅助符号声明,类似于实例属性,如果属性是公开的,则会生成只读访问器。默认值按预期工作,并且只分配一次。

方法§

属性赋予对象状态,而方法赋予对象行为。回到我们的Task示例。让我们暂时忽略new方法;它是一种特殊类型的。考虑第二个方法add-dependency,它将一个新的任务添加到任务的依赖列表中。

method add-dependency(Task $dependency{
    push @!dependencies$dependency;
}

在很多方面,这看起来很像一个sub声明。但是,有两个重要的区别。首先,将此例程声明为方法会将其添加到当前类的所有方法列表中,因此Task类的任何实例都可以使用.方法调用运算符调用它。其次,方法将它的调用者放入特殊变量self中。

该方法本身接受传递的参数(必须是Task类的实例),并将其push到调用者的@!dependencies属性中。

perform方法包含依赖项处理器的主要逻辑。

method perform() {
    unless $!done {
        .perform() for @!dependencies;
        &!callback();
        $!done = True;
    }
}

它不接受任何参数,而是使用对象的属性。首先,它通过检查$!done属性来检查任务是否已完成。如果是,则无需执行任何操作。

否则,该方法将执行所有任务的依赖项,使用for结构迭代@!dependencies属性中的所有项。此迭代将每个项(每个都是Task对象)放入主题变量$_中。在不指定显式调用者的情况下使用.方法调用运算符会使用当前主题作为调用者。因此,迭代结构会在@!dependencies属性中每个Task对象上调用.perform()方法。

在所有依赖项都完成后,是时候通过直接调用&!callback属性来执行当前Task的任务了;这就是括号的作用。最后,该方法将$!done属性设置为True,以便后续对该对象的perform调用(例如,如果此Task是另一个Task的依赖项)不会重复该任务。

私有方法§

与属性一样,方法也可以是私有的。私有方法用前缀感叹号声明。它们用self!后跟方法名来调用。在以下MP3TagData类的实现中,从 mp3 文件中提取ID3v1 元数据,方法parse-datacan-read-formattrim-nulls是私有方法,而其余方法是公共方法。

class MP3TagData {
    has $.filename where { .IO ~~ :e };
 
    has Str $.title   is built(False);
    has Str $.artist  is built(False);
    has Str $.album   is built(False);
    has Str $.year    is built(False);
    has Str $.comment is built(False);
    has Int $.genre   is built(False);
    has Int $.track   is built(False);
    has Str $.version is built(False);
    has Str $.type    is built(False= 'ID3';
 
    submethod TWEAK {
        with $!filename.IO.open(:r:bin-> $fh {
            $fh.seek(-128SeekFromEnd);
            my $tagdata = $fh.read(128);
            self!parse-data: $tagdata;
            $fh.close;
        }
        else {
            warn "Failed to open file."
        }
    }
 
    method !parse-data($data{
        if self!can-read-format($data{
            my $offset = $data.bytes - 128;
 
            $!title  = self!trim-nulls: $data.subbuf($offset +  330);
            $!artist = self!trim-nulls: $data.subbuf($offset + 3330);
            $!album  = self!trim-nulls: $data.subbuf($offset + 6330);
            $!year   = self!trim-nulls: $data.subbuf($offset + 93,  4);
 
            my Int $track-flag = $data.subbuf($offset + 97 + 281).Int;
            $!track            = $data.subbuf($offset + 97 + 291).Int;
 
            ($!version$!comment= $track-flag == 0 && $!track != 0
                ?? ('1.1'self!trim-nulls: $data.subbuf($offset + 9728))
                !! ('1.0'self!trim-nulls: $data.subbuf($offset + 9730));
 
            $!genre = $data.subbuf($offset + 97 + 301).Int;
        }
    }
 
    method !can-read-format(Buf $data --> Bool{
        self!trim-nulls($data.subbuf(0..2)) eq 'TAG'
    }
 
    method !trim-nulls(Buf $data --> Str{
        $data.decode('utf-8').subst(/\x[0000]+/'')
    }
}

要调用另一个类的私有方法,调用者必须得到被调用者的信任。信任关系用trusts声明,要信任的类必须已经声明。调用另一个类的私有方法需要该类的实例和方法的完全限定名 (FQN)。信任关系还允许访问私有属性。

class B {...}
 
class C {
    trusts B;
    has $!hidden = 'invisible';
    method !not-yours () { say 'hidden' }
    method yours-to-use () {
        say $!hidden;
        self!not-yours();
    }
}
 
class B {
    method i-am-trusted () {
        my C $c.=new;
        $c!C::not-yours();
    }
}
 
C.new.yours-to-use(); # the context of this call is GLOBAL, and not trusted by C 
B.new.i-am-trusted();

信任关系不受继承的影响。要信任全局命名空间,可以使用伪包GLOBAL

构造§

到目前为止描述的对象构造机制足以满足大多数用例。但如果实际上需要比这些机制允许的更多地调整对象构造,那么了解对象构造的更详细工作原理是很有必要的。

在构造函数方面,Raku 比许多语言都要自由。构造函数是指任何返回类实例的函数。此外,构造函数是普通方法。您从基类 Mu 继承了一个名为 new 的默认构造函数,但您可以自由地覆盖 new,就像 Task 示例中所做的那样。

method new(&callback*@dependencies{
    return self.bless(:&callback:@dependencies);
}

bless§

Raku 中的构造函数与 C# 和 Java 等语言中的构造函数之间最大的区别在于,Raku 构造函数不是在某个已经神奇地创建的对象上设置状态,而是自己创建对象。它们通过调用 bless 方法来实现,该方法也是从 Mu 继承的。bless 方法期望一组命名参数,以提供每个属性的初始值。

示例中的构造函数将位置参数转换为命名参数,以便该类可以为其用户提供更友好的构造函数。第一个参数是回调(将执行任务的函数)。其余参数是依赖的 Task 实例。构造函数将这些参数捕获到 @dependencies 贪婪数组中,并将它们作为命名参数传递给 bless(请注意,:&callback 使用变量的名称(减去符号)作为参数的名称)。应该避免在构造函数中放置除重新格式化参数之外的逻辑,因为构造函数方法不会递归调用父类。这与 Java 等语言不同。

new 声明为 method 而不是 multi method 会阻止访问默认构造函数。+因此,如果您打算保留默认构造函数,请使用 multi method new

TWEAK§

bless 从传递的值初始化类属性后,它将依次为继承层次结构中的每个类调用 TWEAKTWEAK 将接收传递给 bless 的所有参数。自定义初始化逻辑应该放在这里。

请记住始终将 TWEAK 设为 子方法,而不是普通的 method。如果在一个类层次结构中,一个类包含一个 TWEAK 方法(声明为 method 而不是 submethod),那么该方法将被继承到其子类,因此在子类的构造过程中将被调用两次!

BUILD§

可以禁用自动属性初始化,并自行执行属性初始化。为此,需要编写一个自定义的 BUILD 子方法。但是,需要注意并考虑几个边缘情况。这在 对象构造参考 中有详细说明。由于使用 BUILD 的难度,建议仅在上述其他方法都不够用时才使用它。

销毁§

Raku 是一种垃圾收集语言。这意味着通常不需要关心清理对象,因为 Raku 会自动执行此操作。但是,Raku 不会对何时清理给定对象做出任何保证。它通常只在运行时需要内存时才进行清理运行,因此我们无法依赖它何时发生。

要运行在清理对象时执行的自定义代码,可以使用 DESTROY 子方法。例如,它可以用来关闭句柄或供应,或删除不再使用的临时文件。由于垃圾收集可能发生在程序运行时的任意点,甚至可能发生在不同线程中某个完全无关的代码段的中间,因此我们必须确保在 DESTROY 子方法中不假设任何上下文。

my $in_destructor = 0;
 
class Foo {
    submethod DESTROY { $in_destructor++ }
}
 
my $foo;
for 1 .. 6000 {
    $foo = Foo.new();
}
 
say "DESTROY called $in_destructor times";

这可能会打印类似 DESTROY called 5701 times 的内容,并且可能只在我们踩过 Foo 的前几个实例数千次后才会生效。我们也不能依赖销毁的顺序。

TWEAK 相同:确保始终将 DESTROY 声明为 submethod

使用我们的类§

创建类之后,您可以创建类的实例。声明自定义构造函数提供了一种简单的方法来声明任务及其依赖项。要创建一个没有依赖项的单个任务,请编写

my $eat = Task.new({ say 'eating dinner. NOM!' });

前面部分解释了声明类 Task 会在命名空间中安装一个类型对象。此类型对象是类的“空实例”,具体来说是没有任何状态的实例。您可以对该实例调用方法,只要它们不尝试访问任何状态;new 就是一个例子,因为它创建了一个新对象,而不是修改或访问现有对象。

不幸的是,晚餐永远不会神奇地发生。它有依赖的任务

my $eat =
    Task.new({ say 'eating dinner. NOM!' },
        Task.new({ say 'making dinner' },
            Task.new({ say 'buying food' },
                Task.new({ say 'making some money' }),
                Task.new({ say 'going to the store' })
            ),
            Task.new({ say 'cleaning kitchen' })
        )
    );

注意自定义构造函数和合理的空格使用如何使任务依赖项清晰。

最后,perform 方法调用递归地按顺序调用各种其他依赖项的 perform 方法,从而给出输出

making some money
going to the store
buying food
cleaning kitchen
making dinner
eating dinner. NOM!

关于类型的说明§

声明类会创建一个新的类型对象,默认情况下,它会被安装到当前包中(就像使用our 范围声明的变量一样)。此类型对象是类的“空实例”。例如,IntStr 等类型引用了 Raku 内置类之一的类型对象。可以对这些类型对象调用方法。因此,对类型对象调用new 方法没有什么特别之处。

您可以使用.DEFINITE 方法来确定您拥有的究竟是实例还是类型对象

say Int.DEFINITE# OUTPUT: «False␤» (type object) 
say 426.DEFINITE# OUTPUT: «True␤»  (instance) 
 
class Foo {};
say Foo.DEFINITE;     # OUTPUT: «False␤» (type object) 
say Foo.new.DEFINITE# OUTPUT: «True␤»  (instance) 

在函数签名中,可以使用所谓的类型“表情符号”来仅接受实例或类型对象

multi foo (Int:U{ "It's a type object!" }
multi foo (Int:D{ "It's an instance!"   }
say foo Int# OUTPUT: «It's a type object!␤» 
say foo 42;  # OUTPUT: «It's an instance!␤» 

继承§

面向对象编程提供继承的概念作为代码重用的机制之一。Raku 支持一个类从一个或多个类继承的能力。当一个类从另一个类继承时,它会通知方法调度程序遵循继承链以查找要调用的方法。这适用于通过method 关键字定义的标准方法,也适用于通过其他方式生成的方法,例如属性访问器。

class Employee {
    has $.salary;
}
 
class Programmer is Employee {
    has @.known_languages is rw;
    has $.favorite_editor;
 
    method code_to_solve$problem ) {
        return "Solving $problem using $.favorite_editor in "
        ~ $.known_languages[0];
    }
}

现在,任何类型为 Programmer 的对象都可以使用在 Employee 类中定义的方法和访问器,就好像它们来自 Programmer 类一样。

my $programmer = Programmer.new(
    salary => 100_000,
    known_languages => <Raku Perl Erlang C++>,
    favorite_editor => 'vim'
);
 
say $programmer.code_to_solve('halting problem'),
    " will get \$ {$programmer.salary()}";
# OUTPUT: «Solving halting problem using vim in Raku will get $100000␤» 

覆盖继承的方法§

类可以通过定义自己的方法和属性来覆盖父类定义的方法和属性。下面的示例演示了Baker 类覆盖Cookcook 方法。

class Cook is Employee {
    has @.utensils  is rw;
    has @.cookbooks is rw;
 
    method cook$food ) {
        say "Cooking $food";
    }
 
    method clean_utensils {
        say "Cleaning $_" for @.utensils;
    }
}
 
class Baker is Cook {
    method cook$confection ) {
        say "Baking a tasty $confection";
    }
}
 
my $cook = Cook.new(
    utensils  => <spoon ladle knife pan>,
    cookbooks => 'The Joy of Cooking',
    salary    => 40000
);
 
$cook.cook'pizza' );       # OUTPUT: «Cooking pizza␤» 
say $cook.utensils.raku;     # OUTPUT: «["spoon", "ladle", "knife", "pan"]␤» 
say $cook.cookbooks.raku;    # OUTPUT: «["The Joy of Cooking"]␤» 
say $cook.salary;            # OUTPUT: «40000␤» 
 
my $baker = Baker.new(
    utensils  => 'self cleaning oven',
    cookbooks => "The Baker's Apprentice",
    salary    => 50000
);
 
$baker.cook('brioche');      # OUTPUT: «Baking a tasty brioche␤» 
say $baker.utensils.raku;    # OUTPUT: «["self cleaning oven"]␤» 
say $baker.cookbooks.raku;   # OUTPUT: «["The Baker's Apprentice"]␤» 
say $baker.salary;           # OUTPUT: «50000␤» 

因为调度程序会在它移动到父类之前看到Baker 上的cook 方法,所以会调用Bakercook 方法。

要访问继承链中的方法,请使用重新调度MOP

多重继承§

如前所述,一个类可以从多个类继承。当一个类从多个类继承时,调度程序知道在查找要搜索的方法时查看这两个类。Raku 使用C3 算法 来线性化多重继承层次结构,这比深度优先搜索更适合处理多重继承。

class GeekCook is Programmer is Cook {
    method new*%params ) {
        push%params<cookbooks>"Cooking for Geeks" );
        return self.bless(|%params);
    }
}
 
my $geek = GeekCook.new(
    books           => 'Learning Raku',
    utensils        => ('stainless steel pot''knife''calibrated oven'),
    favorite_editor => 'MacVim',
    known_languages => <Raku>
);
 
$geek.cook('pizza');
$geek.code_to_solve('P =? NP');

现在,Programmer 和 Cook 类提供的所有方法都可以在 GeekCook 类中使用。

虽然多重继承是一个有用的概念,可以了解和偶尔使用,但重要的是要理解还有更多有用的 OOP 概念。当使用多重继承时,最好考虑一下设计是否可以通过使用角色来更好地实现,角色通常更安全,因为它们强制类作者明确解决冲突的方法名称。有关角色的更多信息,请参见角色

also§

可以在类声明体中通过在is特质前加上also来列出要继承的类。这对于角色组合特质does也适用。

class GeekCook {
    also is Programmer;
    also is Cook;
    # ... 
}
 
role A {};
role B {};
class C {
    also does A;
    also does B;
    # ... 
}

内省§

内省是收集程序中某些对象信息的过程,不是通过阅读源代码,而是通过查询对象(或控制对象)的某些属性,例如它的类型。

给定一个对象$o和前面几节中的类定义,我们可以问它一些问题

my Programmer $o .= new;
if $o ~~ Employee { say "It's an employee" };
say $o ~~ GeekCook ?? "It's a geeky cook" !! "Not a geeky cook";
say $o.^name;
say $o.raku;
say $o.^methods(:local.name.join('');

输出可能如下所示

It's an employee
Not a geeky cook
Programmer
Programmer.new(known_languages => ["Perl", "Python", "Pascal"],
        favorite_editor => "gvim", salary => "too small")
code_to_solve, known_languages, favorite_editor

前两个测试都与类名进行智能匹配。如果对象属于该类或其继承类,则返回True。因此,所讨论的对象属于Employee类或其继承类,但不属于GeekCook类。

调用$o.^name告诉我们$o的类型;在本例中为Programmer

$o.raku返回一个可以作为 Raku 代码执行的字符串,并重现原始对象$o。虽然这在所有情况下都不完美,但对于调试简单对象非常有用。 [1]

使用.^而不是单个点调用方法的语法意味着它实际上是对其元类的调用,元类是一个管理Programmer类属性的类——或者任何您感兴趣的其他类。这个元类也支持其他内省方式

say $o.^attributes.join('');
say $o.^parents.map({ $_.^name }).join('');

最后,$o.^name调用元对象上的name方法,不出所料地返回类名。

给定一个对象$mp3和来自私有方法部分的MP3TagData类定义,我们可以使用.^methods查询其公共方法

my $mp3 = MP3TagData.new(filename => 'football-head.mp3');
say $mp3.^methods(:local);
# OUTPUT: (TWEAK filename title artist album year comment genre track version 
# type Submethod+{is-hidden-from-backtrace}.new) 

$mp3.^methods(:local)生成一个可以在$mp3上调用的Method列表。:local命名参数将返回的方法限制为在MP3TagData类中定义的方法,并排除继承的方法;MP3TagData没有继承任何类,因此提供:local没有任何区别。

要检查类型对象(或实例对象)是否实现某个公共方法,请使用.^find-method元方法,如果存在,它将返回方法对象。否则,它将返回Mu

say $mp3.^find_method('name');   # OUTPUT: «(Mu)␤» 
say $mp3.^find_method('artist'); # OUTPUT: «artist␤» 

类型对象也可以内省其私有方法。但是,公共方法和私有方法不使用相同的 API,因此必须使用不同的元方法:.^private_methods.^find_private_method

say $mp3.^private_methods;                     # OUTPUT: «(parse-data can-read-format trim-nulls)␤» 
say $mp3.^find_private_method('parse-data');   # OUTPUT: «parse-data␤» 
say $mp3.^find_private_method('remove-nulls'); # OUTPUT: «(Mu)␤» 

内省对于调试和学习语言和新库非常有用。当一个函数或方法返回一个你不知道的对象时,通过使用.^name找到它的类型,使用.raku查看它的构造配方,等等,你就能很好地了解它的返回值是什么。使用.^methods,你可以学习如何使用这个类。

但也有其他应用。例如,一个将对象序列化为一堆字节的例程需要知道该对象的属性,它可以通过内省来找出这些属性。

覆盖默认的 gist 方法§

某些类可能需要自己的gist版本,它覆盖了它们在被调用时打印的简洁方式,以提供类的默认表示。例如,异常可能只想写入payload,而不是完整的对象,这样更清楚地看到发生了什么。但是,这并不局限于异常;你可以对每个类都这样做

class Cook {
    has @.utensils  is rw;
    has @.cookbooks is rw;
 
    method cook$food ) {
        return "Cooking $food";
    }
 
    method clean_utensils {
        return "Cleaning $_" for @.utensils;
    }
 
    multi method gist(Cook:U:{ '' ~ self.^name ~ '' }
    multi method gist(Cook:D:{
        '⚗ Cooks with ' ~ @.utensils.join" ‣ "~ ' using '
          ~ @.cookbooks.map"«" ~ * ~ "»").join" and "}
}
 
my $cook = Cook.new(
    utensils => <spoon ladle knife pan>,
    cookbooks => ['Cooking for geeks','The French Chef Cookbook']);
 
say Cook.gist# OUTPUT: «⚗Cook⚗» 
say $cook.gist# OUTPUT: «⚗ Cooks with spoon ‣ ladle ‣ knife ‣ pan using «Cooking for geeks» and «The French Chef Cookbook»␤» 

通常你会想要定义两个方法,一个用于类,另一个用于类实例;在本例中,类方法使用炼金术符号,而实例方法(定义在它下面)聚合了我们关于厨师的数据,以叙述的方式显示它。

一个实用的内省示例§

当创建一个新类时,有时需要以公共方法的形式更方便地访问信息丰富(且安全的)内省。例如,以下类用于保存 CSV 电子表格中记录行的属性,其中标题行定义其字段(属性)名称。

unit class CSV-Record;
#| Field names and values for a CSV row 
has $last;
has $first;
#...more fields (attributes)... 
 
method fields(--> List{
    #| Return a list of the the attribute names (fields) 
    #| of the class instance 
    my @attributes = self.^attributes;
    my @names;
    for @attributes -> $a {
        my $name = $a.name;
        # The name is prefixed by its sigil and twigil 
        # which we don't want 
        $name ~~ s/\S\S//;
        @names.push: $name;
    }
    @names
}
 
method values(--> List{
    #| Return a list of the values for the attributes 
    #| of the class instance 
    my @attributes = self.^attributes;
    my @values;
    for @attributes -> $a {
        # Syntax is not obvious 
        my $value = $a.get_value: self;
        @values.push: $value;
    }
    @values
}

我们使用一个简单的 CSV 文件,其内容为

last,   first #...more fields...
Wall,   Larry
Conway, Damian

加载第一条记录并显示其内容

my $record = CSV-Record.new: :$last:$first;
say $record.fields.raku# OUTPUT: «["last", "first"]␤» 
say $record.values.raku# OUTPUT: «["Wall", "Larry"]␤» 

请注意,在实际应用中,我们会设计该类,使其将 fields 列表作为常量,因为其值对于所有类对象都是相同的。

constant @fields = <last first>;
method fields(--> List{
    @fields
}

使用内省方法获取属性名称的缺点包括处理时间和功耗略高,以及可能需要删除符号和 twigil 以便公开展示。

1 [↑] 例如,闭包无法通过这种方式轻松地复制;如果您不知道闭包是什么,请不要担心。此外,当前的实现存在将循环数据结构转储为字符串的问题,但预计 .raku 在某个时候会正确处理这些问题。