散列和映射

关联角色和关联类

关联角色是 Hash 和 Map 以及 MixHash 等其他类的基础。它定义了将在关联类中使用的两种类型; 默认情况下,您可以使用任何内容(字面意思,因为任何 Any 子类的类都可以使用)作为键keyof 方法。

默认情况下,使用 % sigil 声明的任何对象都将获得 Associative 角色,默认情况下将表现为散列,但此角色仅提供上述两种方法,以及默认的 Hash 行为。

say (%).^name ; # 输出 Hash

相反,如果未混入 Associative 角色,则不能使用 % sigil,但由于此角色没有任何关联属性,因此你必须重新定义散列下标操作符的行为。为此,你必须重写几个函数:

class Logger does Associative[Cool,DateTime] {
    has %.store;
 
    method log( Cool $event ) {
        %.store{ DateTime.new( now ) } = $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» 

在这里,我们定义了一个具有关联语义的 logger,它可以使用日期(或其中一部分)作为键。由于我们将参数化 Associative 为那些特定类,of 将返回我们使用的值类型,在这里为 Cool(我们只能记录列表或字符串)。混合 Associative 角色赋予其使用 % sigil 的权利; 因为 %-sigilled 变量默认获得 Hash 类型,所以在定义中需要绑定。

此 log 将仅附加,这就是为什么我们转义关联数组隐喻以使用 log 方法向日志添加新事件。但是,一旦添加它们,我们就可以按日期检索它们或检查它们是否存在。对于第一个,我们必须重写 AT-KEY multi 方法,对于后者 EXIST-KEY。在最后两个语句中,我们展示了下标操作如何调用 AT-KEY,而 :exists 副词调用 EXISTS-KEY

我们重写 DELETE-KEYASSIGN-KEYBIND-KEY,但只抛出异常。尝试赋值,删除或绑定值到键上将导致 Cannot modify an immutable Str (value) 异常抛出。

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

可变哈希和不可变映射

Hash 是从键到值的可变映射(在其他编程语言中称为字典,哈希表或映射)。这些值都是标量容器,这意味着你可以给它们赋值。另一方面,Maps是不可变的。键与值配对后,此配对无法更改。

Maps 和 hashes 通常存储在百分号 % 变量中,用于表示它们是关联的(Associative)。

通过 {} postcircumfix 运算符使用键访问 Hash 和 map 元素:

say %*ENV{'HOME', 'PATH'}.perl;
# 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';

Hash 赋值

将一个元素列表赋值给一个哈希变量首先清空该变量,然后迭代右侧的元素。如果元素是 Pair,则将其键作为新的哈希键,并将其值作为该键的新哈希值。否则,该值被强制转换为 Str 并用作散列键,而列表的下一个元素则被视为相应的值。

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

等价于

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

或者

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

甚至

my %h = %( a => 'b', c => 'd', e => 'f')

或者

my $h = { a => 'b', c => 'd', e => 'f'};

请注意,花括号仅在我们未将其分配给 %-sigilled 变量的情况下使用;如果我们将它用于 %-sigilled 变量,我们将遇到 Potential difficulties:␤ Useless use of hash composer on right side of hash assignment; did you mean := instead? 的错误。但是,正如此错误所示,只要我们使用绑定,我们就可以使用花括号:

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

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

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

但是,你在这里定义的是一个指向 Pair 的键,如果你想要的话,这很好,如果你的嵌套哈希有一个键。但是 %h <e> 将指向 Pair 会产生这些后果:

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

但是,这将有效地定义嵌套哈希:

my %h =  e => { f => '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 = a => 1, a => 2;
say %h<a>;                  # OUTPUT: «2␤» 

要将哈希值分配给不具有%sigil的变量,可以使用%()哈希构造函数:

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

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

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;

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

Hash 切片

您可以使用切片同时分配多个键。

my %h; %h<a b c> = 2 xx *; %h.perl.say;  # OUTPUT: «{:a(2), :b(2), :c(2)}␤» 
my %h; %h<a b c> = ^3;     %h.perl.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实现目前错误地对{}应用与{}相同的规则,并且可以在某些情况下构造块。为避免这种情况,您可以直接实例化参数化哈希。还支持%-sigiled变量的参数化:

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: 0, 0e0;      # 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类型的键的对象哈希来实现基本但类型安全的日志记录机制。我们利用一个命名的状态变量来跟踪前一个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: «False␤» 
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" };

绑定是必要的,因为对象哈希是关于非常可靠的特定对象,这是绑定在跟踪哪些任务并不关心哪些很好的事情。

约束值类型

在声明符和名称之间放置一个类型对象,以约束哈希值的所有值的类型。使用具有where子句的约束的子集。

subset Powerful of Int where * > 9000;
my Powerful %h{Str};
put %h<Goku>   = 9001;
try {
    %h<Vegeta> = 900;
    CATCH { when X::TypeCheck::Binding { .message.put } }
}
 
# OUTPUT: 
# 9001 
# Type check failed in binding assignval; expected Powerful but got Int (900) 

循环哈希键和值

处理散列中元素的常用习惯是循环键和值,例如,

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方法应用于每个元素,通过一元» superroperator生成元数据和值。键/值列表的列表。为了提取键/值,变量因此需要包含在括号中。

另一种解决方案是展平结果列表。然后可以使用与.kv相同的方式访问键/值对:

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

您还可以使用解构来循环哈希。

在位编辑值

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

my %answers = illuminatus => 23, hitchhikers => 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 => 23, hitchhikers => 42;
for %answers.kv -> $k,$v { %answers{$k} = $v + 10 };

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

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

但是,即使在对象哈希的情况下,也不可能进行哈希键的就地编辑。