Scope of the default variable $_ in Perl
Question
I have the following method which accepts a variable and then displays info from a database:
sub showResult {
if (@_ == 2) {
my @results = dbGetResults($_[0]);
if (@results) {
foreach (@results) {
print "$count - $_[1] (ID: $_[0])\n";
}
} else {
print "\n\nNo results found";
}
}
}
Everything works fine, except the print line in the foreach loop. This $_ variable still contains the values passed to the method.
Is there anyway to 'force' the new scope of values on $_, or will it always contain the original values?
If there are any good tutorials that explain how the scope of $_ works, that would also be cool!
Thanks
La solution
In Perl, the _
name can refer to a number of different variables:
The common ones are:
$_ the default scalar (set by foreach, map, grep)
@_ the default array (set by calling a subroutine)
The less common:
%_ the default hash (not used by anything by default)
_ the default file handle (used by file test operators)
&_ an unused subroutine name
*_ the glob containing all of the above names
Each of these variables can be used independently of the others. In fact, the only way that they are related is that they are all contained within the *_
glob.
Since the sigils vary with arrays and hashes, when accessing an element, you use the bracket characters to determine which variable you are accessing:
$_[0] # element of @_
$_{...} # element of %_
$$_[0] # first element of the array reference stored in $_
$_->[0] # same
The for
/foreach
loop can accept a variable name to use rather than $_
, and that might be clearer in your situation:
for my $result (@results) {...}
In general, if your code is longer than a few lines, or nested, you should name the variables rather than relying on the default ones.
Since your question was related more to variable names than scope, I have not discussed the actual scope surrounding the foreach loop, but in general, the following code is equivalent to what you have.
for (my $i = 0; $i < $#results; $i++) {
local *_ = \$results[$i];
...
}
The line local *_ = \$results[$i]
installs the $i
th element of @results
into the scalar slot of the *_
glob, aka $_
. At this point $_
contains an alias of the array element. The localization will unwind at the end of the loop. local
creates a dynamic scope, so any subroutines called from within the loop will see the new value of $_
unless they also localize it. There is much more detail available about these concepts, but I think they are outside the scope of your question.
Autres conseils
The problem here is that you're using really @_
instead of $_
. The foreach
loop changes $_
, the scalar variable, not @_
, which is what you're accessing if you index it by $_[X]
. Also, check again the code to see what it is inside @results
. If it is an array of arrays or refs, you may need to use the indirect ${$_}[0]
or something like that.
As others have pointed out:
- You're really using
@_
and not$_
in your print statement. - It's not good to keep stuff in these variables since they're used elsewhere.
Officially, $_
and @_
are global variables and aren't members of any package. You can localize the scope with my $_
although that's probably a really, really bad idea. The problem is that Perl could use them without you even knowing it. It's bad practice to depend upon their values for more than a few lines.
Here's a slight rewrite in your program getting rid of the dependency on @_
and $_
as much as possible:
sub showResults {
my $foo = shift; #Or some meaningful name
my $bar = shift; #Or some meaningful name
if (not defined $foo) {
print "didn't pass two parameters\n";
return; #No need to hang around
}
if (my @results = dbGetResults($foo)) {
foreach my $item (@results) {
...
}
}
Some modifications:
- I used
shift
to give your two parameters actual names.foo
andbar
aren't good names, but I couldn't find out whatdbGetResults
was from, so I couldn't figure out what parameters you were looking for. The@_
is still being used when the parameters are passed, and myshift
is depending upon the value of@_
, but after the first two lines, I'm free. - Since your two parameters have actual names, I can use the
if (not defined $bar)
to see if both parameters were passed. I also changed this to the negative. This way, if they didn't pass both parameters, you can exit early. This way, your code has one less indent, and you don't have a if structure that takes up your entire subroutine. It makes it easier to understand your code. - I used
foreach my $item (@results)
instead offoreach (@results)
and depend upon$_
. Again, it's clearer what your program is doing, and you wouldn't have confused$_->[0]
with$_[0]
(I think that's what you were doing). It would have been obvious you wanted$item->[0]
.