Wait the light to fall

Raku 中的实例属性

焉知非鱼

Raku 中的实例属性 #

在 Raku 中, 默认情况下, 一个对象的方法是完全可以访问的, 但它的数据(作为属性)不能在类外直接访问, 除非明确指定。为了从外部读取、写入或两者都能访问数据, 你必须以某种方式将其公开。你允许对一个对象的数据进行何种级别的访问, 主要取决于你声明它的属性的方式。

$! twigil #

让我们从一个简单的 Raku 类定义的例子开始。

# person01.raku
class Person {
    has $!name;
}

my $john = Person.new(name => 'Suth'); # 默认地, 传递给 new
                                       # 的参数必须是命名的(键值对儿)
put $john.name; #=> Error: No such method 'name' for invocant
                # of type 'Person'.

在这个例子中, 你既不能通过新的构造函数设置一个属性的值(例如, john 的名字), 也不能检索它, 因为它根本没有被设置过。用$! twigil 声明的属性是私有的, 只能在类内通过! 这意味着即使是默认的 new 构造函数也不能在对象构造过程中用来设置一个显式的 $!-declared属性。

一个直接的解决方案是通过为我们的类实现一个 TWEAK 子方法来自己处理属性初始化。在 Raku 中, 在对象构造的不同阶段会调用多个例程, TWEAK 是其中最后一个。我不会去细说细节, 但简而言之, TWEAK 子方法允许你根据其他属性或实例变量的值为实例变量赋值。

# person02.raku
class Person {
    has $!name;

    submethod TWEAK( :$name ) {
        $!name = $name;
    }

    =begin comment
    Our TWEAK implementation involves simply setting up the attribute without 
    any validation/modification of the argument so the previous submethod
    could be reduced to:

    submethod TWEAK( :$!name ) { }

    with the attribute being set up right in the TWEAK's signature. 
    :$!name is the colon-pair version of $!name => $name.
    =end comment

}

my $john = Person.new(name => 'John');
put $john.name; #=> Error: No such method 'name' for invocant
                # of type 'Person'.

在这里, 我们已经创建了一个 TWEAK 子方法, 它接受一个命名参数(例如::$name), 并使用它来设置 $!name 属性。在我们到达这一点之前, 构造函数(在本例中为 new)将其命名参数提供给 bless 方法, 该方法建立对象并将其传递给 BUILD 子方法。之后, BUILD 将返回对象, 或者将其转到下一个也是最后一个阶段, 即 TWEAK 子方法, 如果它存在的话。

此时, 我们应该能够在对象构造时正确设置 $!name 属性, 但我们还远不能从类外访问它。我们必须创建一个 setter (或 getter)方法来实现这一目的。

# person03.raku
class Person {
    has $!name;
    
    submethod TWEAK( :$name ) {
        $!name = $name;
    }
    
    # Our accessor method
    method get-name {
        $!name
    }
}

my $john = Person.new(name => 'John');
put $john.get-name; #=> «John␤»

最后, 我们已经能够:

  • 通过 TWEAK 子方法在对象构造过程中设置一个属性, 并自己处理属性的初始化。诚然, 这只是一个简单的演示, 但在对象构造时使用 TWEAK 对属性进行更复杂的操作也是同样的步骤。

  • 在通过方法构造对象后, 检索属性的值。

这是一个有教育意义的、有深度的练习。然而, 我们经历这一切的主要原因是为了展示 Raku 通过使用下一节中介绍的构造, 为你免费提供了多少东西。让我们从 $.twigil 开始。

$. twigil #

$. twigil 声明一个属性主要有两件事。

  • 允许我们在构建对象的时候用 new 设置属性, 以及

  • 允许我们通过在属性名称后创建的方法从类外访问该属性。这个访问者方法是由 Raku 自动创建的。

让我们看看我们更新的例子。

# person04.raku
class Person {
    has $.name;
}

my $alina = Person.new(name => 'Alina');

# The method is named after its attribute
put $alina.name; #=> «Alina␤»

正如你所看到的, 对于这样一个简单的类, 我们去掉了所有不必要的模板。值得注意的是, 属性仍然必须用 ! 来访问。我们可以把 $.twigil 看作是同时创建了一个私有属性($!attr)和一个只读访问器方法(attr())。事实上, 我们可以在类内部用它的访问器访问一个属性的值, 但我们不会直接访问属性, 而是通过方法调用。因此, 根据你的目标, 你可能会倾向于直接访问一个属性, 或者如果存在的话, 在类内部通过它的访问器访问。

is rw trait #

在 Raku 中, trait 被定义为附加在对象和类上的编译器钩子, 用于修改它们的默认行为、功能或表示。

为了简单起见, 我们只限于修改对象的属性, 但如果你想了解更多关于不同的 trait 以及如何实现你自己的 trait, 请前往文档

假设我们不仅想从一个对象中读取数据, 而且还想改变它。像往常一样, 一个显而易见的解决方案是声明一个允许我们这样做的方法。这种改变对象属性的方法被称为 setter, 它通常会接受一个参数, 并将其应用于一个特定的属性。

# person05.raku
class Person {
    has $.name;

    # Set the $!name with the provided argument
    method set-name( $name ) {
        $!name = $name
    }
}

my $p1 = Person.new(name => 'Joe');
put $p1.name; #=> «Joe␤»

# Changing the person's name
$p1.set-name('Alua');

put $p1.name; #=> «Alua␤»

