Raku 为 面向对象编程 (OOP) 提供了强大的支持。尽管 Raku 允许程序员使用多种范式进行编程,但面向对象编程是该语言的核心。

Raku 带有大量预定义类型,可以分为两类:常规类型和 原生 类型。您可以在变量中存储的任何内容都是原生值对象。这包括字面量、类型(类型对象)、代码和容器。

原生类型用于低级类型(如 uint64)。即使原生类型没有与对象相同的功能,如果您在它们上调用方法,它们也会自动装箱为普通对象。

所有不是原生值的东西都是对象。对象允许继承封装

使用对象§

要调用对象上的方法,请添加一个点,然后是方法名

say "abc".uc;
# OUTPUT: «ABC␤» 

这会在"abc"上调用uc方法,"abc"是类型为Str的对象。要向方法提供参数,请在方法后面的括号内添加参数。

my $formatted-text = "Fourscore and seven years ago...".indent(8);
say $formatted-text;
# OUTPUT: «        Fourscore and seven years ago...␤» 

$formatted-text现在包含上面的文本,但缩进了 8 个空格。

多个参数用逗号分隔

my @words = "Abe""Lincoln";
@words.push("said"$formatted-text.comb(/\w+/));
say @words;
# OUTPUT: «[Abe Lincoln said (Fourscore and seven years ago)]␤» 

类似地,可以通过在方法后放置一个冒号并将参数列表用逗号分隔来指定多个参数

say @words.join('--').subst: 'years''DAYS';
# OUTPUT: «Abe--Lincoln--said--Fourscore and seven DAYS ago␤» 

由于如果要传递没有括号的参数,则必须在方法后放置一个:,因此没有冒号或括号的方法调用明确地表示没有参数列表的方法调用

say 4.log:   ; # OUTPUT: «1.38629436111989␤» ( natural logarithm of 4 ) 
say 4.log: +2# OUTPUT: «2␤» ( base-2 logarithm of 4 ) 
say 4.log  +2# OUTPUT: «3.38629436111989␤» ( natural logarithm of 4, plus 2 )

许多看起来不像方法调用的操作(例如,智能匹配或将对象插入字符串)可能在幕后导致方法调用。

方法可以返回可变容器,在这种情况下,您可以将方法调用的返回值分配给容器。这就是使用对象的可读写属性的方式

$*IN.nl-in = "\r\n";

在这里,我们在$*IN对象上调用方法nl-in,没有参数,并使用=运算符将其分配给它返回的容器。

所有对象都支持来自类Mu的方法,它是类型层次结构的根。所有对象都派生自Mu

对象相等需要一个特定的运算符:eqv,结构比较运算符

class Foo {
    has $.bar;
    has $.baz
};
my $bar = "42";
my $baz = 24;
my $zipi = Foo.new:$baz:$bar);
my $zape = Foo.new:$bar:$baz);
say $zipi eqv $zape# OUTPUT: «True␤» 

另一方面,对象标识使用===。例如,在上面使用它将返回False

类型对象§

类型本身是对象,您可以通过编写其名称来获取类型对象

my $int-type-obj = Int;

您可以通过调用WHAT方法来请求任何事物的类型对象,该方法实际上是方法形式的宏

my $int-type-obj = 1.WHAT;

类型对象(除了Mu)可以使用===标识运算符进行相等比较

