关联角色和关联类§

Associative 角色是哈希和映射以及其他类(如 MixHash)的基础。它定义了将在关联类中使用的两种类型;默认情况下,您可以使用任何东西(从字面上讲,因为任何子类 Any 的类都可以使用)作为键,尽管它将被强制转换为字符串,以及任何对象作为值。您可以使用 ofkeyof 方法访问这些类型。

默认情况下,任何使用 % 符号声明的对象都将获得 Associative 角色,并且默认情况下将表现得像哈希,但此角色仅提供上述两种方法,以及默认的哈希行为。

say (%).^name ; # OUTPUT: «Hash␤»

反之,如果未混合 Associative 角色,则不能使用 % 符号,但由于此角色没有任何关联的属性,因此您将必须重新定义 哈希下标运算符 的行为。为此,您将必须覆盖几个函数

class Logger does Associative[Cool,DateTime{
    has %.store;
 
    method logCool $event ) {
        %.store{ DateTime.newnow ) } = $event;
    }
 
    multi method AT-KEY ( ::?CLASS:D: $key{
        my @keys = %.store.keys.grep( /$key/ );
        %.store{ @keys };
    }
 
    multi method EXISTS-KEY (::?CLASS:D: $key{
        %.store.keys.grep( /$key/ )??True!!False;
    }
 
    multi method DELETE-KEY (::?CLASS:D: $key{
        X::Assignment::RO.new.throw;
    }
 
    multi method ASSIGN-KEY (::?CLASS:D: $key$new{
        X::Assignment::RO.new.throw;
    }
 
    multi method BIND-KEY (::?CLASS:D: $key, \new){
        X::Assignment::RO.new.throw;
    }
}
say Logger.of;                   # OUTPUT: «(Cool)␤» 
my %logger := Logger.new;
say %logger.of;                  # OUTPUT: «(Cool)␤» 
 
%logger.log"Stuff" );
%logger.log"More stuff");
 
say %logger<2018-05-26>;         # OUTPUT: «(More stuff Stuff)␤» 
say %logger<2018-04-22>:exists;  # OUTPUT: «False␤» 

在本例中,我们定义了一个具有关联语义的日志记录器,它能够使用日期(或日期的一部分)作为键。由于我们正在将 Associative 参数化为这些特定的类,因此 of 将返回我们使用的值类型,在本例中为 Cool(我们只能记录列表或字符串)。混合 Associative 角色赋予它使用 % 符号的权利;定义中需要绑定,因为 % 符号变量默认情况下获得 Hash 类型。

此日志将是追加式的,这就是为什么我们跳出关联数组的隐喻,使用 log 方法将新事件添加到日志中。但是,一旦它们被添加,我们就可以按日期检索它们或检查它们是否存在。对于前者,我们必须覆盖 AT-KEY 多方法,对于后者,我们必须覆盖 EXIST-KEY。在最后两条语句中,我们展示了如何使用下标操作调用 AT-KEY,而 :exists 副词调用 EXISTS-KEY

我们覆盖了 DELETE-KEYASSIGN-KEYBIND-KEY,但只是为了抛出异常。尝试将值分配、删除或绑定到键将导致抛出 Cannot modify an immutable Str (value) 异常。

使类具有关联性提供了一种非常方便的方式来使用和处理它们,使用哈希;可以在 Cro 中看到一个示例,它广泛使用它来方便地使用哈希来定义结构化请求并表达其响应。

可变哈希和不可变映射§

一个 Hash 是一个从键到值的可变映射(在其他编程语言中称为字典哈希表映射)。这些值都是标量容器,这意味着您可以为它们赋值。另一方面,Map 是不可变的。一旦一个键与一个值配对,这个配对就不能改变。

映射和哈希通常存储在带有百分号 % 符号的变量中,该符号用于指示它们是关联的。

哈希和映射元素通过键使用 { } 后缀运算符访问。

say %*ENV{'HOME''PATH'}.raku;
# OUTPUT: «("/home/camelia", "/usr/bin:/sbin:/bin")␤»

一般的 下标 规则适用,为带和不带插值的文字字符串列表提供快捷方式。

my %h = oranges => 'round'bananas => 'bendy';
say %h<oranges bananas>;
# OUTPUT: «(round bendy)␤» 
 
my $fruit = 'bananas';
say %h«oranges "$fruit"»;
# OUTPUT: «(round bendy)␤»

您可以通过简单地为未使用的键赋值来添加新的配对。

my %h;
%h{'new key'} = 'new value';

哈希赋值§

将元素列表分配给哈希变量首先清空该变量,然后迭代右侧元素(必须包含偶数个元素)。请注意,哈希键始终被强制转换为字符串,即使它们没有被引用,但名称中包含空格的键必须被引用。例如,使用常见的 Pair 语法

my %h = 1 => 'a'=> 2'1 2' => 3;
say %h.keys.sort.raku;       # OUTPUT: «("1", "1 2", "b").Seq␤» 
say %h.values.sort.raku      # OUTPUT: «(2, 3, "a").Seq␤»

键的字符串化可能会导致意外的结果。例如,Raku 模块 CSV::Parser 在一种模式下将 CSV 数据行返回为 {UInt => 'value'} 配对的哈希,它可能看起来像 20 字段行的这一片段

my %csv = '2' => 'a''10' => 'b';
say %csv.keys.sort.raku;           # OUTPUT: «("10", "2").Seq␤»

排序结果不是人们通常想要的。我们可以利用 Raku 的强大功能将字符串值强制转换为整数以进行排序。有几种方法可以做到这一点,但这种方法可能对新手或非 Raku 观众来说“行噪声”最小。

say %csv.keys.map(+*).sort.raku;   # OUTPUT: «(2, 10).Seq␤» 

在哈希构造函数列表中,不需要使用 Pair 语法,或者可以与普通列表值混合使用,只要列表总共有偶数个元素,并且在 Pair 之间也是如此。如果一个元素是 Pair 类型(例如,'a => 1'),它的键被视为新的哈希键,它的值被视为该键的新哈希值。否则,该值被强制转换为 Str 并用作哈希键,而列表的下一个元素被用作相应的键值。

my %h = 'a''b'=> 'd''e''f';

与以下相同

my %h = => 'b'=> 'd'=> 'f';

my %h = <a b c d e f>;

甚至

my %h = %=> 'b'=> 'd'=> 'f' );