不过, 按照 Raku 的标准, 这还是太辛苦了。Raku 希望我们快乐, 最重要的是, 懒惰。因此, 它为我们提供了 is rw 特质来解决这个特殊的问题。这个特性将一个属性标记为读/写, 而不是默认的 is readonly 特性。当 is rw 被应用到属性时, 该属性的默认访问器(由 $. 提供)将返回一个可写的值。现在让我们相应地更新我们的类。

# person06.raku
class Person {
    has $.name is rw;
}

my $p1 = Person.new(name => 'Jana');
put $p1.name; #=> «Jana␤»

# Changing the person's name
$p1.name = 'Alua';

put $p1.name; #=> «Alua␤»

就这样, 我们摆脱了我们的自定义设置器。

请记住, 返回的是一个可写的值, 因此语法从使用方法调用(例如, $p1.name('Alua'))变为通过方法调用直接分配给对象的属性(例如, $p1.name = 'Alua')。

更多特性 #

is default trait #

默认情况下, 将 Nil 分配给一个读/写属性会将其设置为 Any。然而, 我们可能有兴趣使用一个更有意义的默认值。例如, 对于我们的 Person 类, 我们可以使用 John 作为这个人的默认名。

# person07.raku
class Person {
    has $.name is default('John') is rw;
}

# Attribute not set at construction so it gets its default value
my $p1 = Person.new();
put $p1.name; #=> «John␤»

# Setting the attribute
my $p2 = Person.new(name => 'Ruth');
put $p2.name; #=> «Ruth␤»

# Reverting it back to its default value
$p2.name = Nil;
put $p2.name; #=> «John␤»

is required trait #

正如我们在前面的例子中看到的那样(例如, my $p1 = Person.new();), 我们不需要在对象构造过程中立即提供属性的值。与你想象的不同, 这并不是因为我们使用了 is default 特性。事实上, 属性不需要在对象构造过程中进行设置, 因为默认的 new 构造函数期待命名参数, 而且它们默认是可选的。当然, 如果没有提供任何属性, 属性就不会有它们的值, 但编译器不会因为你不提供它们而大喊大叫。这时 is required 特性就派上用场了, 它将在实例化对象时标记该属性要用一个值来填充;如果没有这样做, 就会导致运行时错误。

# person08.raku
class Person {
    has $.name is required;
}

# This works fine...
my $p1 = Person.new(name => 'Rob');
$p1.name; #=> «Rob␤»

my $p2 = Person.new(); # Runtime error: The attribute '$!name' is required,
                       # but you did not provide a value for it.

正如你所想的那样, 如果将 is defaultis required 这两个特性连在一起使用, 就会导致 is default 不生效, 因为这样做意味着属性有一个默认值, 因此, 在构建对象时不需要初始化它。

is DEPRECATED trait #

在某些情况下, 有些属性可能对客户端代码不再有用, 但你可能出于某种原因想保留它们。在 Raku 中, 你可以做到这一点, 并且仍然让客户端知道这些属性已经被废弃了。为此, 你可以使用 is DEPRECATED trait 来标记一个属性为废弃的。你也可以提供一个消息告诉客户端应该使用什么来代替。让我们为我们的 Person 类添加更多具体的属性, 并阻止客户端使用 $.name 属性。

# person09.raku
class Person {
    has $.name is DEPRECATED("'firstname' and 'lastname'");
    has $.firstname is required;
    has $.lastname is required;
}

# Initialization won't trigger the warning...
my $rf = Person.new(
    name      => 'Richard Feynman',
    firstname => 'Richard',
    lastname  => 'Feynman',
);

# ...the usage will do, which will send it to STDERR.
put $rf.name;

程序运行后, 打印出以下信息。

Richard Feynman
Saw 1 occurrence of deprecated code.
================================================================================
Method name (from Person) seen at:
  person09.raku, line 16
Please use 'firstname' and 'lastname' instead.
--------------------------------------------------------------------------------
Please contact the author to have these occurrences of deprecated code
adapted, so that this message will disappear!

私有方法 #

在一开始, 我们提到 Raku 对象的方法是公开的。然而, 我们也可以将方法声明为私有方法, 在这种情况下, 它们只能在类中被调用。私有方法的声明就像一个公共方法一样, 但是方法的名称是用 ! 前缀的。它们对于将任务分解成更小的子任务, 并将它们的操作限制在类内的其他方法上是非常有用的。让我们用一个不同的例子来展示它们。

# sodamach.raku
class SodaMachine { 
    has Int $.coke-cans;
    has Int $.sprite-cans;
    has Int $.fanta-cans;
    
    has $!cost-per-can = 1.15;

    # Public method body using the private method
    method get-total-cost {
       self!get-total-cans * $!cost-per-can;
    }

    # Notice the ! in front of the method
    method !get-total-cans {
        $!coke-cans + $!sprite-cans + $!fanta-cans;
    }
}

my SodaMachine $machine .= new:
    coke-cans   => 5,
    sprite-cans => 12,
    fanta-cans  => 8
;

put $machine.get-total-cost; #=> 28.75

put $machine.get-total-cans; #=> Error: No such method 'get-total-cans'
                             # for invocant of type 'SodaMachine'.

正如你所看到的, 私有方法在类内部的调用对象(self)上用 ! 来调用。

结束语 #

正如我们在这篇文章中所证明的那样, Raku 为你提供了几种声明属性的方式, 并根据你的特定需求来定制它们。从对外界隐藏一切的对象到对数据更加慈善的对象, 在 Raku 中, 一切都可以被接受。