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 中, 一切都可以被接受。

comments powered by Disqus