简体   繁体   中英

can a variable be defined in perl whose value cannot be changed in a subroutine?

In the following script, I declare and modify @basearray in the main program. Inside the dosomething subroutine, I access @basearray , assign it to an array local to the script, and modify the local copy. Because I have been careful to change the value only of local variables inside the subroutine, @basearray is not changed.

If I had made the mistake of assigning a value to @basearray inside the subroutine, though, it would have been changed and that value would have persisted after the call to the subroutine.

This is demonstrated in the 2nd subroutine, doagain .

Also, doagain receives the reference \\@basearray as an argument rather than accessing @basearray directly. But going to that additional trouble provides no additional safety. Why do it that way at all?

Is there a way to guarantee that I cannot inadvertently change @basearray inside any subroutine? Any kind of hard safety device that I can build into my code, analogous to use strict; , some combination perhaps of my and local ?

Am I correct in thinking that the answer is No, and that the only solution is to not make careless programmer errors?

#!/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" };
}

There isn't a pragma or a keyword or such, but there are well established "good practices," which in this case completely resolve what you reasonably ponder about.

  1. The first sub, dosomething , commits the sin of using variables visible in its scope but defined in the higher scope. Instead, always pass needed data to a subroutine (exceptions are rare, in crystal clear cases).

    Directly using data from "outside" defies the idea of a function as an encapsulated procedure, exchanging data with its users via a well defined and clear interface. It entangles ("couples") sections of code that are in principle completely unrelated. In practice, it can also be outright dangerous.

    Also, the fact the @basearray is up for grabs in the sub is best considered an accident -- what when that sub gets moved to a module? Or another sub is introduced to consolidate code where @basearray is defined?

  2. The second sub, doagain , nicely takes a reference to that array. Then, to protect the data in the caller, one can copy the caller's array to another one which is local to the sub

    sub doagain { my ($ref_basearray) = @_; my @local_ba = @$ref_basearray; # work with local_ba and the caller's basearray is safe }

    The names of local lexical variables are of course arbitrary, but a convention where they resemble the caller's data names may be useful.

Then you can adopt a general practice, for safety, to always copy input variables to local ones. Work directly with references that are passed in only when you want to change the caller's data (relatively rare in Perl). This may hurt efficiency if it's done a lot with sizeable data, or when really large data structures are involved. So perhaps then make an exception and change data via its reference, and be extra careful.

There are several solutions with various levels of pithiness from "just don't change it" to "use an object or tied array and lock down the update functions". An intermediate solution, not unlike using an object with a getter method, is to define a function that returns your array but can only operate as an rvalue, and to use that function inside subroutines.

my @basearray = (...);
sub basearray { return @basearray }

sub foo {
    foreach my $elem (basearray()) {
       ...
    }
    @bar = map { $_ *= 2 } basearray();  # ok
    @bar = map { $_ *= 2 } @basearray;   # modifies @basearray!
    
}

(Putting my comment as answer) One way to guarantee not changing a variable inside a subroutine is to not change it. Use only lexically scoped variables inside the subroutine, and pass whatever values you need inside the subroutine as arguments to the subroutine. It is a common enough coding practice, encapsulation.

One idea that you can use -- mainly as practice, I would say -- to force yourself to use encapsulation, is to put a block around your "main" code, and place subroutines outside of it. That way, if you should accidentally refer to a (formerly) global variable, use strict will be able to do it's job and produce a fatal error. Before runtime.

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
}

This will not compile, and will produce the error:

Global symbol "@basearray" requires explicit package name at foo.pl line 15.
Execution of foo.pl aborted due to compilation errors.

I would consider this a training device to force yourself to using good coding practices, and not something to necessarily use in production code.

TLDR: yes, but.

I'll start with the "but". But it's better to design your code so that the variable simply doesn't exist in the scope where the untrusted function is defined.

sub untrusted_function {
  ...
}

my @basearray = qw( ... );  # declared after untrusted_function

If untrusted_function needs to be able to access the contents of the array, pass it a copy of the array as a parameter, so it can't modify the original.

Now here's the "yes".

You can mark the array as read-only before calling the untrusted function.

Internals::SvREADONLY($_, 1) for @basearray;
Internals::SvREADONLY(@basearray, 1);

Then mark it read-write again after the function has finished.

Internals::SvREADONLY(@basearray, 0);
Internals::SvREADONLY($_, 0) for @basearray;

Using Internals::SvREADONLY(@basearray, $bool) modifies the read-only state of the array itself, preventing elements from being added or removed from it; Internals::SvREADONLY($_, $bool) for @basearray modifies the read-only state of each element in the array too, which you probably want.

Of course, if your array contains references like blessed objects, you then need to consider whether you need to recurse into the references, marking them read-only too. (But can also be a concern with the shallow copy of the array I mentioned in the preferred solution!)

So yes , it is possible to prevent a sub from accidentally modifying a variable by marking that variable read-only before calling the sub, but it's a better idea to restructure your code so the sub simply doesn't have access to the variable at all.

Yes, but.

Here is a prototype that uses @TLP's answer.

#!/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 );
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM