假设你举办了一场乒乓球比赛。裁判以 Player1 Player2 | 3:2
的格式告诉你每场比赛的结果,这意味着 Player1
以 3:2 的比分战胜了 Player2
。你需要一个脚本来总结每个玩家赢得的比赛和局数,以确定最终的获胜者。
输入数据(存储在名为 scores.txt
的文件中)如下所示
Beth Ana Charlie Dave Ana Dave | 3:0 Charlie Beth | 3:1 Ana Beth | 2:3 Dave Charlie | 3:0 Ana Charlie | 3:1 Beth Dave | 0:3
第一行是玩家列表。每一行记录一场比赛的结果。
以下是在 Raku 中解决该问题的其中一种方法
use v6;# start by printing out the header.say "Tournament Results:\n";my = open 'scores.txt'; # get filehandle and...my = .get.words; # ... get players.my ;my ;for .lines ->my = .sort().sort().reverse;for ->
这将产生以下输出
Tournament Results: Ana has won 2 matches and 8 sets Dave has won 2 matches and 6 sets Charlie has won 1 match and 4 sets Beth has won 1 match and 4 sets
v6
§
use v6;
每个 Raku 程序都应从类似于 use v6;
的行开始。此行告诉编译器程序期望的 Raku 版本。例如,6.c 是 Raku 版本的一个示例。如果你不小心用 Perl 运行文件,你将收到一条有用的错误消息。
语句§
# start by printing out the header.say "Tournament Results:\n";
Raku 程序由零个或多个语句组成。语句以分号或行尾的大括号结尾。
在 Raku 中,单行注释以单个哈希字符 #
开始,并一直延伸到行尾。Raku 还支持 多行/嵌入式注释。编译器不会将注释评估为程序文本,它们仅供人类读者使用。
词法作用域§
my = open 'scores.txt';
my
声明一个词法变量,它仅在声明点到块结束的当前块中可见。如果没有封闭块,它将在文件的其余部分(实际上将是封闭块)中可见。块是包含在大括号 { }
中的任何代码部分。
符印§
变量名以符印开头,符印是非字母数字符号,例如 $
、@
、%
或 &
— 或偶尔是双冒号 ::
。符印指示变量的结构化接口,例如它应被视为单个值、复合值、子例程等。符印后是标识符,它可能由字母、数字和下划线组成。在字母之间,你还可以使用破折号 -
或撇号 '
,因此 isn't
和 double-click
是有效的标识符。
标量§
Sigils 表示变量的默认访问方法。带有 @
sigil 的变量按位置访问;带有 %
sigil 的变量按字符串键访问。但是,$
sigil 表示一个常规标量容器,它可以保存任何单个值,并可以以任何方式访问。标量甚至可以包含复合对象,如 Array
或 Hash
;$
sigil 表示应将其视为单个值,即使在期望多个值(如 Array
或 Hash
)的上下文中也是如此。
文件句柄§
内置函数 open
打开一个文件,此处名为 scores.txt
,并返回一个文件句柄——表示该文件的一个对象。赋值运算符 =
将该文件句柄赋值给左侧的变量,这意味着 $file
现在存储文件句柄。
字符串文字§
'scores.txt'
是一个字符串文字。字符串是一段文本,字符串文字是直接出现在程序中的字符串。在此行中,它是提供给 open
的参数。
数组§
my = .get.words;
右侧调用一个方法——一个名为 get
的命名行为组——在存储在 $file
中的文件句柄上。get
方法从文件中读取并返回一行,删除行尾。如果在调用 get
后打印 $file
的内容,你将看到第一行已不再存在。words
也是一个方法,在从 get
返回的字符串上调用。words
将其调用者——它操作的字符串——分解为一个单词列表,此处表示用空格分隔的字符串。它将单个字符串 'Beth Ana Charlie Dave'
转换为字符串列表 'Beth', 'Ana', 'Charlie', 'Dave'
。
最后,此列表存储在 Array
@names
中。@
sigil 将声明的变量标记为 Array
。数组存储有序列表。
哈希§
my ;my ;
这两行代码声明了两个哈希。%
sigil 将每个变量标记为 Hash
。 Hash
是一个无序的键值对集合。其他编程语言将其称为哈希表、字典或映射。你可以使用 %hash{$key}
查询哈希表以获取对应于特定 $key
的值。
在比分统计程序中,%matches
存储了每个选手赢得的比赛场次。%sets
存储了每个选手赢得的局数。这两个哈希都以选手姓名为索引。
for
§
for .lines ->
for
产生一个循环,该循环对列表中的每一项运行大括号分隔的块一次,将变量 $line
设置为每次迭代的当前值。$file.lines
产生从文件 scores.txt
中读取的行列表,从文件的第二行开始,因为我们已经调用过 $file.get
一次,一直到文件的末尾。
在第一次迭代期间,$line
将包含字符串 Ana Dave | 3:0
;在第二次迭代期间,包含 Charlie Beth | 3:1
,依此类推。
my (, ) = .split(' | ');
my
可以同时声明多个变量。赋值的右侧是调用名为 split
的方法,传递字符串 ' | '
作为参数。
split
将其调用者分解为字符串列表,以便使用分隔符 ' | '
连接列表项会产生原始字符串。
$pairing
获取返回列表的第一个项,$result
获取第二个项。
在处理第一行后,$pairing
将保存字符串 Ana Dave
,$result
将保存 3:0
。
接下来的两行遵循相同的模式
my (, ) = .words;my (, ) = .split(':');
第一行提取并存储两个选手的姓名在变量 $p1
和 $p2
中。第二行提取每个选手的结果并存储在 $r1
和 $r2
中。
在处理文件的第 1 行后,变量包含以下值
变量 | 内容 |
$line | 'Ana Dave | 3:0' |
$pairing | 'Ana Dave' |
$result | '3:0' |
$p1 | 'Ana' |
$p2 | 'Dave' |
$r1 | '3' |
$r2 | '0' |
然后程序计算每个选手赢得的局数
+= ;+= ;
以上两个语句涉及 +=
复合赋值运算符。它们是使用简单赋值运算符 =
的以下两个语句的变体,乍一看可能更容易理解
= + ;= + ;
对于你的程序,首选使用 +=
复合赋值运算符的简写版本,而不是使用简单赋值运算符 =
的长写版本。这不仅是因为较短的版本需要更少的键入,还因为 +=
运算符会将哈希键值对的未定义值静默初始化为零。换句话说:通过使用 +=
,无需在向其中有意义地添加 $r1
之前包含单独的语句,如 %sets{$p1} = 0
。我们将在下面更详细地了解此行为。
Any" data-indexedheader="Tutorial;Any (Basics)">Any
§
+= ;+= ;
%sets{$p1} += $r1
表示将左侧变量中的值增加 $r1。在第一次迭代中,%sets{$p1}
尚未定义,因此它默认为一个称为 Any
的特殊值。+=
运算符方便地将未定义值 Any
视为值为 0
的数字,允许它明智地向其中添加一些其他值。为了执行加法,字符串 $r1
、$r2
等会自动转换为数字,因为加法是一个数字运算。
胖箭头§
在执行这两行之前,%sets
为空。向哈希中尚未存在的条目添加内容将导致该条目立即出现,其值从零开始。此行为称为自动生成。在首次运行这两行之后,%sets
包含 'Ana' => 3, 'Dave' => 0
。(胖箭头 =>
在 Pair
中分隔键和值。)
后增量§
if >else
如果 $r1
数值上大于 $r2
,则 %matches{$p1}
增加 1。如果 $r1
不大于 $r2
,则 %matches{$p2}
增加。与 +=
的情况一样,如果之前不存在哈希值,则增量操作会自动生成该值。
$thing++
是 $thing += 1
的变体;它与后者的不同之处在于,表达式的返回值是在增加之前的 $thing
,而不是增加后的值。与许多其他编程语言一样,你可以将 ++
用作前缀。然后它返回增加后的值:my $x = 1; say ++$x
打印 2
。
主题变量§
my = .sort().sort().reverse;
此行由三个单独的简单步骤组成。数组的 sort
方法返回数组内容的已排序版本。但是,数组上的默认排序按其内容排序。要按获胜者优先的顺序打印玩家姓名,代码必须按玩家的得分对数组进行排序,而不是按姓名排序。sort
方法的参数是一个块,用于将数组元素(玩家姓名)转换为按其排序的数据。数组项通过主题变量 $_
传递。
块§
你之前见过块:for
循环 -> $line { ... }
和 if
语句都在块上工作。块是 Raku 代码的一个自包含部分,具有可选签名(-> $line
部分)。
按得分对玩家进行排序的最简单方法是 @names.sort({ %matches{$_} })
,按获胜场次排序。然而,Ana 和 Dave 都赢得了两场比赛。这种简单的排序没有考虑获胜的盘数,这是决定谁赢得比赛的次要标准。
稳定排序§
当两个数组项具有相同的值时,sort
会将它们保留在找到它们的顺序中。计算机科学家称之为稳定排序。该程序利用 Raku 的 sort
的这一特性,通过两次排序来实现目标:首先按获胜的盘数(次要标准)排序,然后按获胜的场次(主要标准)排序。
在第一个排序步骤之后,名称按 Beth Charlie Dave Ana
的顺序排列。在第二个排序步骤之后,它仍然相同,因为没有人比其他人赢得的场次更少,但赢得的盘数更多。这种情况完全有可能发生,尤其是在大型比赛中。
sort
按升序从最小到最大排序。这与所需顺序相反。因此,代码对第二次排序的结果调用 .reverse
方法,并将最终列表存储在 @sorted
中。
标准输出§
for ->
要打印出玩家及其分数,代码循环遍历 @sorted
,将 $n
设置为每个玩家的名称。将此代码理解为“对于 sorted 的每个元素,将 $n
设置为元素,然后执行以下块的内容。”变量 $match-noun
将存储字符串 match(如果玩家赢得一场比赛)或 matches(如果玩家赢得零场或多场比赛)。为此,使用了三元运算符 (?? !!
)。如果 %matches{$n} == 1
计算结果为 True
,则返回 match。否则,返回 matches。无论哪种方式,返回的值都存储在 $match-noun
中。相同的方法适用于 $set-noun
。
语句 say
将其参数打印到标准输出(通常是屏幕),后跟换行符。(如果您不想要末尾的换行符,请使用 print
。)
请注意,say
将通过调用 .gist
方法截断某些数据结构,因此如果您想要确切的输出,put
会更安全。
变量插值§
当您运行程序时,您会看到 say
不会逐字打印该字符串的内容。它会打印变量 $n
的内容,而不是 $n
— 存储在 $n
中的玩家名称。这种用代码内容自动替换代码的行为称为插值。此插值仅发生在用双引号 "..."
分隔的字符串中。单引号字符串 '...'
不进行插值
双引号字符串§
my = 'things';say 'Do not call me $names'; # OUTPUT: «Do not call me $names»say "Do not call me $names"; # OUTPUT: «Do not call me things»
Raku 中的双引号字符串可以使用 $
sigil 插值变量,也可以插值大括号中的代码块。由于任何任意的 Raku 代码都可以出现在大括号中,因此可以通过将 Array
和 Hash
放在大括号中来插值它们。
大括号中的数组在每个项目之间插值一个空格字符。大括号中的哈希作为一系列行进行插值。每行将包含一个键,后跟一个制表符,然后是与该键关联的值,最后是一个换行符。
我们现在来看一个例子。
在此示例中,您将看到一些特殊语法,它可以更轻松地创建字符串列表。这是 <...>
quote-words 构造。当您将单词放在 <
和 >
之间时,它们都被假定为字符串,因此您不必用双引号 "..."
将它们分别包裹起来。
say "Math: ";# OUTPUT: «Math: 3»my = <Luke Matthew Mark>;say "The synoptics are: ";# OUTPUT: «The synoptics are: Luke Matthew Mark»say "";# OUTPUT (From the table tennis tournament):# Charlie 4# Dave 6# Ana 8# Beth 4
当数组和哈希变量直接出现在双引号字符串中(而不是在大括号内)时,只有当其名称后跟后缀运算符(一个后跟语句的括号对)时,才会进行插值。在变量名和后缀之间进行方法调用也是可以的。
Zen 切片§
my = <vanilla peach>;say "we have @flavors"; # OUTPUT: «we have @flavors»say "we have @flavors[0]"; # OUTPUT: «we have vanilla»# so-called "Zen slice"say "we have @flavors[]"; # OUTPUT: «we have vanilla peach»# method calls ending in postcircumfixsay "we have @flavors.sort()"; # OUTPUT: «we have peach vanilla»# chained method calls:say "we have @flavors.sort.join(', ')";# OUTPUT: «we have peach, vanilla»
练习§
1. 示例程序的输入格式是冗余的:包含所有玩家名称的第一行不是必需的,因为您可以通过查看后续行中的名称来找出哪些玩家参加了锦标赛,因此原则上我们可以安全地将其删除。
如果您不使用 @names
变量,如何使程序运行?提示:%hash.keys
返回存储在 %hash
中的所有键的列表。
答案:删除 scores.txt
中的第一行后,删除行 my @names = $file.get.words;
(它会读取该行),然后更改
my = .sort().sort().reverse;
... 为
my = .keys.sort().sort().reverse;
2. 您可以使用冗余的 @names
变量来警告出现未在第一行中提到的玩家,例如由于错字。您将如何修改程序以实现此目的?
提示:尝试使用 成员运算符。
答案:将 @names
更改为 @valid-players
。在循环遍历文件行时,检查 $p1
和 $p2
是否在 @valid-players
中。请注意,对于成员运算符,您还可以使用 (elem)
和 !(elem)
。
...;my = .get.words;...;for .lines ->