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