如果您使用大多数构造函数有奇数个元素,您将看到错误

my %h = <a b c># OUTPUT: «hash initializer expected...␤»

还有两种有效的哈希构造方法,但用户应该谨慎。

my %h = [ => 'b'=> 'd'=> 'f' ]; # This format is NOT recommended. 
                                          # It cannot be a constant and there 
                                          # will be problems with nested hashes

my $h = { => 'b'=> 'd'=> 'f'};

请注意,花括号只在不将哈希赋值给以%开头的变量时使用;如果将其用于以%开头的变量,则会得到Potential difficulties:␤ Useless use of hash composer on right side of hash assignment; did you mean := instead?错误。正如该错误所指示的那样,只要使用绑定,我们就可以使用花括号。

my %h := { => 'b'=> 'd'=> 'f'};
say %h# OUTPUT: «{a => b, c => d, e => f}␤»

嵌套哈希也可以使用相同的语法定义。

my %h =  => => 'g';
say %h<e><f># OUTPUT: «g␤»

但是,您在这里定义的是一个指向Pair的键,如果这是您想要的,并且您的嵌套哈希只有一个键,那么这样做是可以的。但是%h<e>将指向一个Pair,这将带来以下后果。

my %h =  => => 'g';
%h<e><q> = 'k';
# OUTPUT: «Pair␤Cannot modify an immutable Str (Nil)␤  in block <unit>» 

