【问题标题】:how to replace values in an array of hashes properly in Perl?如何在 Perl 中正确替换散列数组中的值?
【发布时间】:2021-11-23 11:31:01
【问题描述】:

如下所示,我有一个 foreach 循环,其中一个散列数组中的值被另一个散列数组中的值替换。

第二个foreach循环只是打印并测试值是否正确分配。

foreach my $row (0 .. $#row_buff) {
    $row_buff[$row]{'offset'} = $vars[$row]{'expression'};
    print $row_buff[$row]{'offset'},"\n";
}

foreach (0 .. $#row_buff) {
    print $row_buff[$_]{'offset'},"\n";
}

这里@row_buff 和@vars 是两个哈希数组。它们预先填充了所有使用的键的值。

哈希被推入数组,如下所示:

推送@row_buff,\%hash;

问题: 假设第一个 foreach 打印中的打印语句是这样的:

string_a
string_b
string_c
string_d

那么第二个foreach循环中的print语句是这样的:

string_d
string_d
string_d
string_d

这让我很困惑。两个打印语句都应该以完全相同的方式打印,对吗?但是第二个打印语句打印的值只是以重复方式单独打印的最后一个值。有人可以指出我这里可能出了什么问题吗?非常感谢任何提示。这是我第一次提出问题,如有遗漏请见谅。

更新

我本来可以添加一些信息,对此大家感到抱歉。在foreach之前多了一行,是这样的:

@row_buff = (@row_buff) x $itercnt;
foreach my $row (0 .. $#row_buff) {
    $row_buff[$row]{'offset'} = $vars[$row]{'expression'};
    print $row_buff[$row]{'offset'},"\n";
}

foreach (0 .. $#row_buff) {
    print $row_buff[$_]{'offset'},"\n";
}

$itercnt 是一个整数。我用它多次复制@row_buff。

【问题讨论】:

  • 每次填充数组时,您可能会将 ref 推送到相同的哈希 (\%hash)。但正如 toolic 所说,没有看到真正的代码只是猜测。
  • 如果 Dave 的猜测正确,请使用 push @row_buff, { %hash };
  • 感谢 Dave 和 Shawn 的这些提示,他们帮我弄清楚了!

标签: arrays perl hash


【解决方案1】:

这显然与在数组上存储引用有关,而不是与独立数据有关。由于未提供详细信息,因此尚不清楚这是如何发生的,但以下讨论应该会有所帮助。

考虑这两个基本示例。

首先,在数组上放置一个哈希(引用),每次先改变一个值

use warnings;
use strict;
use feature 'say';
use Data::Dump qw(dd);
# use Storable qw(dclone);

my %h = ( a => 1, b => 2 );

my @ary_w_refs;

for my $i (1..3) {
    $h{a} = $i; 
    push @ary_w_refs, \%h;           # almost certainly WRONG

    # push @ary_w_refs, { %h };      # *copy* data
    # push @ary_w_refs, dclone \%h;  # may be necessary, or just safer
}

dd $_ for @ary_w_refs;

我使用Data::Dump 来显示复杂的数据结构,因为它的简单性和默认的紧凑输出。为此目的还有其他模块,Data::Dumper 在核心中(已安装)。

以上印刷品

{ a => 3, b => 2 } { a => 3, b => 2 } { a => 3, b => 2 }

看看我们每次在散列中更改的键 a 的值是如何变化的,因此应该为每个数组元素设置一个不同的值(12 , 3) -- 最后是一样的,和我们最后分配的一样吗? (问题中似乎就是这种情况。)

这是因为我们为每个元素的哈希 %h 分配了一个 引用,所以即使每次通过循环我们都会首先更改该键的哈希值,最终它是只是在每个元素处对相同哈希的引用。

因此,当在循环之后查询数组时,我们只能得到散列中的内容(在键 a 它是最后分配的数字,3)。数组没有自己的数据,只有一个指向哈希数据的指针。(因此哈希的数据也可以通过写入数组来更改,如下例所示。)

大多数时候,我们想要一个单独的、独立的副本。解决方案?复制数据。

天真地,而不是

push @ary_w_refs, \%h;

我们可以的

push @ary_w_refs, { %h };

这里的{} 是一个匿名哈希的构造函数, 所以里面的%h 被复制了。所以实际数据进入数组并且一切都很好?在这种情况下,是的,其中哈希值是纯字符串/数字。

