魔法关联标量
Perl的tie
机制允许我创建看起来像标量但可以做任何我想让它做的事情。我可以改变熟悉的标量接口(简单访问和赋值)的实际工作方式。我有时发现这个功能非常有用,以至于我在精通Perl中为此专门写了一章。
我想我是爱上这项技术了,因为它让我能够手动创建一些HTML,解决了看似无法解决的问题(所以你知道这一定是很久以前的事情了)。有人创建了一个库来创建HTML表格,允许我通过传递标量值给tr
的bgcolor
属性来设置表格行的颜色。幸运的是,我已经忘记了那是谁或代码看起来像什么,但大概是这个样子
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::Cycle。tie
接口允许我决定当访问或存储标量时它应该做什么。我通过定义特殊子程序为这两个操作提供代码。以下是显示这些特殊方法的代码片段
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
。
这是一个整洁的解决方案,因为它可以很好地融入现有的代码。期望得到单个值的现有代码现在得到了一个每次都改变值的标量。
我不想直接提供
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 Perl、Mojolicious Web Clients、Learning Perl Exercises的作者,以及Programming Perl、Learning Perl、Intermediate Perl和Effective Perl Programming的合著者。
查看他们的文章
反馈
这篇文章有什么问题吗?通过在GitHub上打开一个问题或pull请求来帮助我们。