sub f(Int $x{
    if $x.WHAT === Int {
        say 'you passed an Int';
    }
    else {
        say 'you passed a subtype of Int';
    }
}

虽然在大多数情况下,.isa方法就足够了

sub f($x{
    if $x.isa(Int{
        ...
    }
    ...
}

子类型检查通过智能匹配完成

if $type ~~ Real {
    say '$type contains Real or a subtype thereof';
}

§

类使用class关键字声明,通常后跟一个名称。

class Journey { }

此声明会创建一个类型对象,并将其安装在当前包和当前词法作用域中,名称为Journey。您也可以词法地声明类

my class Journey { }

这将限制它们对当前词法作用域的可见性,如果类是嵌套在模块或另一个类中的实现细节,这将很有用。

属性§

属性是每个类实例存在的变量;当实例化为一个值时,变量与其值之间的关联称为属性。它们是存储对象状态的地方。在 Raku 中,所有属性都是私有的,这意味着只有类实例本身才能直接访问它们。它们通常使用has声明符和!修饰符声明。

class Journey {
    has $!origin;
    has $!destination;
    has @!travelers;
    has $!notes;
}

或者,您可以省略修饰符,这仍然会创建私有属性(带有!修饰符),并且还会创建一个别名,将名称(没有修饰符)绑定到该属性。因此,您可以使用以下方法声明上面的同一个类

class Journey {
    has $origin;
    has $destination;
    has @travelers;
    has $notes;
}

如果您像这样声明类,您随后可以使用或不使用修饰符访问属性 - 例如,$!origin$origin 指的是同一个属性。

虽然没有公共(甚至受保护的)属性,但有一种方法可以自动生成访问器方法:将 `!` 符号替换为 `.` 符号(`.` 应该让你想起方法调用)。

class Journey {
    has $.origin;
    has $.destination;
    has @!travelers;
    has $.notes;
}

这默认提供只读访问器。为了允许更改属性,请添加 is rw 特性。

class Journey {
    has $.origin;
    has $.destination;
    has @!travelers;
    has $.notes is rw;
}

现在,在创建 `Journey` 对象后,它的 `.origin`、`.destination` 和 `.notes` 都可以从类外部访问,但只有 `.notes` 可以修改。

如果在没有某些属性(如 origin 或 destination)的情况下实例化对象,我们可能无法获得预期结果。为了防止这种情况,请提供默认值或确保在对象创建时设置属性,方法是使用 is required 特性标记属性。

class Journey {
    # error if origin is not provided 
    has $.origin is required;
    # set the destination to Orlando as default (unless that is the origin!) 
    has $.destination = self.origin eq 'Orlando' ?? 'Kampala' !! 'Orlando';
    has @!travelers;
    has $.notes is rw;
}

由于类从 Mu 继承了默认构造函数,并且我们已经请求为我们生成一些访问器方法,因此我们的类已经具有一定的功能。

# Create a new instance of the class. 
my $vacation = Journey.new(
    origin      => 'Sweden',
    destination => 'Switzerland',
    notes       => 'Pack hiking gear!'
);
 
# Use an accessor; this outputs Sweden. 
say $vacation.origin;
 
# Use an rw accessor to change the value. 
$vacation.notes = 'Pack hiking gear and sunglasses!';

请注意,虽然默认构造函数可以初始化只读属性,但它只会设置具有访问器方法的属性。也就是说,即使你将 `travelers => ["Alex", "Betty"]` 传递给默认构造函数,属性 `@!travelers` 也不会被初始化。

方法§

方法在类体中使用 `method` 关键字声明。

class Journey {
    has $.origin;
    has $.destination;
    has @!travelers;
    has $.notes is rw;
 
    method add-traveler($name{
        if $name ne any(@!travelers{
            push @!travelers$name;
        }
        else {
            warn "$name is already going on the journey!";
        }
    }
 
    method describe() {
        "From $!origin to $!destination"
    }
}

方法可以有签名,就像子程序一样。属性可以在方法中使用,并且始终可以使用 `!` 符号,即使它们使用 `.` 符号声明。这是因为 `.` 符号声明 `!` 符号并生成访问器方法。

查看上面的代码,在 `describe` 方法中使用 `$!origin` 和 `$.origin` 之间存在细微但重要的区别。`$!origin` 是对属性的廉价且明显的查找。`$.origin` 是方法调用,因此可以在子类中被覆盖。只有在你想要允许覆盖时才使用 `$.origin`。

与子程序不同,额外的命名参数不会产生编译时或运行时错误。这允许通过 重新调度 来链接方法。

你可以编写自己的访问器来覆盖任何或所有自动生成的访问器。

my $ = " " xx 4# A tab-like thing 
class Journey {
    has $.origin;
    has $.destination;
    has @.travelers;
    has Str $.notes is rw;
 
    multi method notes() { "$!notes\n" };
    multi method notesStr $note ) { $!notes ~= "$note\n$" };
 
    method Str { "⤷ $!origin\n$" ~ self.notes() ~ "$!destination ⤶\n" };
}
 
my $trip = Journey.new:origin<Here>:destination<There>,
                        travelers => <þor Freya> );
 
$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;
 
# OUTPUT: 
#⤷ Here 
#       First steps 
#       Almost there 
# 
#There ⤶ 

声明的多方法 `notes` 覆盖了在 `$.notes` 声明中隐含的自动生成方法,使用不同的签名来读取和写入。

请注意,在 `notes $trip: "Almost there"` 中,我们使用的是 间接调用语法,它首先放置方法名称,然后放置对象,然后用冒号分隔参数:`method invocant: arguments`。只要感觉比经典的句点和括号语法更自然,我们就可以使用这种语法。它的工作方式完全相同。

注意 `Str` 方法中对 `notes` 方法的调用是如何在 `self` 上进行的。以这种方式编写方法调用会将方法的返回值保留为容器的原样。为了将返回值容器化,你可以在 sigil 上而不是 `self` 上进行方法调用。这会根据用于容器化的 sigil 在返回值上调用各种方法。

Sigil方法
$item
@list
%hash
&item

例如,`Journey` 的 `Str` 方法可以重写为不使用 `~` 运算符,方法是在它返回的字符串中嵌入一个带 sigil 的方法调用。

method Str { "⤷ $!origin\n$$.notes()$!destination ⤶\n" }

用于更新 `$.notes` 的语法在本节中相对于之前的 属性 部分有所改变。不再是赋值

$vacation.notes = 'Pack hiking gear and sunglasses!';

我们现在进行方法调用

$trip.notes("First steps");

覆盖默认的自动生成的访问器意味着它不再可用,无法在返回时提供可变容器以进行赋值。方法调用是为更新属性添加计算和逻辑的首选方法(参见下一节中此类调用的“编程使用”)。许多现代语言可以通过用“setter”方法重载赋值来更新属性。虽然 Raku 可以使用 Proxy 对象为此目的重载赋值运算符,但目前不鼓励使用重载赋值来使用复杂逻辑设置属性,因为 较弱的面向对象设计

类方法和实例方法§

方法名称可以在运行时使用 ."" 运算符解析,这使得可以使用编程方式使用这些名称。例如,由于属性名称也是一种方法,我们可以通过调用 a 方法来显示属性 a 的值。

class A { has $.a = 9 }
my $method = 'a';
A.new."$method"().say;
# OUTPUT: «9␤» 

带参数的方法可以以类似的方式调用。

class B {
    has $.b = 9;
    method mul($n{
        $!b * $n
    }
}
my $method = 'mul';
B.new."$method"(6).say;
# OUTPUT: «54␤» 

方法的签名可以以显式调用者作为其第一个参数,后跟冒号,这允许方法引用它被调用的对象。

class Foo {
    method greet($me: $person{
        say "Hi, I am $me.^name(), nice to meet you, $person";
    }
}
Foo.new.greet("Bob");    # OUTPUT: «Hi, I am Foo, nice to meet you, Bob␤» 

在方法签名中提供调用者还可以通过使用 类型约束 将方法定义为类方法或对象方法。::?CLASS 变量可用于在编译时提供类名,并与 :U(用于类方法)或 :D(用于实例方法)结合使用。

class Pizza {
    has $!radius = 42;
    has @.ingredients;
 
    # class method: construct from a list of ingredients 
    method from-ingredients(::?CLASS:U $pizza: @ingredients{
        $pizza.newingredients => @ingredients );
    }
 
    # instance method 
    method get-radius(::?CLASS:D:{ $!radius }
}
my $p = Pizza.from-ingredients: <cheese pepperoni vegetables>;
say $p.ingredients;     # OUTPUT: «[cheese pepperoni vegetables]␤» 
say $p.get-radius;      # OUTPUT: «42␤» 
say Pizza.get-radius;   # This will fail. 
CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:␤ 
#          Invocant of method 'get-radius' must be 
#          an object instance of type 'Pizza', 
#          not a type object of type 'Pizza'. 
#          Did you forget a '.new'?» 

方法可以使用 multi 声明符同时作为类方法和对象方法。

class C {
    multi method f(::?CLASS:U:{ say "class method"  }
    multi method f(::?CLASS:D:{ say "object method" }
}
C.f;       # OUTPUT: «class method␤» 
C.new.f;   # OUTPUT: «object method␤» 

self§

在方法内部,可以使用 self 并且它绑定到调用者对象。self 可用于在调用者上调用更多方法,包括构造函数。

class Box {
  has $.data;
 
  method make-new-box-from() {
      self.new: data => $!data;
  }
}

self 可用于类方法或实例方法,但请注意不要尝试从一种类型的方法调用另一种类型的方法。

class C {
    method g()            { 42     }
    method f(::?CLASS:U:{ self.g }
    method d(::?CLASS:D:{ self.f }
}
C.f;        # OUTPUT: «42␤» 
C.new.d;    # This will fail. 
CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:␤ 
#          Invocant of method 'f' must be a type object of type 'C', 
#          not an object instance of type 'C'.  Did you forget a 'multi'?» 

self 也可以与属性一起使用,只要它们具有访问器。self.a 将调用为 has $.a 声明的属性的访问器。但是,self.a$.a 之间存在差异,因为后者将进行项化;$.a 等效于 self.a.item$(self.a)

class A {
    has Int @.numbers;
    has $.x = (123);
 
    method show-diff() { .say for self.x.say for $.x }
 
    method twice  { self.times: 2 }
    method thrice { $.times: 3    }
 
    method times($val = 1{ @!numbers.map(* * $val).list }
};
 
my $obj = A.new(numbers => [123]);
$obj.show-diff;   # OUTPUT: «1␤2␤3␤(1 2 3)␤» 
say $obj.twice;   # OUTPUT: «(2 4 6)␤» 
say $obj.thrice;  # OUTPUT: «(3 6 9)␤» 

方法参数的冒号语法支持使用 self 或快捷方式进行方法调用,如上面示例中的 twicethrice 方法所示。

请注意,如果相关方法 blessCREATEMu 未重载,则 self 将在这些方法中指向类型对象。

另一方面,子方法 BUILDTWEAK 在初始化的不同阶段被调用到实例上。来自子类的同名子方法尚未运行,因此您不应依赖这些方法内部的潜在虚拟方法调用。

私有方法§

方法名称前带有感叹号 ! 的方法无法从定义类的外部任何地方调用;此类方法是私有的,因为它们在声明它们的类之外不可见。私有方法使用感叹号而不是点来调用。

class FunMath {
    has $.value is required;
    method !do-subtraction$num ) {
        if $num ~~ Str {
            return $!value + (-1 * $num.chars);
        }
        return $!value + (-1 * $num);
    }
    method minus$minuend: $subtrahend ) {
        # invoking the private method on the explicit invocant 
        $minuend!do-subtraction($subtrahend);
    }
}
my $five = FunMath.new(value => 5);
say $five.minus(6);         # OUTPUT: «-1␤» 
 
say $five.do-subtraction(6);
CATCH { default { put .^name ~ ":\n" ~ .Str } }
# OUTPUT: «X::Method::NotFound: 
# No such method 'do-subtraction' for invocant of type 
# 'FunMath'. Did you mean '!do-subtraction'?␤» 

私有方法有自己的命名空间。它们不是虚拟的,即私有方法不能在继承类中被重写以提供任何多态行为,因此在编译时会检测到缺少的私有方法。与某些语言中 private 是方法的 访问修饰符 不同,在 Raku 中,“私有方法”和“方法”是截然不同的东西——也就是说,最好将“私有方法”读作复合名词,而不是形容词来描述名词。

私有方法不会被子类继承。

子方法§

子方法是不会被子类继承的公共方法。名称源于它们在语义上类似于子例程。

子方法对于对象构造和销毁任务很有用,以及对于特定于特定类型的任务,子类型肯定需要重写这些任务。

例如,默认方法 new继承 链中的每个类上调用子方法 BUILD

class Point2D {
    has $.x;
    has $.y;
 
    submethod BUILD(:$!x:$!y{
        say "Initializing Point2D";
    }
}
 
class InvertiblePoint2D is Point2D {
    submethod BUILD() {
        say "Initializing InvertiblePoint2D";
    }
    method invert {
        self.new(x => - $.x=> - $.y);
    }
}
 
say InvertiblePoint2D.new(x => 1=> 2);
# OUTPUT: «Initializing Point2D␤» 
# OUTPUT: «Initializing InvertiblePoint2D␤» 
# OUTPUT: «InvertiblePoint2D.new(x => 1, y => 2)␤» 

另请参阅:对象构造

继承§

类可以有父类

class Child is Parent1 is Parent2 { }

如果在子类上调用一个方法,而子类没有提供该方法,则会调用父类中的同名方法(如果存在)。父类被查询的顺序称为方法解析顺序(MRO)。Raku 使用 C3 方法解析顺序。可以通过调用类的元类来查询其 MRO。

say List.^mro;      # OUTPUT: «((List) (Cool) (Any) (Mu))␤» 

如果一个类没有指定父类,则默认情况下会假定为 Any。所有类都直接或间接地派生自 Mu,它是类型层次结构的根。

对公共方法的所有调用在 C++ 意义上都是“虚拟”的,这意味着调用哪个方法由对象的实际类型决定,而不是声明类型。

class Parent {
    method frob {
        say "the parent class frobs"
    }
}
 
class Child is Parent {
    method frob {
        say "the child's somewhat more fancy frob is called"
    }
}
 
my Parent $test;
$test = Child.new;
$test.frob;          # calls the frob method of Child rather than Parent 
# OUTPUT: «the child's somewhat more fancy frob is called␤» 

如果要显式地调用子对象上的父方法,请在父命名空间中引用其完整名称。

$test.Parent::frob;  # calls the frob method of Parent 
# OUTPUT: «the parent class frobs␤» 

委托§

委托是一种技术,其中一个对象(委托者)接受方法调用,但指定另一个对象(被委托者)来代替它处理调用。换句话说,委托者被委托者的一个或多个方法发布为自己的方法。

在 Raku 中,委托是通过将 handles 特性应用于属性来指定的。提供给特性的参数指定了对象和被委托者属性将共有的方法。除了方法名称列表之外,还可以提供一个 Pair(重命名;键成为新名称)、一个 Regex(处理所有具有匹配名称的方法)、一个 Whatever(委托属性 can 调用的所有方法),或一个 HyperWhatever(委托所有方法调用,即使是会导致属性的 FALLBACK 方法的方法)。还可以提供一个 List,其中包含任何这些项目以委托多个方法。请注意,RegexWhateverHyperWhatever 形式不会委托类继承的任何方法(例如,来自 AnyMu),但显式命名方法会委托它。

class Book {
    has Str  $.title;
    has Str  $.author;
    has Str  $.language;
    has Cool $.publication;
}
 
class Product {
    has Book $.book handles('title''author''language'year => 'publication');
}
 
my $book = Book.new:
    :title<Dune>,
    :author('Frank Herbert'),
    :language<English>,
    :publication<1965>
;
 
given Product.new(:$book{
    say .title;    # OUTPUT: «Dune␤» 
    say .author;   # OUTPUT: «Frank Herbert␤» 
    say .language# OUTPUT: «English␤» 
    say .year;     # OUTPUT: «1965␤» 
}

在上面的示例中,类 Product 定义了属性 $.book 并用 handles 特性标记它,以指定在 Product 类的实例对象上调用时将转发到 Book 类的那些方法。这里有几点需要注意。

  • 我们没有在 Product 类中编写任何我们在其实例对象中调用的方法。相反,我们指示该类将对这些方法的任何调用委托给 Book 类。

  • 我们指定了方法名称 titleauthorlanguage,因为它们出现在 Book 类中。另一方面,我们通过提供适当的 Pairpublication 方法重命名为 year

委托可以通过委托给父类而不是继承其所有方法来用作继承的替代方法。例如,以下 Queue 类将队列特有的几个方法委托给 Array 类,同时还为其中一些方法提供了首选接口(例如,enqueue 用于 push)。

class Queue {
    has @!q handles(
        enqueue => 'push'dequeue => 'shift',
        'push''shift''head''tail''elems''splice'
    );
 
    method gist {
        '[' ~ @!q.join(''~ ']'
    }
}
 
my Queue $q .= new;
$q.enqueue($_for 1..5;
$q.push(6);
say $q.shift;                  # OUTPUT: «1␤» 
say $q.dequeue while $q.elems# OUTPUT: «2␤3␤4␤5␤6␤» 
 
$q.enqueue($_for <Perl Python Raku Ruby>;
say $q.head;                   # OUTPUT: «Perl␤» 
say $q.tail;                   # OUTPUT: «Ruby␤» 
say $q;                        # OUTPUT: «[Perl, Python, Raku, Ruby]␤» 
$q.dequeue while $q.elems;
say $q;                        # OUTPUT: «[]␤» 

对象构造§

对象通常是通过方法调用创建的,这些方法调用要么在类型对象上,要么在同一类型的另一个对象上。

Mu 提供了一个名为 new 的构造函数方法,它接受命名 参数 并使用它们来初始化公共属性。

class Point {
    has $.x;
    has $.y;
}
my $p = Point.newx => 5=> 2);
#             ^^^ inherited from class Mu 
say "x: "$p.x;
say "y: "$p.y;
# OUTPUT: «x: 5␤» 
# OUTPUT: «y: 2␤» 

Mu.new 在其调用者上调用方法 bless,并将所有命名 参数 传递给它。bless 创建新对象,然后按相反的方法解析顺序(即从 Mu 到最派生类)遍历所有子类。在每个类中,bless 按以下顺序执行以下步骤。

  • 它检查是否存在名为 BUILD 的方法。如果方法存在,则使用它接收到的所有命名参数(来自 new 方法)调用该方法。

  • 如果未找到 BUILD 方法,则从具有相同名称的命名参数初始化此类的公共属性。

  • 所有在任何先前步骤中未被触及的属性都将应用其默认值

    has $.attribute = 'default value';

  • 如果存在,则调用 TWEAK。它将接收与 BUILD 相同的参数。

这种对象构造方案有几个含义

  • 默认 new 构造函数(继承自 Mu)的命名参数可以直接对应于方法解析顺序中任何类的公共属性,或对应于任何 BUILDTWEAK 子方法的任何命名参数。

  • 自定义 BUILD 方法应始终是子方法,否则它们将被继承到子类,并阻止默认属性初始化(上述列表中的第二项),除非子类有自己的 BUILD 方法。

  • BUILD 可以设置属性,但它无法访问声明为其默认值的属性的内容,因为这些内容仅在稍后应用。另一方面,TWEAK 在应用默认值后被调用,因此将找到已初始化的属性。因此,它可用于在对象构造后检查事物或修改属性

    class RectangleWithCachedArea {
        has ($.x1$.x2$.y1$.y2);
        has $.area;
        submethod TWEAK() {
            $!area = abs( ($!x2 - $!x1* ( $!y2 - $!y1) );
        }
    }
     
    say RectangleWithCachedArea.newx2 => 5x1 => 1y2 => 1y1 => 0).area;
    # OUTPUT: «4␤» 
  • 由于将参数传递给例程会将参数绑定到参数,因此可以使用属性作为参数来简化 BUILD 方法。

    使用 BUILD 方法中普通绑定的类

    class Point {
        has $.x;
        has $.y;
     
        submethod BUILD(:$x:$y{
            $!x := $x;
            $!y := $y;
        }
    }
    my $p1 = Point.newx => 10=> 5 );

    以下 BUILD 方法等效于上述方法

    submethod BUILD(:$!x:$!y{
        # Nothing to do here anymore, the signature binding 
        # does all the work for us. 
    }
  • 为了将默认值与 `BUILD()` 方法一起使用,不能使用属性的参数绑定,因为这将始终触及属性,从而阻止自动分配默认值(上述列表中的第三步)。相反,需要有条件地分配值

    class A {
        has $.attr = 'default';
        submethod BUILD(:$attr{
            $!attr = $attr if defined $attr;
        }
    }
    say A.new(attr => 'passed').raku;
    say A.new().raku;
    # OUTPUT: «A.new(attr => "passed")␤» 
    # OUTPUT: «A.new(attr => "default")␤» 

    但是,设置 `BUILD` 参数的默认值更简单

    class A {
        has $.attr;
        submethod BUILD(:$!attr = 'default'{}
    }
  • 当属性具有特殊类型要求(例如 Int 类型)时,在使用属性的参数绑定时要小心。如果在没有此参数的情况下调用 new,则将分配 Any 的默认值,这将导致类型错误。简单的解决方法是为 BUILD 参数添加默认值。

    class A {
        has Int $.attr;
        submethod BUILD(:$!attr = 0{}
    }
    say A.new(attr => 1).raku;
    say A.new().raku;
    # OUTPUT: «A.new(attr => 1)␤» 
    # OUTPUT: «A.new(attr => 0)␤» 
  • BUILD 允许为属性初始化创建别名

    class EncodedBuffer {
        has $.enc;
        has $.data;
     
        submethod BUILD(:encoding(:$!enc), :$!data{ }
    }
    my $b1 = EncodedBuffer.newencoding => 'UTF-8'data => [6465] );
    my $b2 = EncodedBuffer.newenc      => 'UTF-8'data => [6465] );
    #  both enc and encoding are allowed now 
  • 请注意,名称 new 在 Raku 中并不特殊。它仅仅是一个常见的约定,在 大多数 Raku 类 中得到了很好的遵循。您可以从任何方法调用 bless,或使用 CREATE 来处理低级工作。

  • 如果您想要一个接受位置参数的构造函数,则必须编写自己的 new 方法

    class Point {
        has $.x;
        has $.y;
        method new($x$y{
            self.bless(:$x:$y);
        }
    }

    但是请注意,new 是一个普通方法,不参与 bless 的任何构造过程。因此,当使用不同的 new 方法或子类的 new 时,不会调用放置在 new 方法中的任何逻辑。

    class Vector {
        has $.x;
        has $.y;
        has $.length;
        method new($x$y{
            self.bless(:$x:$ylength => sqrt($x**2 * $y**2));
        }
    }
     
    class NamedVector is Vector {
        has $.name;
        method new($name$x$y{
            self.bless(:$name:$x:$y);
        }
    }
     
    my $v = Vector.new: 34;
    say $v.length# OUTPUT: «5␤» 
     
    my $f = NamedVector.new: 'Francis'512;
    say $f.length# OUTPUT: «(Any)␤» 

这是一个示例,我们用自动递增的 ID 丰富 Str

class Str-with-ID is Str {
    my $counter = 0;
    has Int $.ID  is rw = 0;
 
    multi method new$str ) {
        self.blessvalue => $str );
    }
    submethod BUILD:$!ID = $counter++ ) {}
}
 
say Str-with-ID.new("1.1,2e2").ID;                  # OUTPUT: «0␤» 
my $enriched-str = Str-with-ID.new("3,4");
say "$enriched-str{$enriched-str.^name}{$enriched-str.ID}";
# OUTPUT: «3,4, Str-with-ID, 1␤» 

我们创建了一个自定义 new,因为我们希望能够使用裸字符串初始化新类。bless 将调用 Str.BUILD,它将捕获它正在寻找的值,即对 value => $str 的配对,并初始化自身。但我们还必须初始化子类的属性,这就是为什么我们在 BUILD 中初始化 $.ID 的原因。如输出所示,对象将使用 ID 正确初始化,并且可以像普通的 Str 一样使用。

对象克隆§

克隆是使用所有对象上可用的 clone 方法完成的,该方法浅克隆公共和私有属性。可以将公共属性的新值作为命名参数提供。

class Foo {
    has $.foo = 42;
    has $.bar = 100;
}
 
my $o1 = Foo.new;
my $o2 = $o1.clone: :bar(5000);
say $o1# OUTPUT: «Foo.new(foo => 42, bar => 100)␤» 
say $o2# OUTPUT: «Foo.new(foo => 42, bar => 5000)␤» 

有关非标量属性如何克隆以及实现自己的自定义克隆方法的示例,请参阅 clone 的文档。

角色§

角色是属性和方法的集合;但是,与类不同,角色仅用于描述对象行为的一部分;这就是为什么通常角色旨在混合到类和对象中的原因。通常,类用于管理对象,而角色用于管理对象内的行为和代码重用。

角色使用关键字 role 作为声明的角色名称的前缀。角色使用 does 关键字作为混合的角色名称的前缀进行混合。

可以使用 `is` 将角色混合到类中。但是,`is` 与角色的语义与 `does` 提供的语义大不相同。使用 `is`,类从角色中被“戏仿”,然后从它继承。因此,没有扁平化组合,也没有 `does` 提供的任何安全性。

constant ⲧ = " " xx 4#Just a ⲧab 
role Notable {
    has Str $.notes is rw;
 
    multi method notes() { "$!notes\n" };
    multi method notesStr $note ) { $!notes ~= "$note\n" ~ ⲧ };
 
}
 
class Journey does Notable {
    has $.origin;
    has $.destination;
    has @.travelers;
 
    method Str { "⤷ $!origin\n" ~ ⲧ ~ self.notes() ~ "$!destination ⤶\n" };
}
 
my $trip = Journey.new:origin<Here>:destination<There>,
                        travelers => <þor Freya> );
 
$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;
# OUTPUT: 
#⤷ Here 
#       First steps 
#       Almost there 
# 
#There ⤶ 

角色在编译器解析角色声明的闭合花括号时就变得不可变。

应用角色§

角色应用与类继承有很大不同。当角色应用于类时,该角色的方法会被复制到类中。如果将多个角色应用于同一个类,冲突(例如,相同名称的属性或非多方法)会导致编译时错误,可以通过在类中提供相同名称的方法来解决。

这比多重继承安全得多,在多重继承中,冲突永远不会被编译器检测到,而是被解析为在方法解析顺序中更早出现的超类,这可能不是程序员想要的。

例如,如果你发现了一种高效的骑牛方法,并试图将其作为一种新的流行交通方式推向市场,你可能有一个类 `Bull`,用于你家周围所有的公牛,还有一个类 `Automobile`,用于你可以驾驶的东西。

class Bull {
    has Bool $.castrated = False;
    method steer {
        # Turn your bull into a steer 
        $!castrated = True;
        return self;
    }
}
class Automobile {
    has $.direction;
    method steer($!direction{ }
}
class Taurus is Bull is Automobile { }
 
my $t = Taurus.new;
say $t.steer;
# OUTPUT: «Taurus.new(castrated => Bool::True, direction => Any)␤» 

在这种情况下,你的可怜的客户会发现他们无法转向他们的金牛座,而你将无法生产更多产品!在这种情况下,使用角色可能更好。

role Bull-Like {
    has Bool $.castrated = False;
    method steer {
        # Turn your bull into a steer 
        $!castrated = True;
        return self;
    }
}
role Steerable {
    has Real $.direction;
    method steer(Real $d = 0{
        $!direction += $d;
    }
}
class Taurus does Bull-Like does Steerable { }

这段代码将以类似于以下内容的方式死亡

===SORRY!===
Method 'steer' must be resolved by class Taurus because it exists in
multiple roles (Steerable, Bull-Like)

此检查将为你节省很多麻烦

class Taurus does Bull-Like does Steerable {
    method steer($direction?{
        self.Steerable::steer($direction)
    }
}

当角色应用于第二个角色时,实际应用会被延迟,直到第二个角色应用于类,此时两个角色都会应用于类。因此

role R1 {
    # methods here 
}
role R2 does R1 {
    # methods here 
}
class C does R2 { }

生成与以下相同的类 `C`

role R1 {
    # methods here 
}
role R2 {
    # methods here 
}
class C does R1 does R2 { }

存根§

当角色包含一个存根方法时,即代码限制为 `...` 的方法,必须在角色应用于类时提供相同名称的非存根版本的方法。这允许你创建充当抽象接口的角色。

role AbstractSerializable {
    method serialize() { ... }        # literal ... here marks the 
                                      # method as a stub 
}
 
# the following is a compile time error, for example 
#        Method 'serialize' must be implemented by Point because 
#        it's required by a role 
 
class APoint does AbstractSerializable {
    has $.x;
    has $.y;
}
 
# this works: 
class SPoint does AbstractSerializable {
    has $.x;
    has $.y;
    method serialize() { "p($.x$.y)" }
}

存根方法的实现也可以由另一个角色提供。

继承§

角色不能从类继承,但它们可以携带类,导致任何执行该角色的类都从携带的类继承。因此,如果你写

role A is Exception { }
class X::Ouch does A { }
X::Ouch.^parents.say # OUTPUT: «((Exception))␤» 

那么 `X::Ouch` 将直接从 Exception 继承,正如我们上面通过列出它的父类所看到的那样。

由于它们不使用可以正确称为继承的东西,因此角色不是类层次结构的一部分。角色使用 `.^roles` 元方法列出,该方法使用 `transitive` 作为标志来包含所有级别或仅包含第一个级别。尽管如此,仍然可以使用智能匹配或类型约束来测试类或实例,以查看它是否执行了某个角色。

role F { }
class G does F { }
G.^roles.say;                    # OUTPUT: «((F))␤» 
role Ur {}
role Ar does Ur {}
class Whim does Ar {}Whim.^roles(:!transitive).say;   # OUTPUT: «((Ar))␤» 
say G ~~ F;                      # OUTPUT: «True␤» 
multi a (F $a{ "F".say }
multi a ($a)   { "not F".say }
a(G);                            # OUTPUT: «F␤» 

啄食顺序§

直接在类中定义的方法将始终覆盖来自应用角色或从继承类中的定义。如果不存在此类定义,来自角色的方法将覆盖从类继承的方法。这发生在该类是由角色引入时,也发生在该类直接继承时。

role M {
  method f { say "I am in role M" }
}
 
class A {
  method f { say "I am in class A" }
}
 
class B is A does M {
  method f { say "I am in class B" }
}
 
class C is A does M { }
 
B.new.f# OUTPUT: «I am in class B␤» 
C.new.f# OUTPUT: «I am in role M␤» 

请注意,每个多方法的候选者都是它自己的方法。在这种情况下,上述内容仅适用于两个这样的候选者具有相同的签名。否则,不存在冲突,候选者只是被添加到多方法中。

自动角色戏仿§

任何尝试直接实例化角色或将其用作类型对象的行为都会自动创建一个与角色同名的类,使其能够透明地使用角色,就好像它是一个类一样。

role Point {
    has $.x;
    has $.y;
    method abs { sqrt($.x * $.x + $.y * $.y}
    method dimensions { 2 }
}
say Point.new(x => 6=> 8).abs# OUTPUT: «10␤» 
say Point.dimensions;              # OUTPUT: «2␤» 

我们称这种自动创建类为戏仿,并将生成的类称为戏仿

大多数元编程结构不会导致戏仿,因为它们有时用于直接处理角色。

参数化角色§

角色可以通过在方括号中给出签名来进行参数化

role BinaryTree[::Type{
    has BinaryTree[Type$.left;
    has BinaryTree[Type$.right;
    has Type $.node;
 
    method visit-preorder(&cb{
        cb $.node;
        for $.left$.right -> $branch {
            $branch.visit-preorder(&cbif defined $branch;
        }
    }
    method visit-postorder(&cb{
        for $.left$.right -> $branch {
            $branch.visit-postorder(&cbif defined $branch;
        }
        cb $.node;
    }
    method new-from-list(::?CLASS:U: *@el{
        my $middle-index = @el.elems div 2;
        my @left         = @el[0 .. $middle-index - 1];
        my $middle       = @el[$middle-index];
        my @right        = @el[$middle-index + 1 .. *];
        self.new(
            node    => $middle,
            left    => @left  ?? self.new-from-list(@left)  !! self,
            right   => @right ?? self.new-from-list(@right!! self,
        );
    }
}
 
my $t = BinaryTree[Int].new-from-list(456);
$t.visit-preorder(&say);    # OUTPUT: «5␤4␤6␤» 
$t.visit-postorder(&say);   # OUTPUT: «4␤6␤5␤» 

这里签名只包含一个类型捕获,但任何签名都可以

enum Severity <debug info warn error critical>;
 
role Logging[$filehandle = $*ERR{
    method log(Severity $sev$message{
        $filehandle.print("[{uc $sev}$message\n");
    }
}
 
Logging[$*OUT].log(debug'here we go'); # OUTPUT: «[DEBUG] here we go␤» 

你可以拥有多个相同名称但签名不同的角色;多重分派的选择多重候选者的正常规则适用。

Mixins§

角色可以混合到对象中。角色给定的属性和方法将被添加到对象已经拥有的方法和属性中。支持多个 mixins 和匿名角色。

role R { method Str() {'hidden!'} };
my $i = 2 but R;
sub f(\bound){ put bound };
f($i); # OUTPUT: «hidden!␤» 
my @positional := <a b> but R;
say @positional.^name# OUTPUT: «List+{R}␤» 

请注意,对象混合了角色,而不是对象的类或容器。因此,@-sigiled 容器将需要绑定才能使角色保持原样,如 @positional 示例所示。一些运算符将返回一个新值,这实际上会从结果中剥离 mixin。这就是为什么在使用 does 声明变量时混合角色可能更清晰的原因

role R {};
my @positional does R = <a b>;
say @positional.^name# OUTPUT: «Array+{R}␤» 

运算符 infix:<but> 比列表构造器更窄。当提供要混合的角色列表时,始终使用括号。

role R1 { method m {} }
role R2 { method n {} }
my $a = 1 but R1,R2# R2 is in sink context, issues a WARNING 
say $a.^name;
# OUTPUT: «Int+{R1}␤» 
my $all-roles = 1 but (R1,R2);
say $all-roles.^name# OUTPUT: «Int+{R1,R2}␤» 

如果角色提供了一个属性,则可以在括号中传递一个初始化器

role Named {
    has $.name;
}
my $hero = 1.Rat but Named('Remy');
say $hero.name;     # OUTPUT: «Remy␤» 

Mixins 可以用于对象生命周期的任何阶段。

# A counter for Table of Contents 
role TOC-Counter {
    has Int @!counters is default(0);
    method Str() { @!counters.join: '.' }
    method inc($level{
        @!counters[$level - 1]++;
        @!counters.splice($level);
        self
    }
}
 
my Num $toc-counter = NaN;     # don't do math with Not A Number 
say $toc-counter;              # OUTPUT: «NaN␤» 
$toc-counter does TOC-Counter# now we mix the role in 
$toc-counter.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);
put $toc-counter / 1;          # OUTPUT: «NaN␤» (because that's numerical context) 
put $toc-counter;              # OUTPUT: «2.2.2␤» (put will call TOC-Counter::Str) 

角色可以是匿名的。

my %seen of Int is default(0 but role :: { method Str() {'NULL'} });
say %seen<not-there>;          # OUTPUT: «NULL␤» 
say %seen<not-there>.defined;  # OUTPUT: «True␤» (0 may be False but is well defined) 
say Int.new(%seen<not-there>); # OUTPUT: «0␤» 

元对象编程和内省§

Raku 拥有一个元对象系统,这意味着对象、类、角色、语法、枚举等的行为本身由其他对象控制;这些对象称为元对象。元对象就像普通对象一样,是类的实例,在这种情况下,我们称它们为元类

对于每个对象或类,你可以通过调用其上的 .HOW 来获取元对象。请注意,虽然这看起来像方法调用,但它更像宏。

那么,你可以用元对象做什么呢?首先,你可以通过比较它们是否相等来检查两个对象是否具有相同的元类

say 1.HOW ===   2.HOW;      # OUTPUT: «True␤» 
say 1.HOW === Int.HOW;      # OUTPUT: «True␤» 
say 1.HOW === Num.HOW;      # OUTPUT: «False␤» 

Raku 使用HOW(更高阶工作方式)这个词来指代元对象系统。因此,在 Rakudo 中,控制类行为的元类的类名被称为 Perl6::Metamodel::ClassHOW 就不足为奇了。对于每个类,都有一个 Perl6::Metamodel::ClassHOW 实例。

但元模型为你做了更多的事情。例如,它允许你内省对象和类。元对象上方法的调用约定是在元对象上调用方法,并将感兴趣的对象作为第一个参数传递给对象。因此,要获取对象类的名称,你可以编写

my $object = 1;
my $metaobject = 1.HOW;
say $metaobject.name($object);      # OUTPUT: «Int␤» 
 
# or shorter: 
say 1.HOW.name(1);                  # OUTPUT: «Int␤» 

(动机是 Raku 还希望允许更基于原型的对象系统,在这种系统中,没有必要为每种类型创建新的元对象)。

有一个快捷方式可以避免两次使用同一个对象

say 1.^name;                        # OUTPUT: «Int␤» 
# same as 
say 1.HOW.name(1);                  # OUTPUT: «Int␤» 

有关 class 的元类的文档,请参见 Metamodel::ClassHOW,以及有关元对象协议的通用文档