我有一个带有一些值的哈希,这些值不是标量数据,而是返回标量数据的匿名子例程。我想使它对于在哈希中查找值的代码部分完全透明,因此不必知道某些哈希值可能是返回标量数据的匿名子例程,而不仅仅是原来的标量数据。

为此,有什么方法可以在访问键时使用匿名子例程,而无需使用任何特殊语法?这是一个简化的示例,说明了目标和问题:

#!/usr/bin/perl

my %hash = (
    key1 => "value1",
    key2 => sub {
        return "value2"; # In the real code, this value can differ
    },
);

foreach my $key (sort keys %hash) {
    print $hash{$key} . "\n";
}


我想要的输出是:

perl ./test.pl
value1
value2

<而是我得到的:

perl ./test.pl
value1
CODE(0x7fb30282cfe0)

#1 楼

有一个称为“魔术”的功能,该功能允许在访问变量时调用代码。

在变量中添加魔术极大地减慢了对该变量的访问,但是某些变量比其他变量更昂贵。 >

无需访问散列的每个元素都具有魔术性,只需访问一些值即可。

tie是一种更昂贵的魔术形式,在这里不需要。

因此,最有效的解决方案如下:

use Time::HiRes     qw( time );
use Variable::Magic qw( cast wizard );

{
   my $wiz = wizard(
      data => sub { my $code = $_[1]; $code },
      get => sub { ${ $_[0] } = $_[1]->(); },
   );

   sub make_evaluator { cast($_[0], $wiz, $_[1]) }
}

my %hash;
$hash{key1} = 'value1';
make_evaluator($hash{key2}, sub { 'value2@'.time });

print("$hash{$_}\n") for qw( key1 key2 key2 );


输出:

value1
value2@1462548850.76715
value2@1462548850.76721



其他示例:

my %hash; make_evaluator($hash{key}, sub { ... });
my $hash; make_evaluator($hash->{$key}, sub { ... });

my $x; make_evaluator($x, sub { ... });
make_evaluator(my $x, sub { ... });

make_evaluator(..., sub { ... });
make_evaluator(..., \&some_sub);


您还可以“修复”现有哈希。在您的哈希哈希方案中,

my $hoh = {
   { 
      key1 => 'value1',
      key2 => sub { ... },
      ...
   },
   ...
);

for my $h (values(%$hoh)) {
   for my $v (values(%$h)) {
      if (ref($v) eq 'CODE') {
         make_evaluator($v, $v);
      }
   }
}


评论


嗯,假设我能弄清楚如何实现,这实际上看起来很完美……

– iLikeDirt
16年5月6日在16:31

更新了答案,以显示如何“修复”哈希值。

–池上
16年5月6日17:00

这就是我最终所做的,并且效果很好。非常感谢!

– iLikeDirt
16年5月7日在19:34

#2 楼

正如Oleg所指出的,可以使用或多或少的各种奥秘技巧来做到这一点,例如tie,重载或魔术变量。但是,这既不必要,也将变得毫无意义。绝妙的技巧是,至少在99%的时间里在真实代码中使用它们都是错误的。

在实践中,最简单,最简洁的解决方案可能是编写一个使用以下代码的帮助程序子程序:标量,如果它是代码引用,则执行它并返回结果:

sub evaluate {
    my $val = shift;
    return $val->() if ref($val) eq 'CODE';
    return $val;  # otherwise
}


并像这样使用它:

foreach my $key (sort keys %hash) {
    print evaluate($hash{$key}) . "\n";
}


评论


我建议通过对象执行类似的操作也可能会起作用,而不像绑定那样有害。

–Sobrique
16年5月6日在8:10

“而不是像捆绑一样邪恶”这种对捆绑的反感是什么? “通过对象做类似的事情”将导致更多的代码。我唯一反对的是,代码比普通的Perl访问变量要慢得多,因为它是用Perl而不是C编写的。对于任何庞大的数据结构来说,这都很好

– Borodin
16年5月6日在9:28

“不必要地复杂且毫无意义地混淆”请参阅我的解决方案。额外的十五行代码?在单独的模块文件中?其中一半用于调用代码中的一些可爱的初始化语法。 “这些技巧真是太酷了,至少在99%的时间里在实际代码中使用它们将是一个错误”,我认为这是不必要的贬低和令人怀疑的答案。您显然不熟悉领带

– Borodin
16年5月6日在9:46

我不喜欢领带的“远距离动作”元素-就像重载一样,如果做得好,它很有用,如果做得不好,则是通往真正可怕代码的道路。

–Sobrique
16年5月6日在10:12

@Sobrique:很好,但是引用“远距离行动”比分配“邪恶”标签有用得多

– Borodin
16年5月6日在10:44

#3 楼

我不认为其他人在不赞成tie机制的情况下写的话是正确的。没有一个作者似乎正确地理解了它的工作原理以及可用的核心库备份

这是一个基于tieTie::StdHash示例,如果您将哈希值绑定到Tie::StdHash类,那么它就可以像普通哈希一样工作这意味着除了您可能要覆盖的方法之外,没有什么可写的了。在这种情况下,我已经覆盖了TIEHASH,以便可以在与tie命令相同的语句中指定初始化列表,并且FETCH,它将调用超类的FETCH,然后在它恰好是子例程引用时对其进行调用

