魔法关联标量

Perl的tie机制允许我创建看起来像标量但可以做任何我想让它做的事情。我可以改变熟悉的标量接口(简单访问和赋值)的实际工作方式。我有时发现这个功能非常有用,以至于我在精通Perl中为此专门写了一章。

我想我是爱上这项技术了,因为它让我能够手动创建一些HTML,解决了看似无法解决的问题(所以你知道这一定是很久以前的事情了)。有人创建了一个库来创建HTML表格,允许我通过传递标量值给trbgcolor属性来设置表格行的颜色。幸运的是,我已经忘记了那是谁或代码看起来像什么,但大概是这个样子

sub print_table_and_stuff {
    my( $color, @lots_of_other_arguments ) = @_;

    ... lots of code ...
    print "<table>";

    foreach my $item ( @items ) {
            print qq(<tr bgcolor="$color">);
            ... fill in the cells ...
            print "</tr>";
            }

    ... lots of code ...
    }

除了代码中发生的所有愚蠢的事情,比如直接打印而不使用模板,写这个代码的人没有考虑到有人想要有交替(甚至是旋转)的行颜色。在那之前,这还不是一件事情,代码甚至在那之前。我的任务是以尽可能少的干扰来实现交替行颜色。

我可以使用我在精通Perl中展示的技术之一替换子程序,但有一个更简单的方法。如果我能让$color自己改变,我就不必修改代码。

因此,我发明了Tie::Cycletie接口允许我决定当访问或存储标量时它应该做什么。我通过定义特殊子程序为这两个操作提供代码。以下是显示这些特殊方法的代码片段

package Tie::Cycle;
use strict;

sub TIESCALAR {
        my( $class, $list_ref ) = @_;
        my $self = bless [], $class;

        unless( $self->STORE( $list_ref ) ) {
                carp "Argument must be an array reference";
                return;
                }

        return $self;
        }

sub FETCH {
        my( $self ) = @_;

        my $index = $self->[CURSOR_COL]++;
        $self->[CURSOR_COL] %= $self->_count;

        return $self->_item( $index );
        }

sub STORE {
        my( $self, $list_ref ) = @_;
        return unless ref $list_ref eq ref [];
        my @shallow_copy = map { $_ } @$list_ref;

        $self->[CURSOR_COL] = 0;
        $self->[COUNT_COL]  = scalar @shallow_copy;
        $self->[ITEM_COL]   = \@shallow_copy;
        }

tie接口包括创建tied对象的TIESCALAR方法,决定如何返回值的FETCH方法,以及决定如何存储值的STORE方法。在这种情况下,我想存储一个值数组并循环遍历它们。每次我访问标量时,Perl都会调用FETCH。每次它调用FETCH时,我都会增加一个计数器,这样我就会得到下一个值。当我到达数组末尾时,我会自动回到数组的开头。

在这段简短的代码中,我通过调用带有目标标量、定义接口的模块名称和传递给TIESCALAR的参数的tie来创建关联标量。之后,我像使用正常标量一样使用$scalar

use v5.10;
use Tie::Cycle;

tie my $scalar, 'Tie::Cycle', [ qw(red green blue) ];

my $count;
while( $count++ < 10 ) {
        say $scalar;
        }

每次通过while循环时,我都会输出$scalar的值。看起来我并没有做什么复杂的事情,但我每次都隐式地调用Tie::Cycle::FETCH。现在颜色在旋转。

当通过彩色框循环时,这会更加有趣

use v5.10;
use open qw(:std :utf8);

use Tie::Cycle;
use Term::ANSIColor;

tie my $scalar, 'Tie::Cycle', [
        map { colored( [ $_ ], "\x{25AE}" ) }
                qw(red green blue)
        ];

my $count;
while( $count++ < 10 ) {
        print $scalar;
        }
print "\n";

在合适的终端上,我看到一串圣诞灯

最近,David Farrell遇到了一个类似的问题。他可以将一个值传递给一个方法,该方法在失败时会有一个延迟时间来重试。这是一个很好的特性,但他只能传递标量。他不想传递两秒的值,然后每两秒重试一次。相反,他想要退避。第一次等待两秒,然后是四次,然后是八次。如果你因为太频繁地撞击它而使某物跌倒,你想要减轻压力。

然而,使用与HTML行颜色相同的技巧,他能够创建一个看起来像简单的标量变量,但实际上是一个每次增加值的函数调用

use strict;
use warnings;
package Tie::Scalar::Ratio;

use parent 'Tie::Scalar';

sub TIESCALAR {
  my ($class, $ratio, $value) = @_;

  die 'Must provide ratio argument, a number to multiply the scalar value by'
        unless $ratio && $ratio =~ /^[\d.]+$/;

  bless {
        ratio => $ratio,
        value => $value,
  }, $class;
}

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

