学习一门编程语言时,可能因为熟悉另一门编程语言而有一些东西会让你感到意外,并且可能在调试和发现过程中浪费宝贵的时间。

本文档旨在展示常见的误解,以便避免它们。

在创建 Raku 的过程中,我们付出了巨大的努力来消除语法中的缺陷。但是,当你消除一个缺陷时,有时另一个缺陷会冒出来。因此,我们花费了大量时间来寻找最少的缺陷数量,或者试图将它们放在很少能看到的地方。正因为如此,Raku 的缺陷出现在与你从其他语言中预期的不同位置。

变量和常量§

常量在编译时计算§

常量在编译时计算,因此如果你在模块中使用它们,请记住它们的值将由于模块本身的预编译而被冻结。

# WRONG (most likely): 
unit module Something::Or::Other;
constant $config-file = "config.txt".IO.slurp;

$config-file 将在预编译期间被读取,并且对 config.txt 文件的更改不会在再次启动脚本时重新加载;只有在重新编译模块时才会重新加载。

避免 使用容器,而更倾向于 将值绑定 到变量,这提供类似于常量的行为,但允许值更新。

# Good; file gets updated from 'config.txt' file on each script run: 
unit module Something::Or::Other;
my $config-file := "config.txt".IO.slurp;

赋值 Nil 可能产生不同的值,通常是 Any§

实际上,将 Nil 赋值给变量会 将变量恢复为其默认值。例如,