您绑定的哈希将正常运行,除了您要求的更改。我希望很明显,如果您已经将子例程引用存储为哈希值,则不再有直接的方法来检索它。这样的值将始终被不带任何参数的调用结果替换。

SpecialHash.pm

package SpecialHash;

use Tie::Hash;
use base 'Tie::StdHash';

sub TIEHASH {
    my $class = shift;
    bless { @_ }, $class;
}

sub FETCH {
    my $self = shift;
    my $val = $self->SUPER::FETCH(@_);
    ref $val eq 'CODE' ? $val->() : $val;
}

1;


main.pl

use strict;
use warnings 'all';

use SpecialHash;

tie my %hash, SpecialHash => (
    key1 => "value1",
    key2 => sub {
        return "value2"; # In the real code, this value can differ
    },
);

print "$hash{$_}\n" for sort keys %hash;


输出

value1
value2





更新

听起来您的实际情况是存在一个看起来像这样的现有哈希值

my %hash = (
    a => {
        key_a1 => 'value_a1',
        key_a2 => sub { 'value_a2' },
    },
    b => {
        key_b1 => sub { 'value_b1' },
        key_b2 => 'value_b2',
    },
);

在已填充的变量上使用tie并不像绑定那样整洁然后在声明时,然后插入值,因为必须将数据复制到绑定对象。但是,我在TIEHASH类中编写SpecialHash方法的方式使在tie语句中做到这一点变得很简单。并将其与主要哈希值

该程序将恰好是哈希值引用的tie的每个值联系在一起。其核心是语句

tie %$val, SpecialHash => ( %$val )


,其功能与
相同
在先前的代码中
tie my %hash, SpecialHash => ( ... )


,但取消引用%hash以使语法有效,并且还使用哈希的当前内容作为绑定哈希的初始化数据。这就是复制数据的方式

之后,只有几个嵌套循环将整个$val转储以验证关系是否正常

use strict;
use warnings 'all';
use SpecialHash;

my %hash = (
    a => {
        key_a1 => 'value_a1',
        key_a2 => sub { 'value_a2' },
    },
    b => {
        key_b1 => sub { 'value_b1' },
        key_b2 => 'value_b2',
    },
);

# Tie all the secondary hashes that are hash references
#
for my $val ( values %hash ) {
    tie %$val, SpecialHash => ( %$val ) if ref $val eq 'HASH';
}

# Dump all the elements of the second-level hashes
#
for my $k ( sort keys %hash ) {

    my $v = $hash{$k};
    next unless ref $v eq 'HASH';

    print "$k =>\n";

    for my $kk ( sort keys %$v ) {
        my $vv = $v->{$kk};
        print "    $kk => $v->{$kk}\n" 
    }
}


输出

a =>
    key_a1 => value_a1
    key_a2 => value_a2
b =>
    key_b1 => value_b1
    key_b2 => value_b2


评论


这基本上正是我要的!

– iLikeDirt
16年5月6日在13:58

好吧...下一个问题。当它实际上不是哈希,而是我初始化的hashref时,该怎么办?

– iLikeDirt
16年5月6日在14:53

@iLikeDirt,没有变化。只需像以前一样绑定引用的哈希即可。

–池上
16年5月6日在15:18

基本上,有什么方法可以初始化匿名SpecialHash引用?在生产中,我拥有的是二维数据结构,它是匿名哈希引用的匿名哈希引用,我希望它们全部都是SpecialHashes。

– iLikeDirt
16年5月6日15:49



@iLikeDirt,您可以使用领带%$ _,SpecialHash ::,%$ _作为值(%$ HoH);来绑定每个第二级哈希。更灵活的解决方案要复杂得多。

–池上
16年5月6日在17:01

#4 楼

是的你可以。您可以tie哈希到将代码引用解析为其返回值的实现,也可以将带祝福的标量与overload ed方法一起使用,以进行字符串化,数字化以及您要自动解析的任何其他上下文。

评论


Variable :: Magic也会以类似的方式起作用,不仅更复杂,而且速度更快。

–霍布斯
16年5月5日在23:10

#5 楼

对于这种用例,perl的特殊功能之一就是tie。这使您可以将面向对象的样式方法附加到标量或哈希。

应该谨慎使用它,因为这可能意味着您的代码以意想不到的方式在做真正奇怪的事情。

但例如:

#!/usr/bin/env perl

package RandomScalar;

my $random_range = 10;

sub TIESCALAR {
    my ( $class, $range ) = @_;
    my $value = 0;
    bless $value, $class;
}

sub FETCH {
    my ($self) = @_;
    return rand($random_range);
}

sub STORE {
    my ( $self, $range ) = @_;
    $random_range = $range;
}

package main;

use strict;
use warnings;

tie my $random_var, 'RandomScalar', 5;

for ( 1 .. 10 ) {
    print $random_var, "\n";
}

$random_var = 100;
for ( 1 .. 10 ) {
    print $random_var, "\n";
}


