$_ und seine Tücken (war: UnderScore.pm)

Martin H. Sluka <martin@sluka.de>

2003-01-30

$_ ist praktisch. Die Verwendung von $_ kann aber auch gefährlich sein. Warum? Darum:

01| #!/usr/bin/perl
02|
03| use strict;
04|
05| {
06|     my $begin_of_data = tell DATA;
07| 
08|     sub look4($) {
09|         my ($pattern) = @_;
10|         seek DATA, $begin_of_data, 0 or die;
11|         my @found;
12|         while (<DATA>) { push @found, $_ if /$pattern/ }
13|         @found;
14|     }
15| }
16| 
17| my @patterns = qw(eins zwei drei);
18| for (@patterns) {
19|     my $hits = look4 $_;
20|     print "$_: $hits Treffer.\n";
21| }
22| 
23| __DATA__
24| Eins und eins das macht zwei.
25| Das ist der Rand von Ostermundigen.

Die Funktion look4() sucht also nach Zeilen aus dem DATA-Segment des Scripts, in denen das (als regulärer Ausdruck) angegebene Suchmuster vorkommt, und gibt diese Zeilen zurück. Als Ausgabe könnte man also erwarten:

eins: 1 Treffer.
zwei: 1 Treffer.
drei: 0 Treffer.

Tatsächlich gibt das Script jedoch folgendes aus:

: 1 Treffer.
: 1 Treffer.
: 0 Treffer.

Warum? Ganz einfach: :o) Die while-Schleife in der Funktion look4() legt die Daten in $_ ab. Dabei handelt es sich keineswegs — wie dies etwa bei einer foreach-Schleife der Fall wäre — um ein lokales $_, sondern um das der foreach-Schleife aus Zeilen 18 bis 21; dieses wird somit überschrieben, und zwar mit dem undef, das beim Erreichen des Endes des DATA-Segments erzeugt wird und die while-Schleife schließlich beendet. Und schlimmer noch: Da das $_ der foreach-Schleife effektiv ein Verweis auf das jeweilige Element aus @patterns ist, besteht auch dieses Array nach Durchlaufen der Schleife nur noch aus undefs.

Das Fatale an dieser Art von Phänomenen ist dabei, dass man sie oft gar nicht bemerkt; wäre die foreach-Schleife z. B. als

print "$_: ".look4($_)." Treffer.\n" for @patterns;

geschrieben worden, wäre das Problem wahrscheinlich zunächst gar nicht aufgefallen, und man hätte sich möglicherweise sehr viel später gewundert, weshalb @patterns nicht mehr die ursprünglichen Werte enthält.

Was also tun? Auf der sicheren Seite ist man, wenn man die Verwendung von $_ generell vermeidet. Damit man seine guten Vorsätze im Eifer des Gefechts nicht versehentlich wieder über den Haufen schmeißt, könnte man Tom Christiansens Modul UnderScore.pm verwenden, das immer dann virtuelle Schläge auf den Hinterkopf verteilen soll, wenn auf $_ zugegriffen wird; hier der vollständige Quelltext dieses Moduls:

	package UnderScore;
	
	use Carp;
	
	sub TIESCALAR {
	    my $class = shift;
	    my $dummy;
	    return bless \$dummy => $class;
	} 
	
	sub FETCH { croak "Read access to \$_ forbidden"  } 
	sub STORE { croak "Write access to \$_ forbidden" } 
	
	sub unimport { tie($_, __PACKAGE__) }
	sub import { untie $_ } 
	
	tie($_, __PACKAGE__) unless tied $_;
	
	1;

Es hängt sich also via tie() an $_ und schlägt immer dann Alarm, wenn ein Lese- oder Schreibzugriff auf diese Variable erfolgt. Zu beachten ist dabei, dass das tie() im Rahmen der unimport()-Routine passiert, folglich muss man das Modul mit "no UnderScore;" aktivieren; zweifelsohne ist das aber auch ungleich intuitiver, als wenn man "use UnderScore;" sagen müsste, um $_ dann gerade nicht zu benutzen — nebenbei bemerkt kann man dieses Statement jedoch verwenden, um den Zugriff auf $_ (z. B. lokal innerhalb eines Blocks) vorübergehend doch wieder zu erlauben.

So weit, so gut — sollte man meinen. Im Rahmen der Vorbereitung dieses Beitrags stieß ich jedoch auf zwei Probleme:

  1. Auch mit "no UnderScore;" in Aktion wäre im Eingangsbeispiel kein Alarm aufgetreten, da das fragliche $_ ja das lokale der foreach-Schleife war, für das das tie() nicht wirksam wäre.
  2. Eigentlich sollte es erlaubt sein, eine local()isierte Version von $_ zu verwenden — was beispielsweise auch viele Module tun —, was aber in neueren Perl-Versionen (wohl ab 5.005) nicht mehr funktioniert, da das tie() durch das local() nicht aufgehoben wird.

Insofern ist leider auch UnderScore.pm keine wirklich gute Lösung für das eingangs geschilderte Problem; vielleicht hat jemand eine bessere!?