但是,这将有效地定义一个嵌套哈希。

my %h =  => { => 'g' };
say %h<e>.^name;  # OUTPUT: «Hash␤» 
say %h<e><f>;     # OUTPUT: «g␤»

如果在需要值的 地方遇到了Pair,则将其用作哈希值。

my %h = 'a''b' => 'c';
say %h<a>.^name;            # OUTPUT: «Pair␤» 
say %h<a>.key;              # OUTPUT: «b␤»

如果同一个键出现多次,则存储与最后一次出现关联的值。

my %h = => 1=> 2;
say %h<a>;                  # OUTPUT: «2␤»

要将哈希赋值给没有%符号的变量,可以使用%()哈希构造函数。

my $h = %=> 1=> 2 );
say $h.^name;               # OUTPUT: «Hash␤» 
say $h<a>;                  # OUTPUT: «1␤»

如果一个或多个值引用主题变量$_,则赋值的右侧将被解释为Block,而不是哈希。

my @people = [
    %id => "1A"firstName => "Andy"lastName => "Adams" ),
    %id => "2B"firstName => "Beth"lastName => "Burke" ),
    # ... 
];
 
sub lookup-user (Hash $h{ #`(Do something...) $h }
 
my @names = map {
    # While this creates a hash: 
    my  $query = { name => "$person<firstName> $person<lastName>" };
    say $query.^name;      # OUTPUT: «Hash␤» 
 
    # Doing this will create a Block. Oh no! 
    my  $query2 = { name => "$_<firstName> $_<lastName>" };
    say $query2.^name;       # OUTPUT: «Block␤» 
    say $query2<name>;       # fails 
 
    CATCH { default { put .^name''.Str } };
    # OUTPUT: «X::AdHoc: Type Block does not support associative indexing.␤» 
    lookup-user($query);
    # Type check failed in binding $h; expected Hash but got Block 
}@people;

如果您使用%()哈希构造函数,就可以避免这种情况。只使用花括号来创建块。

哈希切片§

您可以使用切片同时为多个键赋值。

my %h%h<a b c> = 2 xx *%h.raku.say;  # OUTPUT: «{:a(2), :b(2), :c(2)}␤» 
my %h%h<a b c> = ^3;     %h.raku.say;  # OUTPUT: «{:a(0), :b(1), :c(2)}␤»

非字符串键(对象哈希)§

默认情况下,{ }中的键会被强制转换为字符串。要使用非字符串键来组合哈希,请使用冒号前缀。

my $when = :{ (now=> "Instant", (DateTime.now=> "DateTime" };

请注意,对于使用对象作为键的情况,您通常无法使用<...>结构来查找键,因为它只创建字符串和同形词。请使用{...}代替。

:{  0  => 42 }<0>.say;   # Int    as key, IntStr in lookup; OUTPUT: «(Any)␤» 
:{  0  => 42 }{0}.say;   # Int    as key, Int    in lookup; OUTPUT: «42␤» 
:{ '0' => 42 }<0>.say;   # Str    as key, IntStr in lookup; OUTPUT: «(Any)␤» 
:{ '0' => 42 }{'0'}.say# Str    as key, Str    in lookup; OUTPUT: «42␤» 
:{ <0> => 42 }<0>.say;   # IntStr as key, IntStr in lookup; OUTPUT: «42␤»

注意:Rakudo 实现目前错误地将相同的规则应用于:{ }{ },并且在某些情况下可以构造Block。为了避免这种情况,您可以直接实例化参数化的哈希。%符号变量的参数化也受支持。

my Num %foo1      = "0" => 0e0# Str keys and Num values 
my     %foo2{Int} =  0  => "x"# Int keys and Any values 
my Num %foo3{Int} =  0  => 0e0# Int keys and Num values 
Hash[Num,Int].new: 00e0;      # Int keys and Num values

现在,如果您想定义一个哈希来保留您用作键的对象作为您提供给哈希用作键的确切对象,那么对象哈希就是您要找的。

my %intervals{Instant};
my $first-instant = now;
%intervals{ $first-instant } = "Our first milestone.";
sleep 1;
my $second-instant = now;
%intervals{ $second-instant } = "Logging this Instant for spurious raisins.";
for %intervals.sort -> (:$key:$value{
    state $last-instant //= $key;
    say "We noted '$value' at $key, with an interval of {$key - $last-instant}";
    $last-instant = $key;
}

此示例使用一个只接受Instant类型键的对象哈希来实现一个基本但类型安全的日志记录机制。我们使用一个命名的state变量来跟踪上一个Instant,以便我们可以提供一个间隔。

对象哈希的全部意义在于将键保留为对象本身。目前,对象哈希使用对象的WHICH方法,该方法为每个可变对象返回一个唯一的标识符。这是对象标识运算符 (===) 所依赖的基石。顺序和容器在这里非常重要,因为.keys的顺序是未定义的,一个匿名列表永远不会与另一个匿名列表===

my %intervals{Instant};
my $first-instant = now;
%intervals{ $first-instant } = "Our first milestone.";
sleep 1;
my $second-instant = now;
%intervals{ $second-instant } = "Logging this Instant for spurious raisins.";
say ($first-instant$second-instant~~ %intervals.keys;       # OUTPUT: «False␤» 
say ($first-instant$second-instant~~ %intervals.keys.sort;  # OUTPUT: «True␤» 
say ($first-instant$second-instant=== %intervals.keys.sort# OUTPUT: «False␤» 
say $first-instant === %intervals.keys.sort[0];                 # OUTPUT: «True␤» 

由于Instant定义了自己的比较方法,因此在我们的示例中,根据cmp进行排序将始终将最早的瞬时对象作为它返回的List中的第一个元素。

如果您想在哈希中接受任何对象,可以使用Any

my %h{Any};
%h{(now)} = "This is an Instant";
%h{(DateTime.now)} = "This is a DateTime, which is not an Instant";
%h{"completely different"} = "Monty Python references are neither DateTimes nor Instants";

有一种更简洁的语法使用绑定。

my %h := :{ (now=> "Instant", (DateTime.now=> "DateTime" };

绑定是必要的,因为对象哈希是关于非常坚固、特定对象的,而绑定非常擅长跟踪这些对象,而赋值并不太关心这些对象。

自从 6.d 发布以来,Junction 也可作为哈希键使用。结果也将是与用作键的相同类型的Junction

my %hash = %=> 1=> 2c=> 3);
say %hash{"a"|"c"};   # OUTPUT: «any(1, 3)␤» 
say %hash{"b"^"c"};   # OUTPUT: «one(2, 3)␤» 
say %hash{"a" & "c"}# OUTPUT: «all(1, 3)␤»

如果使用任何类型的连接来定义键,它将具有将 Junction 的元素定义为单独键的相同效果。

my %hash = %"a"|"b" => 1=> 2 );
say %hash{"b"|"c"};       # OUTPUT: «any(1, 2)␤» 

约束值类型§

在声明符和名称之间放置一个类型对象,以约束 Hash 中所有值的类型。

my Int %h;
put %h<Goku>   = 900;
 
try {
    %h<Vegeta> = "string";
    CATCH { when X::TypeCheck::Binding { .message.put } }
}
 
# OUTPUT: 
# 9001 
# Type check failed in assignment to %h; expected Int but got Str ("string")

您可以通过更易读的语法来做到这一点。

my %h of Int# the same as my Int %h

如果要约束 Hash 中所有键的类型,请在变量名称后添加 {Type}

my %h{Int};

甚至将这两个约束放在一起。

my %h{Int} of Int;
put %h{21} = 42;
 
try {
    %h{0} = "String";
    CATCH { when X::TypeCheck::Binding { .message.put } }
}
 
try {
    %h<string> = 42;
    CATCH { when X::TypeCheck::Binding { .message.put } }
}
 
try {
    %h<string> = "String";
    CATCH { when X::TypeCheck::Binding { .message.put } }
}
 
# OUTPUT: 
# 42 
# Type check failed in binding to parameter 'assignval'; expected Int but got Str ("String") 
# Type check failed in binding to parameter 'key'; expected Int but got Str ("string") 
# Type check failed in binding to parameter 'key'; expected Int but got Str ("string")

遍历哈希键和值§

处理哈希中元素的常用习惯用法是遍历键和值,例如,

my %vowels = 'a' => 1'e' => 2'i' => 3'o' => 4'u' => 5;
for %vowels.kv -> $vowel$index {
  "$vowel$index".say;
}

给出类似于此的输出

a: 1
e: 2
o: 4
u: 5
i: 3

我们使用 kv 方法从哈希中提取键及其相应的值,以便我们可以将这些值传递到循环中。

请注意,打印的键和值的顺序不可靠;哈希的元素在同一程序的不同运行中并不总是以相同的方式存储在内存中。事实上,从 2018.05 版本开始,保证每次调用时的顺序都不同。有时人们希望按顺序处理元素,例如,哈希的键。如果要按字母顺序打印元音列表,则应编写

my %vowels = 'a' => 1'e' => 2'i' => 3'o' => 4'u' => 5;
for %vowels.sort(*.key)>>.kv -> ($vowel$index{
  "$vowel$index".say;
}

它打印

a: 1
e: 2
i: 3
o: 4
u: 5

按字母顺序排列,如预期的那样。为了实现此结果,我们按键对元音哈希进行了排序 (%vowels.sort(*.key)),然后通过对每个元素应用 .kv 方法(通过一元 >> 超运算符)来请求其键和值,从而得到一个 List 的键/值列表。要提取键/值,因此需要将变量括在括号中。

另一种解决方案是展平结果列表。然后,可以像使用普通 .kv 一样访问键/值对。

my %vowels = 'a' => 1'e' => 2'i' => 3'o' => 4'u' => 5;
for %vowels.sort(*.key)>>.kv.flat -> $vowel$index {
  "$vowel$index".say;
}

您还可以使用 解构 遍历 Hash

就地编辑值§

有时您可能希望在迭代哈希时修改其值。

my %answers = illuminatus => 23hitchhikers => 42;
# OUTPUT: «hitchhikers => 42, illuminatus => 23» 
for %answers.values -> $v { $v += 10 }# Fails 
CATCH { default { put .^name''.Str } };
# OUTPUT: «X::AdHoc: Cannot assign to a readonly variable or a value␤»

这通常通过以下方式完成,即发送键和值。

my %answers = illuminatus => 23hitchhikers => 42;
for %answers.kv -> $k,$v { %answers{$k} = $v + 10 };

但是,可以利用块的签名来指定您希望对值进行读写访问。

my %answers = illuminatus => 23hitchhikers => 42;
for %answers.values -> $v is rw { $v += 10 };

即使在对象哈希的情况下,也不可能直接对哈希键进行就地编辑;但是,可以删除一个键并添加一个新的键/值对以实现相同的结果。例如,给定此哈希

my %h = => 1=> 2;
for %h.keys.sort -> $k {
    # use sort to ease output comparisons 
    print "$k => {%h{$k}}";
}
say ''# OUTPUT: «a => 1; b => 2; ␤» 

将键 'b' 替换为 'bb',但保留 'b' 的值作为新键的值。

for %h.keys -> $k {
    if $k eq 'b' {
        %h<bb> = %h{$k}:delete;
    }
}
for %h.keys.sort -> $k {
    print "$k => {%h{$k}}";
}
say ''# OUTPUT: «a => 1; bb => 2; ␤»