如您所见-这使您可以采用“普通”标量,并通过它。您可以对hash使用非常类似的机制-例如,可能要进行数据库查找。

但是,您也需要非常谨慎-因为这样做是在远距离创建动作的。未来的维护程序员可能不会希望$random_var每次运行时都实际更改,并且值分配未实际“设置”。

它对于例如测试,这就是为什么我给出一个例子。

在您的示例中-您可能会“绑定”哈希:

#!/usr/bin/env perl

package MagicHash;

sub TIEHASH {
    my ($class) = @_;
    my $self = {};
    return bless $self, $class;
}

sub FETCH {
    my ( $self, $key ) = @_;
    if ( ref( $self->{$key} ) eq 'CODE' ) {
        return $self->{$key}->();
    }
    else {
        return $self->{$key};
    }
}

sub STORE {
    my ( $self, $key, $value ) = @_;
    $self->{$key} = $value;
}

sub CLEAR {
    my ($self) = @_;
    $self = {};
}

sub FIRSTKEY {
    my ($self) = @_;
    my $null = keys %$self;    #reset iterator
    return each %$self;
}

sub NEXTKEY {
    my ($self) = @_;
    return each %$self;
}

package main;

use strict;
use warnings;
use Data::Dumper;

tie my %magic_hash, 'MagicHash';
%magic_hash = (
    key1 => 2,
    key2 => sub { return "beefcake" },
);

$magic_hash{random} = sub { return rand 10 };

foreach my $key ( keys %magic_hash ) {
    print "$key => $magic_hash{$key}\n";
}
foreach my $key ( keys %magic_hash ) {
    print "$key => $magic_hash{$key}\n";
}
foreach my $key ( keys %magic_hash ) {
    print "$key => $magic_hash{$key}\n";
}


此它的邪恶程度要小一些,因为将来的维护程序员可以正常使用您的“哈希”。但是动态评估可以使您的脚步放松,因此-仍然请谨慎。

另外一种选择是将其“面向对象”进行操作-创建一个“存储对象”,其基本原理如下:上面-仅创建对象,而不使用tie。对于长期使用,这应该更加清楚,因为您不会得到意想不到的行为。 (这是一个在做魔术的对象,这很正常,而不是“有趣的哈希”)。

评论


看一下Tie :: Hash和类似的模块。有很多库支持,除非您要更改它们,否则通常无需重写所有访问方法。看看我自己的解决方案

– Borodin
16年5月6日在9:59

#6 楼

您需要确定何时存在代码引用,然后将其作为实际调用执行:
真正的调度表),而不是有一些返回非代码引用,而有一些返回非引用。

但是,如果您这样定义哈希,那么在涉及哈希时就不必做任何特殊的欺骗了是时候使用哈希了。查找键时,它将调用sub并直接返回值。

foreach my $key (sort keys %hash) {
    if (ref $hash{$key} eq 'CODE'){
        print $hash{$key}->() . "\n";
    }
    else {
        print "$hash{$key}\n";
    }
}


评论


“它会调用sub并在查找键时直接返回值。”也许这就是您的意思,但是在哈希被初始化时,sub仅被调用一次。如果OP的潜艇有副作用,这可能行不通(但如果没有副作用,显然效率会更高)。

–ThisSuitIsBlackNot
16年5月6日15:57



#7 楼

不,不是没有一些辅助代码。您要求一个简单的标量值和一个代码引用以相同的方式运行。可以做到这一点的代码远非简单,而且还会在哈希及其使用之间注入复杂性。您可能会发现以下方法更简单,更干净。

您可以使所有值都成为代码引用,使哈希成为调度表,以进行统一调用

my %hash = (
    key1 => sub { return "value1" },
    key2 => sub {
        # carry on some processing ...
        return "value2"; # In the real code, this value can differ
    },
);

print $hash{$_}->() . "\n" for sort keys %hash;


但当然,这种方法的开销很小。

评论


并非完全正确。有一些方法可以完成。他们是否是一个好主意是另一回事...

–Sobrique
16年5月6日在9:27

@Sobrique是的,谢谢您的评论。我本来想通过“不,不像这样”来防范这一点……但这根本不够好。我认为OP是直接简单的事情,而不是在哈希和使用之间注入代码层和复杂性。 (此外,我也不知道该怎么做,并且会很高兴地阅读您和Borodin的答案!)鉴于此问题的去向,我可能会删除此帖子。

–zdim
16年5月6日在9:35



我是否可以建议您改写为“可能,但是它需要复杂且晦涩的代码。使用以下方法可能会更简单,更简洁...”

–池上
16年5月6日16:50



@ikegami感谢您的评论。鉴于其他答案,我不确定该帖子是否在此页面上有位置。但我想这是一种简单的方法。

–zdim
16年5月6日在18:01

@zdim:嘿,这是你的问题。您可以随心所欲地做任何事情!我进行编辑是为了帮助您更清楚地表达“不喜欢这样”。您随时可以回滚其他人对您的帖子所做的任何更改,并且您当然无需在更改之前先与其他任何人确认

– Borodin
16年5月7日在14:51