1

我需要将数据结构从数组列表转换为树状结构。在开始处理数据之前,我知道树的深度,但我想保持灵活性,以便我可以重用代码。

所以我想到了动态生成子引用(从基于 Moose 的模块中)从数组到树的想法。像这样(以简化的方式):

use Data::Dump qw/dump/;

sub create_tree_builder {
     my $depth = shift;
     return eval join '', 'sub { $_[0]->{$_[', 
                           join(']}->{$_[', (1..$depth)),
                          ']} = $_[',  $depth + 1 , '] }'; 
}


my $s = create_tree_builder(5);
my $tree = {};

$s->($tree, qw/one two three four five/, 'a value');

print dump $tree;

# prints
#  {
#     one => { two => { three => { four => { five => "a value" } } } },
#  }

这为我打开了世界,我发现这个 eval-in 过程很酷的用途——将参数化生成的字符串放入一个函数中(显然,这是一个寻找问题的解决方案)。

然而,感觉有点好得令人难以置信,几乎。

这种做法有什么建议吗?或者改进建议?

我可以清楚地看到,评估任意输入可能不是最安全的事情,但还有什么呢?

跟进

感谢所有的答案。我使用了 amon 的代码并进行了一些基准测试,如下所示:

use Benchmark qw(:all) ;

$\ = "\n";

sub create_tree_builder {
 my $depth = shift;
 return eval join '', 'sub { $_[0]->{$_[', 
               join(']}->{$_[', (1..$depth)),
              ']} = $_[',  $depth + 1 , '] }'; 
}


my $s = create_tree_builder(5);

$t = sub {
$_[0] //= {};

    my ($tree, @keys) = @_;
    my $value = pop @keys;

    $tree = $tree->{shift @keys} //= {} while @keys > 1;
    $tree->{$keys[0]} = $value;
};


cmpthese(900000, {
        'eval'  => sub { $s->($tree, qw/one two three four five/, 'a value') },
    'build' => sub { $t->($tree, qw/one two three four five/, 'a value') },

});

结果显然有利于构建树,而不是评估工厂:

            Rate build  eval
build  326087/s    --  -79%
eval  1525424/s  368%    -- 

我承认我以前可以这样做。我将尝试使用更多随机树(而不是一遍又一遍地分配相同的元素),但我认为结果应该不同。

非常感谢您的帮助。

4

4 回答 4

5

编写一个通用的子程序来构建这样一个嵌套的哈希是非常容易的。这种方式比编写一个为特定数量的哈希级别生成这样一个子例程的工厂要简单得多。

use strict;
use warnings;

sub tree_assign {

  # Create an empty tree if one was not given, using an alias to the original argument
  $_[0] //= {};

  my ($tree, @keys) = @_;
  my $value = pop @keys;

  $tree = $tree->{shift @keys} //= {} while @keys > 1;
  $tree->{$keys[0]} = $value;
}

tree_assign(my $tree, qw/one two three four five/, 'a value');

use Data::Dump;
dd $tree;

输出

{
  one => { two => { three => { four => { five => "a value" } } } },
}
于 2013-01-09T23:39:45.510 回答
3

为什么这可能是个坏主意

  1. 可维护性。

    eval 的代码必须首先在程序员的头脑中进行 eval —— 这并不总是一件容易的事。本质上,评估是混淆。

  2. 速度。

    eval在正常执行恢复之前重新运行 perl 解析器和编译器。但是,可以使用相同的技术通过推迟子程序的编译直到需要它们来获得启动时间。这不是这样的情况。

  3. 有不止一种方法可以做到这一点。

    我喜欢匿名子例程,但您不必使用 aneval来构造它们。无论如何,它们都是关闭的。就像是

    ...;
    return sub {
      my ($tree, $keys, $value) = @_;
      $#$keys >= $depth or die "need moar keys";
      $tree = $tree->{$keys->[$_]} for 0 .. $depth - 1;
      $tree->{$keys->[$depth]} = $value;
    };
    

    $s->($tree, [qw(one two three four five)], "a value");
    

    会做一些惊人的相似的事情。(实际上,使用$depthnow 看起来像一个设计错误;完整的路径已经由键指定。因此,创建一个普通的命名子程序可能是最好的。)

于 2013-01-09T23:32:25.843 回答
2

根据他们的评论了解 OP 做得更好,并反复研究 Borodin 的代码,我建议更改界面。与其编写一个子程序来在树的深处应用一个值,我会编写一个子程序来创建一个空的子树,然后在那个子树上工作。这使您可以在子树上有效地工作,而不必在每个操作中遍历树。

package Root;

use Mouse;

has root =>
  is    => 'ro',
  isa   => 'HashRef',
  default => sub { {} };

sub init_subtree {
    my $self = shift;
    my $tree = $self->root;

    for my $key (@_) {
        $tree = $tree->{$key} //= {};
    }

    return $tree;
}

my $root = Root->new;
my $subtree = $root->init_subtree(qw/one two three four/);

# Now you can quickly work with the subtree without having
# to walk down every time.  This loop's performance is only
# dependent on the number of keys you're adding, rather than
# the number of keys TIMES the depth of the subtree.
my $val = 0;
for my $key ("a".."c") {
    $subtree->{$key} = $val++;
}

use Data::Dump;
dd $root;
于 2013-01-10T00:12:37.470 回答
2

Data::Diver是你的朋友:

use Data::Diver 'DiveVal', 'DiveRef';
my $tree = {};

DiveVal( $tree, qw/one two three four five/ ) = 'a value';

# or if you hate lvalue subroutines:
${ DiveRef( $tree, qw/one two three four five/ ) } = 'a value';

use Data::Dump 'dump';
print dump $tree;
于 2013-01-10T00:54:23.170 回答