可以在 perl 中定义一个变量,其值不能在子例程中更改吗?
can a variable be defined in perl whose value cannot be changed in a subroutine?
在下面的脚本中,我在主程序中声明并修改了@basearray
。在 dosomething
子例程中,我访问 @basearray
,将其分配给脚本的本地数组,并修改本地副本。因为我一直很小心只改变子程序内部局部变量的值,@basearray
没有改变。
如果我在子例程中错误地为 @basearray
赋值,那么它会被更改并且该值会在子例程调用后保留。
这在第二个子例程中进行了演示,doagain
。
此外,doagain
接收引用 \@basearray
作为参数,而不是直接访问 @basearray
。但是,去那个额外的麻烦并不能提供额外的安全。为什么要这样做?
有没有办法保证我不会在任何子程序中无意中更改 @basearray
?我可以在我的代码中构建任何类型的硬安全设备,类似于 use strict;
,可能是 my
和 local
的某种组合?
我认为答案是否定的,唯一的解决方案是不要犯粗心的程序员错误,我是否正确?
#!/usr/bin/perl
use strict; use warnings;
my @basearray = qw / amoeba /;
my $count;
{
print "\@basearray==\n";
$count = 0;
foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}
sub dosomething
{
my $sb_name = (caller(0))[3];
print "entered $sb_name\n";
my @sb_array=( @basearray , 'dog' );
{
print "\@sb_array==\n";
$count = 0;
foreach my $el (@sb_array) { $count++; print "$count:\t$el\n" };
}
print "return from $sb_name\n";
}
dosomething();
@basearray = ( @basearray, 'rats' );
{
print "\@basearray==\n";
$count = 0;
foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}
sub doagain
{
my $sb_name = (caller(0))[3];
print "entered $sb_name\n";
my $sf_array=$_[0];
my @sb_array=@$sf_array;
@sb_array=( @sb_array, "piglets ... influenza" );
{
print "\@sb_array==\n";
$count = 0;
foreach my $el (@sb_array) { $count++; print "$count:\t$el\n" };
}
print "now we demonstrate that passing an array as an argument to a subroutine does not protect it from being globally changed by programmer error\n";
@basearray = ( @sb_array );
print "return from $sb_name\n";
}
doagain( \@basearray );
{
print "\@basearray==\n";
$count = 0;
foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}
从“只是不要更改它”到“使用对象或绑定数组并锁定更新函数”,有几种不同层次的解决方案。一个中间解决方案,与使用带有 getter 方法的对象一样,是定义一个 returns 你的数组但只能作为右值操作的函数,并在子例程中使用该函数。
my @basearray = (...);
sub basearray { return @basearray }
sub foo {
foreach my $elem (basearray()) {
...
}
@bar = map { $_ *= 2 } basearray(); # ok
@bar = map { $_ *= 2 } @basearray; # modifies @basearray!
}
没有 pragma 或关键字等,但有公认的“良好做法”,在这种情况下完全解决了您合理思考的问题。
第一个子 dosomething
犯了使用在其范围内可见但在更高范围内定义的变量的错误。相反,始终将需要的数据传递给子例程(在 crystal 明确的情况下,例外情况很少见)。
直接使用来自“外部”的数据违背了函数作为封装过程的想法,通过定义明确且清晰的界面与其用户交换数据。它纠缠(“耦合”)原则上完全不相关的代码部分。在实践中,它也可能非常危险。
此外,@basearray
在潜艇中待售这一事实最好被视为意外——当潜艇被移至模块时怎么办?或者引入另一个子来合并定义@basearray
的代码?
第二个子 doagain
很好地引用了该数组。然后,为了保护调用者中的数据,可以将调用者的数组复制到另一个子
本地的数组
sub doagain {
my ($ref_basearray) = @_;
my @local_ba = @$ref_basearray;
# work with local_ba and the caller's basearray is safe
}
局部词法变量的名称当然是任意的,但它们与调用者的数据名称相似的约定可能会有用。
然后你可以采用一般做法,为了安全起见,总是将输入变量复制到本地变量。直接使用仅在您想要更改调用者的数据时传入的引用(在 Perl 中相对较少)。如果对大量数据进行了大量操作,或者涉及非常大的数据结构,这可能会降低效率。因此,也许 then 通过引用更改数据,并格外小心。
(把我的评论作为答案)
保证不更改子例程内的变量的一种方法是不更改它。在子例程中仅使用词法范围的变量,并将子例程中所需的任何值作为参数传递给子例程。这是一种足够常见的编码实践,封装。
您可以使用的一个想法——主要是作为练习,我想说——强迫自己使用封装,是在您的“主要”代码周围放置一个块,并将子例程放在它之外。这样,如果您不小心引用了一个(以前的)全局变量,use strict
将能够完成它的工作并产生致命错误。运行前。
use strict;
use warnings;
main: { # lexical scope reduced to this block
my @basearray = qw / amoeba /;
print foo(@basearray); # works
print bar(); # fatal error
} # END OF MAIN lexical scope of @basearray ends here
sub foo {
my @basearray = @_; # encapsulated
return $basearray[1]++;
}
sub bar {
return $basearray[1]++; # out of scope ERROR
}
这不会编译,并且会产生错误:
Global symbol "@basearray" requires explicit package name at foo.pl line 15.
Execution of foo.pl aborted due to compilation errors.
我认为这是一种训练工具,可以强迫自己使用良好的编码实践,而不是必须在生产代码中使用的东西。
TLDR:是的,但是。
我将从“但是”开始。但最好设计您的代码,使变量根本不存在于定义不受信任函数的范围内。
sub untrusted_function {
...
}
my @basearray = qw( ... ); # declared after untrusted_function
如果untrusted_function
需要能够访问数组的内容,将数组的副本作为参数传递给它,这样它就不能修改原来的。
现在是“是”。
您可以在调用不受信任的函数之前将数组标记为read-only。
Internals::SvREADONLY($_, 1) for @basearray;
Internals::SvREADONLY(@basearray, 1);
然后在函数结束后再次标记read-write
Internals::SvREADONLY(@basearray, 0);
Internals::SvREADONLY($_, 0) for @basearray;
使用Internals::SvREADONLY(@basearray, $bool)
修改数组本身的read-only状态,防止元素被添加或删除; Internals::SvREADONLY($_, $bool) for @basearray
也会修改数组中每个元素的 read-only 状态,这可能是您想要的。
当然,如果你的数组包含像祝福对象这样的引用,那么你需要考虑是否需要递归到引用中,也标记它们read-only。 (但也可能与我在首选解决方案中提到的数组的浅拷贝有关!)
所以是,可以通过在调用子之前标记变量read-only来防止子意外修改变量,但是 最好重构您的代码,这样 sub 根本无法访问变量。
是的,但是。
这是一个使用@TLP 答案的原型。
#!/usr/bin/perl
use strict; use warnings;
{ # block_main BEG
my @basearray = qw / amoeba elephants sequoia /;
print join ( ' ', 'in main, @basearray==', join ( ' ', @basearray ), "\n" );
print "Now we call subroutine to print it:\n"; enumerateprintarray ( \@basearray );
my $ref_basearray = changearray ( \@basearray, 'wolves or coyotes . . . ' );
@basearray = @$ref_basearray;
print "Now we call subroutine to print it:\n"; enumerateprintarray ( \@basearray );
} # block_main END
sub enumerateprintarray
{
my $sb_name = (caller(0))[3];
#print join ( '' , @basearray ); # mortal sin! for in the day that thou eatest thereof thou shalt surely die.
my $sb_exact_count_arg = 1;
die "$sb_name must have exactly $sb_exact_count_arg arguments" unless ( ( scalar @_ ) == $sb_exact_count_arg );
my $sf_array = $_[0];
my @sb_array = @$sf_array;
my $sb_count = 0;
foreach (@sb_array)
{
$sb_count++;
print "\t$sb_count:\t$_\n";
}
}
sub changearray
{
my $sb_name = (caller(0))[3];
#print join ( '' , @basearray ); # in the day that thou eatest thereof thou shalt surely die.
my $sb_exact_count_arg = 2;
die "$sb_name must have exactly $sb_exact_count_arg arguments" unless ( ( scalar @_ ) == $sb_exact_count_arg );
my ( $sf_array, $addstring ) = @_;
my @sb_array = @$sf_array;
push @sb_array, $addstring;
return ( \@sb_array );
}
在下面的脚本中,我在主程序中声明并修改了@basearray
。在 dosomething
子例程中,我访问 @basearray
,将其分配给脚本的本地数组,并修改本地副本。因为我一直很小心只改变子程序内部局部变量的值,@basearray
没有改变。
如果我在子例程中错误地为 @basearray
赋值,那么它会被更改并且该值会在子例程调用后保留。
这在第二个子例程中进行了演示,doagain
。
此外,doagain
接收引用 \@basearray
作为参数,而不是直接访问 @basearray
。但是,去那个额外的麻烦并不能提供额外的安全。为什么要这样做?
有没有办法保证我不会在任何子程序中无意中更改 @basearray
?我可以在我的代码中构建任何类型的硬安全设备,类似于 use strict;
,可能是 my
和 local
的某种组合?
我认为答案是否定的,唯一的解决方案是不要犯粗心的程序员错误,我是否正确?
#!/usr/bin/perl
use strict; use warnings;
my @basearray = qw / amoeba /;
my $count;
{
print "\@basearray==\n";
$count = 0;
foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}
sub dosomething
{
my $sb_name = (caller(0))[3];
print "entered $sb_name\n";
my @sb_array=( @basearray , 'dog' );
{
print "\@sb_array==\n";
$count = 0;
foreach my $el (@sb_array) { $count++; print "$count:\t$el\n" };
}
print "return from $sb_name\n";
}
dosomething();
@basearray = ( @basearray, 'rats' );
{
print "\@basearray==\n";
$count = 0;
foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}
sub doagain
{
my $sb_name = (caller(0))[3];
print "entered $sb_name\n";
my $sf_array=$_[0];
my @sb_array=@$sf_array;
@sb_array=( @sb_array, "piglets ... influenza" );
{
print "\@sb_array==\n";
$count = 0;
foreach my $el (@sb_array) { $count++; print "$count:\t$el\n" };
}
print "now we demonstrate that passing an array as an argument to a subroutine does not protect it from being globally changed by programmer error\n";
@basearray = ( @sb_array );
print "return from $sb_name\n";
}
doagain( \@basearray );
{
print "\@basearray==\n";
$count = 0;
foreach my $el (@basearray) { $count++; print "$count:\t$el\n" };
}
从“只是不要更改它”到“使用对象或绑定数组并锁定更新函数”,有几种不同层次的解决方案。一个中间解决方案,与使用带有 getter 方法的对象一样,是定义一个 returns 你的数组但只能作为右值操作的函数,并在子例程中使用该函数。
my @basearray = (...);
sub basearray { return @basearray }
sub foo {
foreach my $elem (basearray()) {
...
}
@bar = map { $_ *= 2 } basearray(); # ok
@bar = map { $_ *= 2 } @basearray; # modifies @basearray!
}
没有 pragma 或关键字等,但有公认的“良好做法”,在这种情况下完全解决了您合理思考的问题。
第一个子
dosomething
犯了使用在其范围内可见但在更高范围内定义的变量的错误。相反,始终将需要的数据传递给子例程(在 crystal 明确的情况下,例外情况很少见)。直接使用来自“外部”的数据违背了函数作为封装过程的想法,通过定义明确且清晰的界面与其用户交换数据。它纠缠(“耦合”)原则上完全不相关的代码部分。在实践中,它也可能非常危险。
此外,
@basearray
在潜艇中待售这一事实最好被视为意外——当潜艇被移至模块时怎么办?或者引入另一个子来合并定义@basearray
的代码?第二个子
本地的数组doagain
很好地引用了该数组。然后,为了保护调用者中的数据,可以将调用者的数组复制到另一个子sub doagain { my ($ref_basearray) = @_; my @local_ba = @$ref_basearray; # work with local_ba and the caller's basearray is safe }
局部词法变量的名称当然是任意的,但它们与调用者的数据名称相似的约定可能会有用。
然后你可以采用一般做法,为了安全起见,总是将输入变量复制到本地变量。直接使用仅在您想要更改调用者的数据时传入的引用(在 Perl 中相对较少)。如果对大量数据进行了大量操作,或者涉及非常大的数据结构,这可能会降低效率。因此,也许 then 通过引用更改数据,并格外小心。
(把我的评论作为答案) 保证不更改子例程内的变量的一种方法是不更改它。在子例程中仅使用词法范围的变量,并将子例程中所需的任何值作为参数传递给子例程。这是一种足够常见的编码实践,封装。
您可以使用的一个想法——主要是作为练习,我想说——强迫自己使用封装,是在您的“主要”代码周围放置一个块,并将子例程放在它之外。这样,如果您不小心引用了一个(以前的)全局变量,use strict
将能够完成它的工作并产生致命错误。运行前。
use strict;
use warnings;
main: { # lexical scope reduced to this block
my @basearray = qw / amoeba /;
print foo(@basearray); # works
print bar(); # fatal error
} # END OF MAIN lexical scope of @basearray ends here
sub foo {
my @basearray = @_; # encapsulated
return $basearray[1]++;
}
sub bar {
return $basearray[1]++; # out of scope ERROR
}
这不会编译,并且会产生错误:
Global symbol "@basearray" requires explicit package name at foo.pl line 15.
Execution of foo.pl aborted due to compilation errors.
我认为这是一种训练工具,可以强迫自己使用良好的编码实践,而不是必须在生产代码中使用的东西。
TLDR:是的,但是。
我将从“但是”开始。但最好设计您的代码,使变量根本不存在于定义不受信任函数的范围内。
sub untrusted_function {
...
}
my @basearray = qw( ... ); # declared after untrusted_function
如果untrusted_function
需要能够访问数组的内容,将数组的副本作为参数传递给它,这样它就不能修改原来的。
现在是“是”。
您可以在调用不受信任的函数之前将数组标记为read-only。
Internals::SvREADONLY($_, 1) for @basearray;
Internals::SvREADONLY(@basearray, 1);
然后在函数结束后再次标记read-write
Internals::SvREADONLY(@basearray, 0);
Internals::SvREADONLY($_, 0) for @basearray;
使用Internals::SvREADONLY(@basearray, $bool)
修改数组本身的read-only状态,防止元素被添加或删除; Internals::SvREADONLY($_, $bool) for @basearray
也会修改数组中每个元素的 read-only 状态,这可能是您想要的。
当然,如果你的数组包含像祝福对象这样的引用,那么你需要考虑是否需要递归到引用中,也标记它们read-only。 (但也可能与我在首选解决方案中提到的数组的浅拷贝有关!)
所以是,可以通过在调用子之前标记变量read-only来防止子意外修改变量,但是 最好重构您的代码,这样 sub 根本无法访问变量。
是的,但是。
这是一个使用@TLP 答案的原型。
#!/usr/bin/perl
use strict; use warnings;
{ # block_main BEG
my @basearray = qw / amoeba elephants sequoia /;
print join ( ' ', 'in main, @basearray==', join ( ' ', @basearray ), "\n" );
print "Now we call subroutine to print it:\n"; enumerateprintarray ( \@basearray );
my $ref_basearray = changearray ( \@basearray, 'wolves or coyotes . . . ' );
@basearray = @$ref_basearray;
print "Now we call subroutine to print it:\n"; enumerateprintarray ( \@basearray );
} # block_main END
sub enumerateprintarray
{
my $sb_name = (caller(0))[3];
#print join ( '' , @basearray ); # mortal sin! for in the day that thou eatest thereof thou shalt surely die.
my $sb_exact_count_arg = 1;
die "$sb_name must have exactly $sb_exact_count_arg arguments" unless ( ( scalar @_ ) == $sb_exact_count_arg );
my $sf_array = $_[0];
my @sb_array = @$sf_array;
my $sb_count = 0;
foreach (@sb_array)
{
$sb_count++;
print "\t$sb_count:\t$_\n";
}
}
sub changearray
{
my $sb_name = (caller(0))[3];
#print join ( '' , @basearray ); # in the day that thou eatest thereof thou shalt surely die.
my $sb_exact_count_arg = 2;
die "$sb_name must have exactly $sb_exact_count_arg arguments" unless ( ( scalar @_ ) == $sb_exact_count_arg );
my ( $sf_array, $addstring ) = @_;
my @sb_array = @$sf_array;
push @sb_array, $addstring;
return ( \@sb_array );
}