Raku 编程/类和属性
到目前为止,我们已经看到了面向过程编程的构建块:表达式、分支、循环和子例程的列表,它们告诉计算机该做什么以及如何做。Raku 很好的支持面向过程编程,但这并不是 Raku 支持的唯一编程风格。Raku 很好的适应的另一个常见范式是面向对象方法。
对象是数据及其操作的组合。对象的“数据”称为其属性,对象的“操作”称为其方法。从这个意义上说,属性定义了对象的“状态”,而方法定义了对象的“行为”。
类是创建对象的模板。当提到特定类的对象时,通常将该对象称为该类的实例。
类使用 class
关键字定义,并被赋予一个名称
class MyClass {
}
在类声明内部,您可以定义属性、方法或子方法。
属性使用 has
关键字定义,并使用特殊的语法指定。例如,考虑以下类
class Point3D {
has $!x-axis;
has $!y-axis;
has $!z-axis;
}
Point2D 类定义了 3D 坐标系中的一个点,它有三个属性,分别名为 x-轴、y-轴和 z-轴。
在 Raku 中,所有属性都是私有的,一种明确表达这一点的方式是使用 ! twigil。使用 ! twigil 声明的属性只能在类内部通过使用 !attribute-name 直接访问。用这种方式声明属性的另一个重要后果是,对象不能使用默认的 new 构造函数进行填充。
如果您使用 . twigil 而不是 ! twigil 声明属性,则会自动生成一个只读访问器[检查拼写] 方法。您可以将 . twigil 视为“属性 + 访问器”。这个访问器,它是一个以其属性命名的“方法”,可以从类外部调用并返回其属性的值。为了允许通过提供的访问器更改属性,必须将 is rw
特性添加到属性中。
之前的 Point3D 类可以声明如下
class Point3D {
has $.x-axis;
has $.y-axis;
has $.z-axis is rw;
}
鉴于 . twigil 声明了一个 ! twigil 和一个访问器[检查拼写] 方法,即使属性使用 . twigil 声明,也可以始终使用 ! twigil 使用它们。
方法的定义与普通子例程相同,只是有一些关键区别
- 方法使用
method
关键字而不是sub
。 - 方法具有特殊的变量
self
,它引用正在调用方法的对象。这被称为调用者。 - 方法可以直接访问对象的内部特性。
在定义方法时,您可以为调用者指定不同的名称,而不是必须使用 self
。为此,您将其放在方法签名的开头,并用冒号将其与签名的其余部分隔开
method myMethod($invocant: $x, $y)
在此上下文中,冒号被视为一种特殊的逗号类型,因此您可以用额外的空格来写它,如果这样更容易
method myMethod($invocant : $x, $y)
以下是一个示例
class Point3D {
has $.x-axis;
has $.y-axis;
has $.z-axis;
method set-coord($new-x, $new-y, $new-z) {
$!x-axis = $new-x;
$!y-axis = $new-y;
$!z-axis = $new-z;
}
method print-point {
say "("~$!x-axis~","~$!y-axis~","~$!z-axis~")";
}
# method using the self invocant
method distance-to-center {
return sqrt(self.x-axis ** 2 + self.y-axis ** 2);
}
# method using a custom invocant named $rect
method polar-coordinates($rect:) {
my $r = $rect.distance-to-center;
my $theta = atan2($rect.y-axis, $rect.x-axis);
return "("~$r~","~$theta~","~$rect.z-axis~")";
}
}
对象是其类型为给定类的“数据项”。对象包含类定义的任何属性,并且还可以访问类中的任何方法。对象使用 new
关键字创建。
使用 Point3D 类
my $point01 = Point3D.new();
类构造函数,new()
可以接受命名方法,这些方法用于初始化类的任何属性
# Either syntax would work for object initialization
my $point01 = Point3D.new(:x-axis(3), :y-axis(4), :z-axis(6));
my $point02 = Point3D.new(x-axis => 3, y-axis => 4, z-axis => 6);
类中的方法使用点符号调用。这是对象、一个句点,然后是该方法的名称。
$point01.print-point();
say $point01.polar-coordinates();
当没有为点符号方法调用提供对象时,将使用默认变量 $_
代替
$_ = Point3D.new(:x-axis(6), :y-axis(8), :z-axis(6));;
.print-point();
say .polar-coordinates();
基本类系统使“数据”及其操作该数据的“代码例程”能够以逻辑的方式捆绑在一起。但是,大多数类系统还有更高级的功能,这些功能也允许继承,这使得类能够相互构建。继承是类形成逻辑层次结构的能力。Raku 支持类的正常继承和子类,但也支持称为mixin和角色的特殊高级功能。我们将不得不为后面的章节保留其中一些更难的功能,但我们将在本章中介绍一些基础知识。
我们在前面的章节中讨论了 Perl 的一些基本类型。您可能会惊讶地发现,所有 Raku 数据类型都是类,并且所有这些值都具有可以使用的内置方法。在这里,我们将讨论一些可以对到目前为止看到的各种对象调用的方法。
我们已经看到了内置函数 print
和 say
。所有内置类都有相同名称的方法,这些方法打印对象的字符串形式。
我们将快速讨论一下 eval
函数。eval
使我们能够在运行时编译并执行 Raku 代码字符串。
eval("say 'hello world!';");
所有 Raku 对象都具有一个名为 .perl
的方法,该方法返回表示该对象的 Raku 代码字符串。
my Int $x = 5;
$x.perl.say; # "5"
my @y = (1, 2, 3);
@y.perl.say; # "[1, 2, 3]"
my %z = :first(1), :second(2), :third(3);
%z.perl.say; # "{:first(1), :second(2), :third(3)}"
有一些方法可以显式地将给定的数据项更改为不同的形式。这就像一种强制给定的数据项以不同的上下文进行解释的显式方法。以下是一个部分列表
.item
- 返回标量上下文的项目。
.iterator
- 返回对象的迭代器。我们将在后面的章节中讨论迭代器。
.hash
- 返回哈希上下文的对象
.list
- 返回数组或“列表”上下文的对象
.Bool
- 返回对象的布尔值
.数组
- 返回包含对象数据的数组
.哈希
- 返回包含对象数据的哈希
.迭代器
- 返回对象的迭代器。我们将在后面的章节中讨论迭代器。
.标量
- 返回指向对象的标量引用
.字符串
- 返回对象的字符串表示
.WHENCE
- 返回对象类型自动创建闭包的代码引用。我们稍后将讨论自动创建和闭包。
.WHERE
- 返回数据对象的内存位置地址
.WHICH
- 返回对象的标识值,对于大多数对象来说,它就是它的内存位置(即它的 .WHERE)
.HOW
- (HOW = 高阶工作方式) 返回处理此对象的元类
.WHAT
- 返回当前对象的类型对象