但是当哈希值本身是引用时呢?然后这些引用被复制,@ary_w_refs 再次没有自己的数据!我们会遇到完全相同的问题。 (尝试上面的哈希为( a => [1..10] )

如果我们有一个复杂的数据结构,携带值的引用,我们需要一个深拷贝。一个很好的方法是使用库,Storable 和它的dclone 非常好

use Storable qw(dclone);
...

    push @ary_w_refs, dclone \%h;

现在数组元素有自己的数据,与%h无关(但在复制时相等)。

这对于简单的哈希/数组也是一件好事,以防止将来发生更改,即哈希已更改但我们忘记了它被复制的位置(或者哈希及其副本没有甚至互相认识)。

另一个例子。让我们用 hashref 填充一个数组,然后将它复制到另一个数组

use warnings;
use strict;
use feature 'say';    
use Data::Dump qw(dd pp);

my %h = ( a => 1, b => 2 );

my @ary_src = \%h;
say "Source array: ", pp \@ary_src;

my @ary_tgt = $ary_src[0];
say "Target array: ", pp \@ary_tgt;

$h{a} = 10;
say "Target array: ", pp(\@ary_tgt), " (after hash change)";

$ary_src[0]{b} = 20;
say "Target array: ", pp(\@ary_tgt), " (after hash change)";

$ary_tgt[0]{a} = 100;
dd \%h;

(为简单起见,我使用只有一个元素的数组。)

打印出来

源数组:[{ a => 1, b => 2 }] 目标数组:[{ a => 1, b => 2 }] 目标数组:[{ a => 10, b => 2 }](哈希更改后) 目标数组:[{ a => 10, b => 20 }](哈希更改后) { a => 100, b => 20 }

那个“目标”数组,据说只是从源数组中复制出来的,当远处的哈希发生变化时会发生变化!当它的源数组发生变化时。同样,这是因为对哈希的引用被复制,首先复制到一个数组,然后复制到另一个。

为了获得独立数据副本,再次复制数据,每次。我再次建议安全起见并使用Storable::dclone(当然也可以使用等效的库),即使是简单的哈希和数组。

最后,请注意最后一个有点险恶的情况——写入该数组会更改哈希!这个(第二次复制的)数组可能远离散列,在散列甚至不知道的函数(在另一个模块中)中。这种错误可能是真正隐藏的错误的根源。

现在,如果您澄清引用的复制位置,更完整(简单)地表示您的问题,我们可以提供更具体的补救措施。


使用正确且经常使用的引用的一种重要方法是,每次通过时将引用的结构声明为词法变量

for my $elem (@data) { 
    my %h = ...
    ... 
    push @results, \%h;  # all good
}

该词法 %h 每次都会重新引入,因此它在数组上的引用数据被保留,因为数组在循环之外持续存在,对于每个元素都是独立的。

这样做也更有效率,因为%h 中的数据不会像{ %h } 那样被复制,而只是“重新定位”,可以说,来自词汇@987654347 @ 在迭代结束时被销毁到数组中的引用。

如果要复制的结构自然存在于循环之外,这当然可能并不总是合适的。然后使用它的深层副本。

同样的机制在函数调用中起作用

sub some_func {
    ...
    my %h = ...
    ...
    return \%h;  # good
}

my $hashref = some_func();

同样,词法 %h 在函数返回时超出范围并且不再存在,但它携带的数据和对它的引用被保留,因为它被返回并分配,所以它的引用计数是非零。 (至少返回给调用者,也就是说;它可能已经在子执行期间被传递到其他地方,所以我们可能仍然会对使用相同引用的多个参与者造成混乱。)所以@987654350 @ 引用了在 sub 中创建的数据。

回想一下,如果一个函数被传递了一个引用,当它被调用或在其执行过程中(通过调用返回引用的其他子程序),更改并返回它,那么我们再次在某个调用者中更改了数据,可能被删除了从这部分程序流程开始。

这当然是经常发生的,因为有更大的数据池不能一直被复制,但是需要小心并组织代码(对于一个来说,尽可能模块化),以尽量减少出错的可能性。

对于引用的作用,这是“指针”一词的松散用法,但如果要引用 C,我会说它有点“打扮” C指针

在不同的上下文中它可以是一个块

【讨论】:

  • 非常感谢您提供详细而翔实的答案。我尝试使用push \@ary_w_refs, \%h;push @ary_w_refs, { %h };push \@ary_w_refs, dclone \%h;,就像你提到的那样,但这并没有解决问题,然后我意识到我有这样一行:\@row_buff = (\@row_buff) x $itercnt; 在这一行中,数组中的引用是被复制,所以我在 foreach 循环中使用您的建议替换了这一行,如下所示:
  • $size = $#row_buff; foreach (0 .. $itercnt -2){ foreach (0 .. $size){ push @row_buff, dclone \%h; } } 现在这里不是通过复制其中的哈希引用来复制数组,而是深度克隆数组的每个元素并将其推回以具有相同的复制效果但没有问题复制参考资料。这有助于解决问题。
  • @Vishnu3333 太好了,您找到并修复了它! :) 很高兴这有帮助:)。请注意,我在脚注中添加了一些关于从子返回引用的内容。如果出现问题,请告诉我。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-15
  • 1970-01-01
  • 2010-12-31
  • 2014-01-16
  • 2014-05-19
  • 2016-08-03
  • 1970-01-01
相关资源
最近更新 更多