my @a = 481516;
@a[2= Nil;
say @a# OUTPUT: «[4 8 (Any) 16]␤» 

在这种情况下,AnyArray 元素的默认值。

你可以有目的地将 Nil 作为默认值赋值

my %h is default(Nil= => Nil;
say %h# OUTPUT: «Hash %h = {:a(Nil)}␤» 

或者将值绑定到 Nil,如果这是你想要的结果

@a[3:= Nil;
say @a# OUTPUT: «[4 8 (Any) Nil]␤» 

这个陷阱可能隐藏在函数的结果中,例如匹配

my $result2 = 'abcdef' ~~ / dex /;
say "Result2 is { $result2.^name }"# OUTPUT: «Result2 is Any␤» 

如果 Match 找不到任何内容,它将是 Nil;但是,将 Nil 赋值给上面的 $result2 将导致其默认值,即 Any,如所示。

使用块来插值匿名状态变量§

程序员希望代码计算例程被调用的次数,但计数器没有增加。

sub count-it { say "Count is {$++}" }
count-it;
count-it;
 
# OUTPUT: 
# Count is 0 
# Count is 0 

当涉及状态变量时,声明变量的代码块在每次重新进入该代码块时都会被克隆,并且变量会重新初始化。这使得像下面这样的结构能够正常工作;循环内的状态变量在每次调用子程序时都会重新初始化。

sub count-it {
    for ^3 {
        state $count = 0;
        say "Count is $count";
        $count++;
    }
}
 
count-it;
say "…and again…";
count-it;
 
 
# OUTPUT: 
# Count is 0 
# Count is 1 
# Count is 2 
# …and again… 
# Count is 0 
# Count is 1 
# Count is 2 

我们的错误程序中也存在相同的布局。双引号字符串中的 { } 不仅仅是执行一段代码的插值。它实际上是一个独立的代码块,就像上面的例子一样,每次进入子程序时都会被克隆,重新初始化我们的状态变量。为了获得正确的计数,我们需要去掉这个内部代码块,使用标量上下文化器来插值我们的代码段。

sub count-it { say "Count is $($++)" }
count-it;
count-it;
 
# OUTPUT: 
# Count is 0 
# Count is 1 

或者,你也可以使用 连接运算符

sub count-it { say "Count is " ~ $++ }

在值为空时使用设置子程序在 Associative§

在实现 Associative 的类上使用 (cont)(elem),如果键的值为空,则将返回 False

enum Foo «a b»;
say Foo.enums  'a';
 
# OUTPUT: 
# False 

相反,请使用 :exists

enum Foo «a b»;
say Foo.enums<a>:exists;
 
# OUTPUT: 
# True 

代码块§

注意空“代码块”§

花括号用于声明代码块。但是,空花括号将声明一个哈希。

$ = {say 42;} # Block 
$ = {;}       # Block 
$ = {}       # Block 
$ = { }       # Hash 

如果你实际上想要声明一个空代码块,可以使用第二种形式。

my &does-nothing = {;};
say does-nothing(33); # OUTPUT: «Nil␤»

对象§

给属性赋值§

新手经常认为,由于带有访问器的属性被声明为 has $.x,因此他们可以在类中给 $.x 赋值。事实并非如此。

例如

class Point {
    has $.x;
    has $.y;
    method double {
        $.x *= 2;   # WRONG 
        $.y *= 2;   # WRONG 
        self;
    }
}
 
say Point.new(x => 1=> -2).double.x
# OUTPUT: «Cannot assign to an immutable value␤» 

double 方法中的第一行被标记为 # WRONG,因为 $.x$( self.x ) 的简写)是对只读访问器的调用。

语法 has $.xhas $!x; method x() { $!x } 的简写,因此实际的属性称为 $!x,并且会自动生成一个只读访问器方法。

因此,编写 double 方法的正确方法是

method double {
    $!x *= 2;
    $!y *= 2;
    self;
}

它直接对属性进行操作。

BUILD 阻止从构造函数参数自动初始化属性§

当你定义自己的 BUILD 子方法时,你必须自己负责初始化所有属性。例如

class A {
    has $.x;
    has $.y;
    submethod BUILD {
        $!y = 18;
    }
}
 
say A.new(x => 42).x;       # OUTPUT: «Any␤» 

会使 $!x 未初始化,因为自定义的 BUILD 不会初始化它。

注意: 考虑使用 TWEAK。自 2016.11 版本起,Rakudo 支持 TWEAK 方法。

一种可能的解决方法是在 BUILD 中显式初始化属性

submethod BUILD(:$x{
    $!y = 18;
    $!x := $x;
}

可以简化为

submethod BUILD(:$!x{
    $!y = 18;
}

空白§

正则表达式中的空白不会按字面匹配§

say 'a b' ~~ /a b/# OUTPUT: «False␤» 

正则表达式中的空白默认情况下被视为可选的填充物,没有语义,就像 Raku 语言中的其他部分一样。

匹配空白的方法

  • \s 匹配任何一个空白,\s+ 匹配至少一个空白

  • ' '(带引号的空格)匹配单个空格

  • \t\n 用于特定空白(制表符、换行符)

  • \h\v 用于水平、垂直空白

  • .ws,一个用于空白的内置规则,通常可以实现你想要的功能

  • 使用 m:s/a b/m:sigspace/a b/,正则表达式中的空格会匹配任意空白

解析中的歧义§

虽然一些语言允许您尽可能地删除标记之间的空格,但 Raku 对此不太宽容。总的来说,我们不鼓励代码高尔夫,因此不要吝啬空格(这些限制背后的更深层原因是单遍解析和能够在几乎没有 回溯 的情况下解析 Raku 程序)。

您应该注意的常见区域是

块与哈希切片歧义§

# WRONG; trying to hash-slice a Bool: 
while ($++ > 5){ .say }
# RIGHT: 
while ($++ > 5{ .say }
 
# EVEN BETTER; Raku does not require parentheses there: 
while $++ > 5 { .say }

归约与数组构造函数歧义§

# WRONG; ambiguity with `[<]` metaop: 
my @a = [[<foo>],];
# RIGHT; reductions cannot have spaces in them, so put one in: 
my @a = [[ <foo>],];
 
# No ambiguity here, natural spaces between items suffice to resolve it: 
my @a = [[<foo bar ber>],];

小于与单词引用/关联索引§

# WRONG; trying to index 3 associatively: 
say 3<5>4
# RIGHT; prefer some extra whitespace around infix operators: 
say 3 < 5 > 4

排他序列与包含范围的序列§

有关 ...^ 运算符如何可能被误认为是紧随其后的 ^ 运算符的 ... 运算符的更多信息,请参阅有关 运算符陷阱 的部分。您必须正确使用空格来指示将遵循哪种解释。

捕获§

捕获中的容器与值§

初学者可能会期望 Capture 中的变量在 Capture 稍后使用时提供其当前值。例如

my $a = 2say join ",", ($a++$a);  # OUTPUT: «3,3␤» 

这里 Capture 包含 $a 指向的容器和表达式 ++$a 结果的。由于 Capture 必须在 &say 使用它之前被具体化,因此 ++$a 可能会在 &say 查看 $a 中的容器内部之前发生(以及在使用两个项创建 List 之前),因此它可能已经被递增。

相反,当您想要一个值时,请使用一个产生值的表达式。

my $a = 2say join ",", (+$a++$a); # OUTPUT: «2,3␤» 

或者更简单

my $a = 2say  "$a{++$a}"# OUTPUT: «2, 3␤» 

在本例中也会发生相同的情况

my @arr;
my ($a$b= (1,1);
for ^5 {
    ($a,$b= ($b$a+$b);
    @arr.push: ($a$b);
    say @arr
};

输出 «[(1 2)]␤[(2 3) (2 3)]␤[(3 5) (3 5) (3 5)]␤...$a$b 直到调用 say 才会被具体化,它们在那一刻具有的值就是打印的值。为了避免这种情况,请在使用值之前将值从容器中取出或以某种方式从变量中取出。

my @arr;
my ($a$b= (1,1);
for ^5 {
    ($a,$b= ($b$a+$b);
    @arr.push: ($a.item$b.item);
    say @arr
};

使用 item,容器将在项目上下文中进行评估,其值将被提取,并实现所需的结果。

Cool 技巧§

Raku 包含一个 Cool 类,它提供了一些我们通过在必要时强制转换参数而习惯的 DWIM 行为。但是,DWIM 从来都不是完美的。特别是对于 List,它们是 Cool,有许多方法不会像您可能认为的那样工作,包括 containsstarts-withindex。请参阅下面部分中的一些示例。

字符串不是 List,因此请注意索引§

在 Raku 中,字符串 (Str) 不是字符列表。尽管 .index 方法 的名称,但 不能像使用 List 一样对它们进行迭代或索引。

List 变成字符串,因此请注意 .index()ing§

List 继承自 Cool,它提供了对 .index 的访问。由于 .index 强制转换 ListStr 的方式,这有时看起来像是返回列表中元素的索引,但这不是行为的定义方式。

my @a = <a b c d>;
say @a.index(a);    # OUTPUT: «0␤» 
say @a.index('c');    # OUTPUT: «4␤» -- not 2! 
say @a.index('b c');  # OUTPUT: «2␤» -- not undefined! 
say @a.index(<a b>);  # OUTPUT: «0␤» -- not undefined! 

这些相同的注意事项也适用于 .rindex

List 会变成字符串,所以要小心 .contains()§

类似地,.contains 不会在列表中查找元素。

my @menu = <hamburger fries milkshake>;
say @menu.contains('hamburger');            # OUTPUT: «True␤» 
say @menu.contains('hot dog');              # OUTPUT: «False␤» 
say @menu.contains('milk');                 # OUTPUT: «True␤»! 
say @menu.contains('er fr');                # OUTPUT: «True␤»! 
say @menu.contains(<es mi>);                # OUTPUT: «True␤»! 

如果你真的想检查元素是否存在,请使用 (cont) 运算符来检查单个元素,并使用 超集真超集 运算符来检查多个元素。

my @menu = <hamburger fries milkshake>;
say @menu (cont) 'fries';                   # OUTPUT: «True␤» 
say @menu (cont) 'milk';                    # OUTPUT: «False␤» 
say @menu (>) <hamburger fries>;            # OUTPUT: «True␤» 
say @menu (>) <milkshake fries>;            # OUTPUT: «True␤» (! NB: order doesn't matter) 

如果你要进行大量的元素测试,你最好使用 Set

Numeric 字面量在强制转换之前会被解析§

经验丰富的程序员可能不会对这一点感到惊讶,但 Numeric 字面量会在被强制转换为字符串之前被解析为它们的数值,这可能会产生非直观的結果。

say 0xff.contains(55);      # OUTPUT: «True␤» 
say 0xff.contains(0xf);     # OUTPUT: «False␤» 
say 12_345.contains("23");  # OUTPUT: «True␤» 
say 12_345.contains("2_");  # OUTPUT: «False␤» 

List 中获取随机项§

一个常见的任务是从集合中检索一个或多个随机元素,但 List.rand 不是这样做的方法。 Cool 提供了 rand,但它首先将 List 强制转换为列表中项目的数量,并返回一个介于 0 和该值之间的随机实数。要获取随机元素,请参见 pickroll

my @colors = <red orange yellow green blue indigo violet>;
say @colors.rand;       # OUTPUT: «2.21921955680514␤» 
say @colors.pick;       # OUTPUT: «orange␤» 
say @colors.roll;       # OUTPUT: «blue␤» 
say @colors.pick(2);    # OUTPUT: «(yellow violet)␤»  (cannot repeat) 
say @colors.roll(3);    # OUTPUT: «(red green red)␤»  (can repeat) 

List 在数字上下文中会转换为其元素的数量§

你想检查一个数字是否可以被一组数字中的任何一个整除

say 42 %% <11 33 88 55 111 20325># OUTPUT: «True␤»

什么?没有一个单一的数字可以被 42 整除。但是,该列表有 6 个元素,而 42 可以被 6 整除。这就是输出为真的原因。在这种情况下,你应该将 List 转换为 Junction

say 42 %% <11 33 88 55 111 20325>.any;
# OUTPUT: «any(False, False, False, False, False, False)␤» 

这将清楚地揭示列表中所有数字的可除性是错误的,这些数字将被分别转换为数字。

数组§

引用数组的最后一个元素§

在某些语言中,可以通过请求数组的“第 -1 个”元素来引用数组的最后一个元素,例如

my @array = qw{victor alice bob charlie eve};
say @array[-1];    # OUTPUT: «eve␤»

在 Raku 中,无法使用负索引,但是可以通过实际使用函数来实现相同的效果,即 *-1。因此,访问数组的最后一个元素变为

my @array = qw{victor alice bob charlie eve};
say @array[*-1];   # OUTPUT: «eve␤» 

另一种方法是利用数组的尾部方法

my @array = qw{victor alice bob charlie eve};
say @array.tail;      # OUTPUT: «eve␤» 
say @array.tail(2);   # OUTPUT: «(charlie eve)␤» 

类型化数组参数§

通常,新用户会在深入了解文档之前写出类似以下内容

sub foo(Array @a{ ... }

...他们意识到这实际上是要求一个数组的数组。要说明 @a 应该只接受数组,请改用

sub foo(@a where Array{ ... }

人们也经常期望以下内容能起作用,但实际上它不起作用

sub bar(Int @a{ 42.say };
bar([123]);             # expected Positional[Int] but got Array 

这里的问题是 [1, 2, 3] 不是 Array[Int],它是一个普通的数组,只是碰巧包含 Int。要使其起作用,参数也必须是 Array[Int]

my Int @b = 123;
bar(@b);                    # OUTPUT: «42␤» 
bar(Array[Int].new(123));

这可能看起来很不方便,但从好的方面来说,它将对 @b 赋值的类型检查转移到赋值发生的地方,而不是要求在每次调用时检查每个元素。

在不需要时使用 «» 引用§

这个陷阱可以在不同的变体中看到。以下是一些变体

my $x = hello;
my $y = foo bar;
 
my %h = $x => 42$y => 99;
say %h«$x»;   # ← WRONG; assumption that $x has no whitespace 
say %h«$y»;   # ← WRONG; splits ‘foo bar’ by whitespace 
say %h«"$y"»; # ← KINDA OK; it works but there is no good reason to do that 
say %h{$y};   # ← RIGHT; this is what should be used 
 
run «touch $x»;        # ← WRONG; assumption that only one file will be created 
run «touch $y»;        # ← WRONG; will touch file ‘foo’ and ‘bar’ 
run «touch "$y"»;      # ← WRONG; better, but has a different issue if $y starts with - 
run «touch -- "$y"»;   # ← KINDA OK; it works but there is no good enough reason to do that 
run touch--$y# ← RIGHT; explicit and *always* correct 
run <touch -->$y;    # ← RIGHT; < > are OK, this is short and correct 

基本上,«» 引用只有在你记得始终引用你的变量时才是安全的。问题是它将默认行为反转为不安全的变体,因此仅仅忘记了一些引用,你就有可能引入错误,甚至安全漏洞。为了安全起见,请不要使用 «»

字符串§

在处理 Str 时可能会出现一些问题。

引号和插值§

字符串字面量中的插值可能过于聪明,反倒适得其反。

# "HTML tags" interpreted as associative indexing: 
"$foo<html></html>" eq
"$foo{'html'}{'/html'}"
# Parentheses interpreted as call with argument: 
"$foo(" ~ @args ~ ")" eq
"$foo(' ~ @args ~ ')"

您可以使用非插值单引号并使用 \qq[] 转义序列切换到更自由的插值来避免这些问题。

my $a = 1;
say '\qq[$a]()$b()';
# OUTPUT: «1()$b()␤» 

另一种选择是使用 Q:c 引号,并使用代码块 {} 进行所有插值。

my $a = 1;
say Q:c«{$a}()$b()»;
# OUTPUT: «1()$b()␤» 

注意 qqx 中使用的变量§

qqx[] 中的变量可能会引入安全漏洞;变量内容可以设置为精心制作的字符串并执行任意代码。

my $world = "there\";rm -rf /path/to/dir\"";
say qqx{echo "hello $world"};
# OUTPUT: «hello there␤» 

上面的代码还会删除 /path/to/dir,您可以通过确保变量内容不包含 shell 特殊字符来避免此问题,或者使用 runProc::Async 来更好地执行外部命令。

字符串不可迭代§

StrAny 继承的方法,这些方法适用于列表等可迭代对象。字符串上的迭代器包含一个元素,即整个字符串。要使用基于列表的方法(如 sortreverse),您需要先将字符串转换为列表。

say "cba".sort;              # OUTPUT: «(cba)␤» 
say "cba".comb.sort.join;    # OUTPUT: «abc␤» 

.chars 获取的是音节数,而不是码点§

在 Raku 中,.chars 返回音节数,即用户可见的字符数。例如,这些音节可以由一个字母加上一个重音组成。如果您需要码点数,则应使用 .codes。如果您需要以 UTF8 编码时的字节数,则应使用 .encode.bytes 将字符串编码为 UTF8,然后获取字节数。

say "\c[LATIN SMALL LETTER J WITH CARON, COMBINING DOT BELOW]"# OUTPUT: «ǰ̣␤» 
say 'ǰ̣'.codes;        # OUTPUT: «2␤» 
say 'ǰ̣'.chars;        # OUTPUT: «1␤» 
say 'ǰ̣'.encode.bytes# OUTPUT: «4␤»

有关 Raku 中字符串工作原理的更多信息,请参阅 Unicode 页面

默认情况下所有文本都已规范化§

Raku 将所有文本规范化为 Unicode NFC 形式(规范化形式规范)。文件名是默认情况下未规范化的唯一文本。如果您希望您的字符串保持与原始字符串相同的字节表示,则需要在读写任何文件句柄时使用 UTF8-C8

同形异义词通常遵循数字语义§

Str "0"True,而 NumericFalse。那么 Bool 的同形异义词 <0> 的值是什么?

一般来说,同形异义词遵循 Numeric 语义,因此从数值上评估为零的同形异义词为 False

say so   <0># OUTPUT: «False␤» 
say so <0e0># OUTPUT: «False␤» 
say so <0.0># OUTPUT: «False␤»

要强制对同形异义词的 Stringy 部分进行比较,请使用 前缀 ~ 运算符Str 方法将同形异义词强制转换为 Str,或者使用 chars 例程来测试同形异义词是否具有任何长度。

say so      ~<0>;     # OUTPUT: «True␤» 
say so       <0>.Str# OUTPUT: «True␤» 
say so chars <0>;     # OUTPUT: «True␤»

字符串的区分大小写比较§

为了进行区分大小写比较,您可以使用 .fc(折叠大小写)。问题是人们倾向于使用 .lc.uc,并且它似乎在 ASCII 范围内有效,但在其他字符上失败。这不仅仅是 Raku 的陷阱,其他语言也一样。

say groß.lc eq GROSS.lc# ← WRONG; False 
say groß.uc eq GROSS.uc# ← WRONG; True, but that's just luck 
say groß.fc eq GROSS.fc# ← RIGHT; True 

如果您使用的是正则表达式,则无需使用 .fc,而是可以使用 :i:ignorecase)副词。

§

对符号表示法左侧的常量§

考虑以下代码

enum Animals <Dog Cat>;
my %h := :{ Dog => 42 };
say %h{Dog}# OUTPUT: «(Any)␤» 

:{ … } 语法用于创建 对象哈希。编写该代码的人的意图是使用枚举对象作为键创建哈希(并且 say %h{Dog} 尝试使用枚举对象作为键进行查找)。但是,这并不是配对表示法的运作方式。

例如,在 Dog => 42 中,键将是 Str。也就是说,无论是否存在具有相同名称的常量或枚举,配对表示法都将始终使用左侧作为字符串字面量,只要它看起来像标识符。

为了避免这种情况,请使用 (Dog) => 42::Dog => 42

Pair 中的标量值§

在处理 Scalar 值时,Pair 会保存指向该值的容器。这意味着可以从 Pair 外部反映对 Scalar 值的更改。

my $v = 'value A';
my $pair = Pair.new'a'$v );
$pair.say;  # OUTPUT: a => value A 
 
$v = 'value B';
$pair.say# OUTPUT: a => value B 

使用 freeze 方法强制从 Pair 中删除 Scalar 容器。有关更多详细信息,请参阅有关 Pair 的文档。

集合、袋子和混合§

集合、袋子和混合没有固定的顺序§

在迭代此类对象时,顺序未定义。

my $set = <a b c>.Set;
.say for $set.list# OUTPUT: «a => True␤c => True␤b => True␤» 
# OUTPUT: «a => True␤c => True␤b => True␤» 
# OUTPUT: «c => True␤b => True␤a => True␤» 

每次迭代都可能(并且会)产生不同的顺序,因此您不能信任集合元素的特定序列。如果顺序无关紧要,只需按这种方式使用它们。如果确实如此,请使用 sort

my $set = <a b c>.Set;
.say for $set.list.sort;  # OUTPUT: «a => True␤b => True␤c => True␤»

通常,集合、袋子和混合是无序的,因此您不应该依赖它们具有特定的顺序。

运算符§

一些在其他语言中常用的运算符在 Raku 中被重新用于其他更常见的事物。

连接§

^|& 不是 位运算符,它们创建 连接。Raku 中相应的位运算符是:对于整数为 +^+|+&,对于 布尔值?^?|?&

排他序列运算符§

大量使用空格有助于可读性,但请记住,中缀运算符不能包含任何空格。其中一个运算符是排除右端点的序列运算符:...^(或其 Unicode 等效项 …^)。

say 1... ^5# OUTPUT: «(1 0 1 2 3 4)␤» 
say 1...^5;  # OUTPUT: «(1 2 3 4)␤»

如果您在省略号 () 和插入符号 (^) 之间放置空格,它将不再是一个中缀运算符,而是一个中缀包含序列运算符 () 和一个前缀 Range 运算符 (^)。可迭代对象 是序列运算符的有效端点,因此您得到的结果可能与您预期的不符。

字符串范围/序列§

在某些语言中,使用字符串作为范围端点时,会考虑整个字符串来确定下一个字符串应该是什么;将字符串松散地视为大基数中的数字。以下是 Perl 版本

say join """az".."bc";
# OUTPUT: «az, ba, bb, bc␤» 

Raku 中的这种范围将产生不同的结果,其中每个字母都将与端点中的对应字母进行范围匹配,从而产生更复杂的序列

say join """az".."bc";
#`{ OUTPUT: «
    az, ay, ax, aw, av, au, at, as, ar, aq, ap, ao, an, am, al, ak, aj, ai, ah,
    ag, af, ae, ad, ac, bz, by, bx, bw, bv, bu, bt, bs, br, bq, bp, bo, bn, bm,
    bl, bk, bj, bi, bh, bg, bf, be, bd, bc
␤»}
say join """r2".."t3";
# OUTPUT: «r2, r3, s2, s3, t2, t3␤» 

要实现更简单的行为,类似于上面的 Perl 示例,请使用一个序列运算符,该运算符对起始字符串调用 .succ 方法

say join "", ("az"*.succ ... "bc");
# OUTPUT: «az, ba, bb, bc␤» 

主题化运算符§

智能匹配运算符 ~~andthen 将主题 $_ 设置为其左侧。结合对主题的隐式方法调用,这会导致令人惊讶的结果。

my &method = { note $_$_ };
$_ = 'object';
say .&method;
# OUTPUT: «object␤object␤» 
say 'topic' ~~ .&method;
# OUTPUT: «topic␤True␤» 

在许多情况下,将方法调用翻转到 LHS 将起作用。

my &method = { note $_$_ };
$_ = 'object';
say .&method;
# OUTPUT: «object␤object␤» 
say .&method ~~ 'topic';
# OUTPUT: «object␤False␤» 

胖箭头和常量§

箭头运算符 => 会将左侧的词语转换为 Str,而不会检查作用域以查找常量或 \ 符号变量。使用显式作用域来获得你想要的结果。

constant V = 'x';
my %h = => 'oi‽', ::=> 42;
say %h.raku
# OUTPUT: «{:V("oi‽"), :x(42)}␤» 

中缀运算符赋值§

中缀运算符,无论是内置的还是用户定义的,都可以与赋值运算符结合使用,例如这个加法示例所示

my $x = 10;
$x += 20;
say $x;     # OUTPUT: «30␤»

对于任何给定的中缀运算符 opL op= R 等效于 L = L op R(其中 LR 分别是左右参数)。这意味着以下代码可能不会按预期执行

my @a = 123;
@a += 10;
say @a;  # OUTPUT: «[13]␤»

从像 C++ 这样的语言而来,这可能看起来很奇怪。重要的是要记住 += 不是在左侧参数(这里是指 @a 数组)上定义的方法,而只是 L = L op R 的简写

my @a = 123;
@a = @a + 10;
say @a;  # OUTPUT: «[13]␤»

这里 @a 被赋值为 @a(包含三个元素)和 10 相加的结果;因此 13 被放置在 @a 中。

使用赋值运算符的 超形式 代替

my @a = 123;
@a »+=» 10;
say @a;  # OUTPUT: «[11 12 13]␤»

方法调用不链式§

上述 L = L op R 的澄清有一个例外,即当中缀运算符赋值与方法调用运算符结合使用时,例如 L .= R。在这种情况下,只有链中的第一个方法在赋值中应用。

my $s = "abcd";
say $s .= uc.lc# OUTPUT: abcd 
say $s;          # OUTPUT: ABCD

通过将链分解为单独的中缀运算符赋值,你可以实现想要的效果

my $s = "abcd";
say $s .= uc .= lc# OUTPUT: abcd 
say $s;             # OUTPUT: abcd

正则表达式§

$x vs <$x>,以及 $(code) vs <{code}>§

Raku 提供了多种结构,通过插值在运行时生成正则表达式(参见 这里 的详细描述)。当以这种方式生成的正则表达式仅包含字面量时,上述结构(成对)的行为相同,就好像它们是等效的替代方案一样。但是,一旦生成的正则表达式包含元字符,它们的行为就会不同,这可能会令人困惑。

前两个容易混淆的结构是 $variable<$variable>

my $variable = 'camelia';
say I ♥ camelia ~~ /  $variable  /;   # OUTPUT: 「camelia」 
say I ♥ camelia ~~ / <$variable> /;   # OUTPUT: 「camelia」

这里它们的行为相同,因为 $variable 的值由字面量组成。但是,当变量被更改为包含正则表达式元字符时,输出就会不同

my $variable = '#camelia';
say I ♥ #camelia ~~ /  $variable  /;   # OUTPUT: «「#camelia」␤» 
say I ♥ #camelia ~~ / <$variable> /;   # !! Error: malformed regex

这里发生的事情是,字符串 #camelia 包含元字符 #。在正则表达式的上下文中,此字符应该被引用以匹配字面量;如果没有引用,# 将被解析为注释的开始,该注释一直运行到行尾,这反过来会导致正则表达式没有终止,从而导致正则表达式格式错误。

另外两个需要区分的结构是 $(code)<{code}>。与之前一样,只要 code 的(字符串化)返回值仅包含字面量,这两个结构之间就没有区别

my $variable = 'ailemac';
say I ♥ camelia ~~ / $($variable.flip)   /;   # OUTPUT: «「camelia」␤» 
say I ♥ camelia ~~ / <{$variable.flip}>  /;   # OUTPUT: «「camelia」␤»

但是,当返回值被更改为包含正则表达式元字符时,输出就会不同

my $variable = 'ailema.';
say I ♥ camelia ~~ / $($variable.flip)   /;   # OUTPUT: Nil 
say I ♥ camelia ~~ / <{$variable.flip}>  /;   # OUTPUT: «「camelia」␤»

在这种情况下,代码的返回值是字符串 .amelia,其中包含元字符 .$(code) 尝试以字面量方式匹配点失败;<{code}> 尝试以正则表达式通配符方式匹配点成功。因此输出不同。

| vs ||:哪个分支会获胜§

为了匹配多个可能的备选方案,将使用 |||。但它们是如此不同。

当存在多个匹配的备选方案时,对于那些由 || 分隔的备选方案,第一个匹配的备选方案获胜;对于那些由 | 分隔的备选方案,获胜者由 LTM 策略决定。另请参见:关于 || 的文档关于 | 的文档

对于简单的正则表达式,只需使用||代替|即可获得熟悉的语义,但如果编写语法,则学习 LTM 和声明式前缀并优先使用|很有用。并且避免在一个正则表达式中使用它们。当您必须这样做时,添加括号并确保您了解 LTM 策略的工作原理,以使代码按您的意愿执行。

陷阱通常出现在您尝试在同一个正则表达式中混合使用|||时。

say 42 ~~ / [  0 || 42 ] | 4/# OUTPUT: «「4」␤» 
say 42 ~~ / [ 42 ||  0 ] | 4/# OUTPUT: «「42」␤» 

上面的代码可能看起来像是产生了错误的结果,但实际上实现是正确的。

$/ 每次匹配正则表达式时都会改变§

每次对某个内容匹配正则表达式时,保存结果的特殊变量$/匹配对象会根据匹配结果进行相应更改(也可能是Nil)。

$/ 的更改与正则表达式匹配所在的范围无关。

有关更多信息和示例,请参阅正则表达式文档中的相关部分

<foo>< foo>:命名规则与引用的列表§

正则表达式可以包含引用的列表;对列表的元素执行最长令牌匹配,就像指定了|交替一样(有关更多信息,请参阅此处)。

在正则表达式中,以下是有一个项目的列表,'foo'

say 'foo' ~~ /< foo >/;  # OUTPUT: «「foo」␤» 
say 'foo' ~~ /< foo>/;   # OUTPUT: «「foo」␤»

但这是一个对命名规则foo的调用。

say 'foo' ~~ /<foo>/;
# OUTPUT: «No such method 'foo' for invocant of type 'Match'␤ in block <unit> at <unknown file> line 1␤» 

注意区别;如果您打算使用引用的列表,请确保初始<之后有空格。

列表上下文中非捕获、非全局匹配§

与 Perl 不同,列表上下文中非捕获和非全局匹配不会产生任何值。

if  'x' ~~ /./ { say 'yes' }  # OUTPUT: «yes␤» 
for 'x' ~~ /./ { say 'yes' }  # NO OUTPUT

这是因为它的“列表”槽(继承自 Capture 类)没有用原始匹配对象填充。

say ('x' ~~ /./).list  # OUTPUT: «()␤»

要实现预期结果,请使用全局匹配、捕获括号或带尾部逗号的列表。

for 'x' ~~ m:g/./ { say 'yes' }  # OUTPUT: «yes␤» 
for 'x' ~~ /(.)/  { say 'yes' }  # OUTPUT: «yes␤» 
for ('x' ~~ /./,) { say 'yes' }  # OUTPUT: «yes␤»

常见的优先级错误§

副词和优先级§

副词确实具有优先级,这可能不遵循您屏幕上显示的操作符顺序。如果两个优先级相同的操作符后面跟着一个副词,它将选择在抽象语法树中找到的第一个操作符。使用括号来帮助 Raku 理解您的意思,或者使用优先级较低的运算符。

my %x = => 42;
say !%x<b>:exists;            # dies with X::AdHoc 
say %x<b>:!exists;            # this works 
say !(%x<b>:exists);          # works too 
say not %x<b>:exists;         # works as well 
say True unless %x<b>:exists# avoid negation altogether 

范围和优先级§

.. 的松散优先级会导致一些错误。通常最好在您想对整个范围进行操作时对范围使用括号。

1..3.say;    # OUTPUT: «3␤» (and warns about useless use of "..") 
(1..3).say;  # OUTPUT: «1..3␤» 

松散的布尔运算符§

andor 等的优先级比例程调用更低。这对于对在其他语言(如returnlast 和许多其他语言)中是运算符或语句的例程的调用可能会产生令人惊讶的结果。

sub f {
    return True and False;
    # this is actually 
    # (return True) and False; 
}
say f# OUTPUT: «True␤» 

指数运算符和前缀减号§

say -1²;   # OUTPUT: «-1␤» 
say -1**2# OUTPUT: «-1␤» 

在执行常规数学计算时,幂优先于减号;因此-1² 可以写成-(1²)。Raku 匹配这些数学规则,** 运算符的优先级高于前缀-。如果您想将负数提高到幂,请使用括号。

say (-1)²;   # OUTPUT: «1␤» 
say (-1)**2# OUTPUT: «1␤» 

方法运算符调用和前缀减号§

前缀减号的绑定比点式方法运算符调用更松散。前缀减号将应用于方法的返回值。为了确保减号作为参数的一部分传递,请将其括在括号中。

say  -1.abs;  # OUTPUT: «-1␤» 
say (-1).abs# OUTPUT: «1␤» 

子例程和方法调用§

子例程和方法调用可以使用两种形式之一进行。

foo(...); # function call form, where ... represent the required arguments 
foo ...;  # list op form, where ... represent the required arguments 

当在函数或方法名后和左括号前添加空格时,函数调用形式可能会给粗心的人带来问题。

首先,我们考虑参数为零或一个的函数。

sub foo() { say 'no arg' }
sub bar($a{ say "one arg: $a" }

然后分别执行带空格和不带空格的情况。

foo();    # okay: no arg 
foo ();   # FAIL: Too many positionals passed; expected 0 arguments but got 1 
bar($a);  # okay: one arg: 1 
bar ($a); # okay: one arg: 1 

现在声明一个有两个参数的函数。

sub foo($a$b{ say "two args: $a$b" }

分别执行带空格和不带空格的情况。

foo($a$b);  # okay: two args: 1, 2 
foo ($a$b); # FAIL: Too few positionals passed; expected 2 arguments but got 1 

教训是:“在使用函数调用格式时,要小心函数和方法名后的空格。” 作为一般规则,良好的实践可能是使用函数调用格式时避免函数名后的空格。

请注意,有一些巧妙的方法可以消除函数调用格式和空格带来的错误,但这有点像黑客行为,这里不予提及。有关更多信息,请参阅 函数

最后,请注意,目前,在声明函数时,可以在函数或方法名和包围参数列表的括号之间使用空格,不会出现问题。

命名参数§

许多内置子例程和方法调用接受命名参数,您自己的代码也可能接受命名参数,但请确保在调用例程时传递的参数实际上是命名参数。

sub foo($a:$b{ ... }
foo(1'b' => 2); # FAIL: Too many positionals passed; expected 1 argument but got 2 

发生了什么?第二个参数不是命名参数,而是作为位置参数传递的 Pair。如果您想要命名参数,它必须看起来像 Perl 中的名称。

foo(1=> 2); # okay 
foo(1:b(2));  # okay 
foo(1:b<it>); # okay 
 
my $b = 2;
foo(1:b($b)); # okay, but redundant 
foo(1:$b);    # okay 
 
# Or even... 
my %arg = 'b' => 2;
foo(1|%arg);  # okay too 

最后一个可能令人困惑,但因为它在 Hash 上使用了 | 前缀,这是一个特殊的编译器结构,表示您想要使用变量的内容作为参数,对于哈希来说,这意味着将它们视为命名参数。

如果您真的想将它们作为对传递,您应该使用 ListCapture 代替。

my $list = ('b' => 2),; # this is a List containing a single Pair 
foo(|$list:$b);       # okay: we passed the pair 'b' => 2 to the first argument 
foo(1|$list);         # FAIL: Too many positionals passed; expected 1 argument but got 2 
foo(1|$list.Capture); # OK: .Capture call converts all Pair objects to named args in a Capture 
my $cap = \('b' => 2); # a Capture with a single positional value 
foo(|$cap:$b); # okay: we passed the pair 'b' => 2 to the first argument 
foo(1|$cap);   # FAIL: Too many positionals passed; expected 1 argument but got 2 

Capture 通常是最好的选择,因为它与在常规调用期间捕获例程参数的方式完全相同。

这里区别的好处是,它使开发人员可以选择将对作为命名参数或位置参数传递,这在各种情况下都非常方便。

参数数量限制§

虽然通常不会注意到,但存在一个依赖于后端的参数数量限制。任何将任意大小的数组展平成参数的代码,如果元素过多,将无法正常工作。

my @a = 1 xx 9999;
my @b;
@b.push: |@a;
say @b.elems # OUTPUT: «9999␤» 
my @a = 1 xx 999999;
my @b;
@b.push: |@a# OUTPUT: «Too many arguments in flattening array.␤  in block <unit> at <tmp> line 1␤␤» 

通过重写代码以避免展平来避免此陷阱。在上面的示例中,您可以用 append 替换 push。这样,就不需要展平,因为可以按原样传递数组。

my @a = 1 xx 999999;
my @b;
@b.append: @a;
say @b.elems # OUTPUT: «999999␤» 

Phaser 和隐式返回值§

sub returns-ret () {
    CATCH {
        default {}
    }
    "ret";
}
 
sub doesn't-return-ret () {
    "ret";
    CATCH {
        default {}
    }
}
 
say returns-ret;        # OUTPUT: «ret␤» 
say doesn't-return-ret;
# BAD: outputs «Nil» and a warning «Useless use of constant string "ret" in sink context (line 13)» 

returns-retdoesn't-return-ret 的代码可能看起来完全相同,因为原则上,CATCH 块的位置并不重要。但是,块是一个对象,sub 中的最后一个对象将被返回,因此 doesn't-return-ret 将返回 Nil,此外,由于 "ret" 现在处于 sink 上下文,它将发出警告。如果您想出于传统原因将 phaser 放在最后,请使用 return 的显式形式。

sub explicitly-return-ret () {
    return "ret";
    CATCH {
        default {}
    }
}

LEAVE 需要从子例程中显式返回才能运行§

正如 LEAVE phaser 的文档所述LEAVE 在块退出时运行,“... 除了程序意外退出时”。也就是说,与 END 不同,它只有在块正常返回时才会被调用。这就是为什么

sub a() { LEAVE say "left"exit 1 }# No output, it will simply exit 

不会运行 LEAVE 代码,因为从技术上讲它没有返回。另一方面,END 是一个程序执行 phaser,无论如何都会运行。

sub a() {
    END say "Begone"exit 1
}a# OUTPUT: «Begone␤» 

输入和输出§

关闭打开的文件句柄和管道§

与其他一些语言不同,Raku 不使用引用计数,因此 **文件句柄在超出作用域时不会关闭**。您必须使用 close 例程或使用 :close 参数显式关闭它们,IO::Handle's 的一些方法接受此参数。有关详细信息,请参阅 IO::Handle.close

相同的规则适用于 IO::Handle's 的子类 IO::Pipe,这是您在从 Proc 读取数据时操作的对象,您使用例程 runshell 获取该对象。

该警告也适用于 IO::CatHandle 类型,尽管不那么严重。有关详细信息,请参阅 IO::CatHandle.close

IO::Path 字符串化§

出于历史原因和设计原因,IO::Path 对象 字符串化 时不会考虑其 CWD 属性,这意味着如果您 chdir 然后字符串化一个 IO::Path,或者字符串化一个具有自定义 $!CWD 属性的 IO::Path,则结果字符串将不会引用原始文件系统对象。

with 'foo'.IO {
    .Str.say;       # OUTPUT: «foo␤» 
    .relative.say;  # OUTPUT: «foo␤» 
 
    chdir "/tmp";
    .Str.say;       # OUTPUT: «foo␤» 
    .relative.say   # OUTPUT: «../home/camelia/foo␤» 
}
 
# Deletes ./foo, not /bar/foo 
unlink IO::Path.new("foo":CWD</bar>).Str

避免此问题的简单方法是根本不字符串化 IO::Path 对象。与路径一起工作的核心例程可以接受 IO::Path 对象,因此您不需要字符串化路径。

如果您确实需要 IO::Path 的字符串化版本,请使用 absoluterelative 方法将其分别字符串化为绝对路径或相对路径。

如果您遇到此问题是因为您在代码中使用了 chdir,请考虑以不涉及更改当前目录的方式重写它。例如,您可以将 cwd 命名参数传递给 run,而无需在它周围使用 chdir

将输入数据拆分为行§

IO::HandleStr 上使用 .lines 之间存在差异。如果您开始假设两者以相同的方式拆分数据,就会出现陷阱。

say $_.raku for $*IN.lines # .lines called on IO::Handle 
# OUTPUT: 
# "foox" 
# "fooy\rbar" 
# "fooz" 

如上面的示例所示,有一行包含 \r(“回车”控制字符)。但是,输入严格地按 \n 拆分,因此 \r 保留在字符串中。

另一方面,Str.lines 尝试“智能”地处理来自不同操作系统的數據。因此,它将按所有可能的换行符变体进行拆分。

say $_.raku for $*IN.slurp(:bin).decode.lines # .lines called on a Str 
# OUTPUT: 
# "foox" 
# "fooy" 
# "bar" 
# "fooz" 

规则很简单:在处理以编程方式生成的输出时使用 IO::Handle.lines,在处理用户编写的文本时使用 Str.lines

在您需要 IO::Handle.lines 的行为但原始 IO::Handle 不可用时,请使用 $data.split(“\n”)

请注意,如果您真的想先将数据吸入,那么您将不得不使用 .IO.slurp(:bin).decode.split(“\n”)。注意我们如何使用 :bin 来防止它进行解码,而只是稍后调用 .decode。所有这些都是必要的,因为 .slurp 假设您正在处理文本,因此它试图对换行符进行智能处理。

如果您使用的是 Proc::Async,那么目前还没有简单的方法让它以正确的方式拆分数据。您可以尝试读取整个输出,然后使用 Str.split(如果您处理的是大量数据,则不可行)或编写您自己的逻辑来按您需要的方式拆分传入数据。如果您的数据是空分隔的,则也是如此。

Proc::Asyncprint§

在使用 Proc::Async 时,您不应该假设 .print(或任何其他类似方法)是同步的。此陷阱的最大问题是您很可能不会通过运行代码一次来注意到问题,因此它可能会导致难以检测的间歇性故障。

这是一个演示问题的示例

loop {
    my $proc = Proc::Async.new: :whead-n1;
    my $got-something;
    react {
        whenever $proc.stdout.lines { $got-something = True }
        whenever $proc.start        { die FAIL! unless $got-something }
 
        $proc.print: one\ntwo\nthree\nfour;
        $proc.close-stdin;
    }
    say $++;
}

以及它可能产生的输出

0
1
2
3
An operation first awaited:
  in block <unit> at print.raku line 4

Died with the exception:
    FAIL!
      in block  at print.raku line 6

解决这个问题很简单,因为 .print 返回一个可以等待的 promise。如果你在 react 块中工作,解决方案会更漂亮

whenever $proc.print: one\ntwo\nthree\nfour {
    $proc.close-stdin;
}

使用 .stdout 但不使用 .lines§

Proc::Async.stdout 方法返回一个供应,它发出的是数据,而不是行。陷阱在于,有时人们会认为它会立即给出行。

my $proc = Proc::Async.new(cat/usr/share/dict/words);
react {
    whenever $proc.stdout.head(1{ .say } # ← WRONG (most likely) 
    whenever $proc.start { }
}

输出显然不仅仅是 1 行

A
A's
AMD
AMD's
AOL
AOL's
Aachen
Aachen's
Aaliyah
Aaliyah's
Aaron
Aaron's
Abbas
Abbas's
Abbasid
Abbasid's
Abbott
Abbott's
Abby

如果你想使用行,那么使用 $proc.stdout.lines。如果你想要整个输出,那么类似这样的代码应该可以解决问题:whenever $proc.stdout { $out ~= $_ }

异常处理§

沉没的 Proc§

有些方法返回一个 Proc 对象。如果它代表一个失败的进程,Proc 本身不会是异常,但沉没它会导致抛出 X::Proc::Unsuccessful 异常。这意味着即使有 try,这个结构也会抛出异常

try run("raku""-e""exit 42");
say "still alive";
# OUTPUT: «The spawned process exited unsuccessfully (exit code: 42)␤» 

这是因为 try 接收一个 Proc 并返回它,此时它会沉没并抛出异常。在 try 中显式地沉没它可以避免这个问题,并确保异常在 try 中抛出

try sink run("raku""-e""exit 42");
say "still alive";
# OUTPUT: «still alive␤» 

如果你对捕获任何异常不感兴趣,那么使用一个匿名变量来保存返回的 Proc;这样它就不会沉没

$ = run("raku""-e""exit 42");
say "still alive";
# OUTPUT: «still alive␤» 

使用快捷方式§

^ 修饰符§

使用 ^ 修饰符可以节省大量时间和空间,尤其是在编写小块代码时。例如

for 1..8 -> $a$b { say $a + $b}

可以简化为

for 1..8 { say $^a + $^b}

当一个人想要使用更复杂的变量名,而不是仅仅使用一个字母时,问题就出现了。^ 修饰符能够让位置变量乱序并命名为任何你想要的,但它根据变量的 Unicode 排序分配值。在上面的例子中,我们可以让 $^a$^b 交换位置,这些变量将保持它们的位置值。这是因为 Unicode 字符 'a' 在字符 'b' 之前。例如

# In order 
sub f1 { say "$^first $^second"}
f1 "Hello""there";    # OUTPUT: «Hello there␤» 
# Out of order 
sub f2 { say "$^second $^first"}
f2 "Hello""there";    # OUTPUT: «there Hello␤» 

由于变量可以被命名为任何东西,如果你不习惯 Raku 如何处理这些变量,这可能会导致一些问题。

# BAD NAMING: alphabetically `four` comes first and gets value `1` in it: 
for 1..4 { say "$^one $^two $^three $^four"}    # OUTPUT: «2 4 3 1␤» 
 
# GOOD NAMING: variables' naming makes it clear how they sort alphabetically: 
for 1..4 { say "$^a $^b $^c $^d"}               # OUTPUT: «1 2 3 4␤» 

使用 »map 交换§

虽然 » 看起来像是编写 map 的更短方式,但它们在一些关键方面有所不同。

首先,» 包含一个提示,告诉编译器它可以自动线程执行,因此如果你使用它来调用一个产生副作用的例程,这些副作用可能会以乱序的方式产生(操作符的结果是按顺序保持的)。此外,如果被调用的例程访问了一个资源,那么可能会出现竞争条件,因为多个调用可能同时发生,来自不同的线程。

<a b c d>».say # OUTPUT: «d␤b␤c␤a␤» 

其次,» 检查被调用的例程的 节点性,并根据它使用 deepmapnodemap 来映射列表,这可能与 map 调用映射列表的方式不同

say ((123), [^4], '5'.Numeric;       # OUTPUT: «((1 2 3) [0 1 2 3] 5)␤» 
say ((123), [^4], '5').map: *.Numeric# OUTPUT: «(3 4 5)␤» 

底线是 map» 不可互换,但只要你理解它们之间的差异,使用其中一个代替另一个是可以的。

« » 中的单词分割§

请记住,« » 执行单词分割的方式类似于 shell 的方式,因此 许多 shell 陷阱 也适用于此(尤其是在与 run 结合使用时)

my $file = --my arbitrary filename;
run touch--$file;  # RIGHT 
run <touch -->$file;     # RIGHT 
 
run «touch -- "$file"»;    # RIGHT but WRONG if you forget quotes 
run «touch -- $file»;      # WRONG; touches ‘--my’, ‘arbitrary’ and ‘filename’ 
run touch$file;        # WRONG; error from `touch` 
run «touch "$file"»;       # WRONG; error from `touch`

请注意,对于许多程序来说,-- 是必需的,以区分命令行参数和 以连字符开头的文件名

作用域§

使用 once§

once 块是一段代码块,当其父代码块运行时,它只运行一次。例如

my $var = 0;
for 1..10 {
    once { $var++}
}
say "Variable = $var";    # OUTPUT: «Variable = 1␤» 

此功能也适用于其他代码块,例如 subwhile,而不仅仅是 for 循环。但是,当尝试将 once 块嵌套在其他代码块中时,就会出现问题。

my $var = 0;
for 1..10 {
    do { once { $var++} }
}
say "Variable = $var";    # OUTPUT: «Variable = 10␤» 

在上面的示例中,once 块嵌套在一个代码块中,该代码块又嵌套在一个 for 循环代码块中。这会导致 once 块运行多次,因为 once 块使用状态变量来确定它是否已运行。这意味着如果父代码块超出范围,那么 once 块用来跟踪它是否已运行的状态变量也会超出范围。这就是为什么 once 块和 state 变量在嵌套在多个代码块中时会导致一些意外行为。

如果你想拥有一个模拟 once 块功能的东西,但仍然可以在嵌套几个代码块的情况下工作,我们可以手动构建 once 块的功能。使用上面的示例,我们可以更改它,使其即使在 do 块中也能只运行一次,方法是更改 state 变量的范围。

my $var = 0;
for 1..10 {
    state $run-code = True;
    do { if ($run-code{ $run-code = False$var++} }
}
say "Variable = $var";    # OUTPUT: «Variable = 1␤» 

在这个示例中,我们本质上是通过在将要运行多次的最高级别创建一个名为 $run-codestate 变量,然后使用常规的 if 检查 $run-code 是否为 True,来手动构建一个 once 块。如果变量 $run-codeTrue,则将该变量设为 False,并继续执行应该只完成一次的代码。

使用像上面示例这样的 state 变量和使用常规 once 块之间的主要区别在于 state 变量的范围。由 once 块创建的 state 变量的范围与你放置块的位置相同(想象一下,将单词“once”替换为一个状态变量和一个用于查看该变量的 if)。上面使用 state 变量的示例之所以有效,是因为该变量位于将要重复的最高范围内;而将 once 块放在 do 块中的示例,则将变量放在了 do 块中,而 do 块不是将要重复的最高范围。

在类方法中使用 once 块会导致一次状态在该类的所有实例中传递。例如

class A {
    method sayit() { once say 'hi' }
}
my $a = A.new;
$a.sayit;      # OUTPUT: «hi␤» 
my $b = A.new;
$b.sayit;      # nothing 

LEAVE 阶段和 exit§

使用 LEAVE 阶段来执行优雅的资源终止是一种常见模式,但它不涵盖程序使用 exit 停止的情况。

以下非确定性示例应该说明了此陷阱的复杂性。

my $x = say Opened some resource;
LEAVE say Closing the resource gracefully with $x;
 
exit 42 if rand < ⅓; # ① 「exit」 is bad 
die Dying because of unhandled exception if rand < ½; # ② 「die」 is ok 
# fallthru ③ 

有三种可能的结果。

①
Opened some resource

②
Opened some resource
Closing the resource gracefully
Dying because of unhandled exception
  in block <unit> at print.raku line 5

③
Opened some resource
Closing the resource gracefully

exit 的调用是许多程序正常操作的一部分,因此请注意 LEAVE 阶段和 exit 调用之间的意外组合。

LEAVE 阶段可能比你想象的要早运行§

当我们“在”例程的代码块中时,参数绑定就会执行,这意味着如果在给出错误参数时参数绑定失败,那么当我们离开该代码块时,LEAVE 阶段就会运行。

sub foo(Int{
    my $x = 42;
    LEAVE say $x.Int# ← WRONG; assumes that $x is set 
}
say foo rand# OUTPUT: «No such method 'Int' for invocant of type 'Any'␤» 

避免此问题的简单方法是将你的子例程或方法声明为多态的,这样候选者就会在调度期间被淘汰,代码永远不会绑定子例程中的任何内容,因此永远不会进入例程的主体。

multi foo(Int{
    my $x = 42;
    LEAVE say $x.Int;
}
say foo rand# OUTPUT: «Cannot resolve caller foo(Num); none of these signatures match: (Int)␤» 

另一种方法是将 LEAVE 放入另一个代码块中(假设它适合在代码块离开时执行,而不是例程的主体)。

sub foo(Int{
    my $x = 42;
    { LEAVE say $x.Int}
}
say foo rand# OUTPUT: «Type check failed in binding to parameter '<anon>'; expected Int but got Num (0.7289418947969465e0)␤» 

你还可以确保即使例程由于参数绑定失败而离开,LEAVE 也可以执行。在我们的示例中,我们在对 $x 做任何操作之前,先检查它是否已定义

sub foo(Int{
    my $x = 42;
    LEAVE $x andthen .Int.say;
}
say foo rand# OUTPUT: «Type check failed in binding to parameter '<anon>'; expected Int but got Num (0.8517160389079508e0)␤» 

语法§

在语法操作中使用正则表达式§

# Define a grammar 
grammar will-fail {
    token TOP {^ <word> $}
    token word { \w+ }
}
 
# Define an action class 
class will-fail-actions {
    method TOP ($/{      # <- note the $/ in the signature, which is readonly 
        my $foo = ~$/;
        say $foo ~~ /foo/# <- the regex tries to assign the result to $/ and will fail 
    }
}
 
# Try to parse something... 
will-fail.parse('word':actions(will-fail-actions));
CATCH { default { put .^name''.Str } };
# OUTPUT: «X::AdHoc: Cannot assign to a readonly variable or a value␤» 

在方法 TOP 中,使用 $/ 会导致错误 Cannot assign to a readonly variable ($/) or a value。问题在于正则表达式也会影响 $/。由于它在 TOP 的签名中,它是一个只读变量,这就是导致错误的原因。您可以安全地使用签名中的另一个变量,或者添加 is copy,这样

method TOP ($/ is copy{ my $foo = ~$/my $v = $foo ~~ /foo/;  }

使用某些名称作为规则/标记/正则表达式§

语法实际上是一种类。

grammar G {};
say G.^mro# OUTPUT: «((G) (Grammar) (Match) (Capture) (Cool) (Any) (Mu))␤»

^mro 打印此空语法的类层次结构,显示所有超类。这些超类有它们自己的方法。在该语法中定义的方法可能会与类层次结构中的方法冲突

grammar g {
    token TOP { <item> };
    token item { 'defined' }
};
say g.parse('defined');
# OUTPUT: «Too many positionals passed; expected 1 argument but got 2␤  in regex item at /tmp/grammar-clash.raku line 3␤  in regex TOP at /tmp/grammar-clash.raku line 2␤  in block <unit> at /tmp/grammar-clash.raku line 5» 

item 看起来很无害,但它是一个 sub 定义在类 Mu。消息有点神秘,与这个事实完全无关,但这就是为什么它被列为陷阱。一般来说,在层次结构的任何部分定义的所有子程序都会导致问题;一些方法也会导致问题。例如,CREATEtakedefined(它们在 Mu 中定义)。一般来说,多方法和简单方法不会有任何问题,但将它们用作规则名称可能不是一个好习惯。

还要避免使用 阶段 作为规则/标记/正则表达式名称:TWEAKBUILDBUILD-ALL 如果这样做会抛出另一种类型的异常:Cannot find method 'match': no method cache and no .^find_method,这再次与实际发生的事情只有很小的关系。

不幸的泛化§

:exists 使用多个键§

假设您有一个哈希,并且您想对多个元素使用 :exists

my %h = => 1=> 2;
say a exists if %h<a>:exists;   # ← OK; True 
say y exists if %h<y>:exists;   # ← OK; False 
say Huh‽     if %h<x y>:exists# ← WRONG; returns a 2-item list 

您的意思是“如果它们中的 any 存在”,还是您指的是它们都应该存在?使用 anyall Junction 来澄清

my %h = => 1=> 2;
say x or y     if any %h<x y>:exists;   # ← RIGHT (any); False 
say a, x or y  if any %h<a x y>:exists# ← RIGHT (any); True 
say a, x and y if all %h<a x y>:exists# ← RIGHT (all); False 
say a and b    if all %h<a b>:exists;   # ← RIGHT (all); True 

它始终为 True(不使用连接)的原因是它返回一个包含每个请求查找的 Bool 值的列表。非空列表在您将其 Bool 化时始终返回 True,因此无论您提供什么键,检查始终成功。

使用 […] 元运算符与列表列表§

偶尔,有人会想到可以使用 [Z] 来创建列表列表的转置

my @matrix = <X Y>, <a b>, <1 2>;
my @transpose = [Z@matrix# ← WRONG; but so far so good ↙ 
say @transpose;              # OUTPUT: «[(X a 1) (Y b 2)]␤» 

一切都很好,直到您得到一个输入 @matrix,它只有一个行(子列表)

my @matrix = <X Y>,;
my @transpose = [Z@matrix# ← WRONG; ↙ 
say @transpose;              # OUTPUT: «[(X Y)]␤» – not the expected transpose [(X) (Y)] 

这部分是由于 单参数规则,并且在其他情况下,这种泛化可能无法正常工作。

使用 [~] 连接 Blob 列表§

~ 中缀运算符 可用于连接 StrBlob。但是,空列表始终会简化为一个空 Str。这是因为,在存在没有元素的列表的情况下,归约 元运算符返回给定运算符的 标识元素~ 的标识元素是一个空字符串,无论列表可以填充的元素类型如何。

my Blob @chunks;
say ([~@chunks).raku# OUTPUT: «""␤» 

如果您尝试在假设它是 Blob 的情况下使用结果,这可能会导致问题

my Blob @chunks;
say ([~@chunks).decode;
# OUTPUT: «No such method 'decode' for invocant of type 'Str'. Did you mean 'encode'?␤…» 

有很多方法可以解决这种情况。您可以完全避免使用 [ ] 元运算符

my @chunks;
# … 
say Blob.new: |«@chunks# OUTPUT: «Blob:0x<>␤» 

或者,您可以使用一个空 Blob 初始化数组

my @chunks = Blob.new;
# … 
say [~@chunks# OUTPUT: «Blob:0x<>␤» 

或者,您可以利用 || 运算符,以便在列表为空的情况下使用空 Blob

my @chunks;
# … 
say [~@chunks || Blob.new# OUTPUT: «Blob:0x<>␤» 

请注意,在使用其他运算符归约列表时,可能会出现类似的问题。

地图§

小心在 sink 上下文中嵌套 Maps§

Maps 将表达式应用于 List 的每个元素并返回一个 Seq

say <þor oðin loki>.map: *.codes# OUTPUT: «(3 4 4)␤»

Maps 通常用作循环的紧凑替代方案,在 map 代码块中执行某种操作

<þor oðin loki>.map: *.codes.say# OUTPUT: «3␤4␤4␤»

当 maps 嵌套并且 在 sink 上下文中 时,可能会出现问题。

<foo bar ber>.map: { $^a.comb.map: { $^b.say}}# OUTPUT: «»

您可能期望最内层的 map 将结果冒泡到最外层的 map,但它什么也不做。Maps 返回 Seqs,在 sink 上下文中,最内层的 map 将迭代并丢弃生成的 value,这就是它不产生任何结果的原因。

只需在句子的开头使用 say 即可将结果从 sink 上下文中保存下来

say <foo bar ber>.map: *.comb.map: *.say ;
# OUTPUT: «f␤o␤o␤b␤a␤r␤b␤e␤r␤((True True True) (True True True) (True True True))␤»

但是,它不会按预期工作;第一个 f␤o␤o␤b␤a␤r␤b␤e␤r␤ 是最内层 say 的结果,但随后 say 返回一个 Bool,在本例中为 True。这些 True 是最外层 say 打印的,每个字母一个。一个更好的选择是将最外层的序列 flat

<foo bar ber>.map({ $^a.comb.map: { $^b.say}}).flat
# OUTPUT: «f␤o␤o␤b␤a␤r␤b␤e␤r␤»

当然,将 say 保存为结果也将产生预期的结果,因为它将从 void 上下文中保存两个嵌套的序列

say <foo bar ber>.map: { $^þ.comb }# OUTPUT: « ((f o o) (b a r) (b e r))␤»

智能匹配§

智能匹配运算符 缩短到右侧接受左侧。这可能会造成一些混淆。

智能匹配和 WhateverCode§

在智能匹配的左侧使用 WhateverCode 不会按预期工作,或者根本不工作

my @a = <1 2 3>;
say @a.grep*.Int ~~ 2 );
# OUTPUT: «Cannot use Bool as Matcher with '.grep'.  Did you mean to 
# use $_ inside a block?␤␤␤» 

错误消息没有多大意义。但是,如果您用 ACCEPTS 方法来表达,它确实有意义:该代码等效于 2.ACCEPTS( *.Int ),但 *.Int 无法 强制转换为 Numeric,因为它是一个 Block

解决方案:不要在智能匹配的左侧使用 WhateverCode

my @a = <1 2 3>;
say @a.grep2 ~~ *.Int ); # OUTPUT: «(2)␤» 

§

文件系统存储库§

将大型目录添加到库搜索路径可能会对模块加载时间产生负面影响,即使加载的模块不在任何添加的路径中。这是因为编译器需要遍历并校验树中所有相关的文件才能加载请求的模块,除非目录包含 META6.json 文件。

有三种常见的将目录添加到搜索路径的方法

  • 在命令行上为 Raku 提供一个开关:$ raku -Idir

  • 设置环境变量:$ env RAKULIB=dir raku

  • 使用 lib pragma(即 use lib 'dir'

当使用任何这些方法添加特别大或深度嵌套的目录时,性能损失很明显。例如,在典型的 Unix 机器上将 /usr/lib 添加到搜索路径会对加载模块时产生明显的性能影响,

    $ time raku -e 'use Test'
    real    0m0.511s
    user    0m0.554s
    sys     0m0.118s

    # no penalty, since we don't load anything:
    $ time raku -I/usr/lib -e ''
    real    0m0.247s
    user    0m0.254s
    sys     0m0.066s

    $ time raku -I/usr/lib -e 'use Test'
    real    0m6.344s
    user    0m6.232s
    sys     0m0.356s

    $ time raku -e 'use lib "/usr/lib"; use Test'
    real    0m6.555s
    user    0m6.445s
    sys     0m0.326s

    $ time env RAKULIB=/usr/lib raku -e 'use Test'
    real    0m6.479s
    user    0m6.368s
    sys     0m0.383s

请注意,即使请求已安装在其他地方的模块,也会发生运行时间增加(Test 是作为 Rakudo 的一部分提供的,但仍然扫描了 /usr/lib 才能加载它)。

避免此陷阱的最佳方法是安装驻留在大型树中的模块,并完全省略这些目录。或者,将 META6.json 文件添加到树中将阻止耗时的遍历。