sub FETCH {
  my ($self) = @_;
  my $old_value = $self->{value};
  $self->{value} *= $self->{ratio} if $self->{value};
  return $old_value;
}

1;

我演示这个程序的部分几乎和我先前的程序一样。我使用$scalar的部分是相同的。

use v5.10;
use Tie::Scalar::Ratio;

tie my $scalar, 'Tie::Scalar::Ratio', 2, 37;

my $count;
while( $count++ < 10 ) {
        say $scalar;
        }

每次我访问标量时,都会返回上一次值乘以比率。在这种情况下,我每次都把上一次的值乘以2

这是一个整洁的解决方案,因为它可以很好地融入现有的代码。期望得到单个值的现有代码现在得到了一个每次都改变值的标量。

我不想直接提供,而是想提供一个回调函数。David还创建了Tie::Scalar::Callback。每次我访问标量时,该模块都会调用我传递给它的子程序,并返回结果。代码看起来与其他代码类似

use strict;
use warnings;
package Tie::Scalar::Callback;

use parent 'Tie::Scalar';
use Carp qw(carp);

sub TIESCALAR {
  my ($class, $sub ) = @_;

  die 'Must provide subroutine reference argument'
        unless $sub && ref $sub eq ref sub {};

  bless $sub, $class;
}

sub STORE {
  carp "You can't assign to this tied scalar";
}

sub FETCH {
  my ($self) = @_;
  return $self->();
}

1;

这是一个子程序,它执行与上一个示例相同的功能,但它将状态存储在子程序中,而不是在绑定的对象中

my $coderef = sub {
        state $value  = 1/2;
        state $factor = 2;
        $value *= $factor;
        }

tie my $scalar, 'Tie::Scalar::Callback', $sub;

my $count;
while( $count++ < 10 ) {
        say $scalar;
        }

这是一个简单的回调,但我可以做一些更奇特的事情。比如基于正弦函数的函数?

use v5.10;
use Tie::Scalar::Callback;

my $coderef = sub {
        state $pi     = 3.14152926;
        state $eighth = $pi / 8;
        state $value  = 0;

        sprintf '%.3f', abs sin( $value += $eighth );
        };

tie my $scalar, 'Tie::Scalar::Callback', $coderef;

my $count;
while( $count++ < 10 ) {
        say $scalar;
        }

现在输出减慢并加速。可能有些东西更有用。也许我想使用平均负载来决定数字

use Sys::LoadAvg qw(loadavg);
use Tie::Scalar::Callback;

my $coderef = sub {
        state $factor  = 5;

        my @loads = loadavg();

        $factor * $loads[-1];
        };

tie my $scalar, 'Tie::Scalar::Callback', $coderef;

最后,为了好玩,这里有一个绑定的标量,它使用v5.14中引入的inline package NAMESPACE BLOCK语法创建斐波那契数列

use v5.14;

package Tie::Scalar::Fibonacci {
        use parent 'Tie::Scalar';
        use Carp qw(croak);
        use List::Util qw(sum);

        sub TIESCALAR {
                my( $class, $count ) = @_;
                $count = 2 unless defined $count;
                die "count must be a counting number" if $count =~ /[^0-9]/;
                die "count must be greater than 1"    if $count <= 1;

                my $array = [ ( 1 ) x ( $count ) ];
                bless $array, $class
                }
        sub STORE     { croak "You can't assign to this scalar!" }

        sub FETCH {
                my ($self) = @_;
                push @$self, sum( @$self );
                shift @$self;
                }
        }

tie my $scalar, 'Tie::Scalar::Fibonacci';

my $count;
while( $count++ < 10 ) {
        print $scalar, ' ';
        }
print "\n";

每次我访问它时,我都会得到斐波那契数列中的下一个数字。奇怪的是,这样计算时,我通过将其推到末尾来计算将来会使用的数字,并通过将其移到前端来返回最老的价值。这与互联网上几乎所有示例中的递归不同。

但是,它可以生成其他数列。我不需要查看前两个值,而是可以给TIESCALAR一个不同的数字来指定要累加多少个前面的数字

tie my $scalar, 'Tie::Scalar::Fibonacci', 5;

my $count;
while( $count++ < 10 ) {
        print $scalar, ' ';
        }
print "\n";

这个想法是相同的,但求和不同。如果你以有趣的方式使用了绑定的变量,请告诉我们!
这篇文章最初发布在PerlTricks.com上。

标签

brian d foy

brian d foy是一位Perl培训师和作家,也是Perl.com的高级编辑。他是Mastering PerlMojolicious Web ClientsLearning Perl Exercises的作者,以及Programming PerlLearning PerlIntermediate PerlEffective Perl Programming的合著者。

查看他们的文章

反馈

这篇文章有什么问题吗?通过在GitHub上打开一个问题或pull请求来帮助我们。