от Курс за ССОК
Увод в програмирането на Perl
Лекция #9 Препратки
В тази глава ще се запознаем с препратките (references), които дават непряк достъп до променлива или функция. Препратките са скалари, като те могат да се ползват за манипулиране на няколко променливи едновременно, както и за олесняване на предаване на някои аргументи на функции. Препратките могат да се ползват и за създаване и манипулиране на анонимни и вложени стуктури от данни.
Обикновените скалари директно съдържат стойности, но те също така могат да съдържат и стойност, която указва къде да се намери друга стойност в паметта. Именно последните скалари се наричат препратки. Има два вида препратки – твърди (hard references) и символични (symbolic references). Разликата между двата вида е, че твърдите препратки реферират стойност в паметта, а символичните съдържат само името на променливата, която те реферират. Твърдите препратки са по-ефикасни и имат предимство пред символичните (по подразбиране под думата 'препратка' ще разбираме твърда такава). Създаваме препратки към променливи като пред името на променливата сложим унарния оператор \. Препратките имат скаларния типов идентификатор, $. За да достъпим реферираната от препратка стойност, трябва да дереферираме препратката. Ако например $reference реферира скалар, то стойността на този скалар може да се достъпи с $$reference. Аналогично, можем да ползваме @$reference и %$reference да достъпим масив или хеш, рефериран от препратката:
#!/usr/bin/perl
use warnings;
use strict;
my $var = 10;
my $ref = \$var;
print(“\$var = $var\n”);
print(“\$ref = $ref\n”);
print(“\$\$ref = $$ref\n”);
$var++;
print(“\$var = $var\n”);
print(“\$ref = $ref\n”);
print(“\$\$ref = $$ref\n”);
$$ref++;
print(“\$var = $var\n”);
print(“\$ref = $ref\n”);
print(“\$\$ref = $$ref\n”);
В примера се забелязва, че когато принтираме препратка без да я дереферираме, то резултатът е низ с вида на препратката и местоположението на реферираната стойност в паметта. Препратките могат да се превръщат в низове, но обратната операция не може да се извърши.
Можем да създаваме референции до всеки вид структури от данни. Синтаксисът е аналогичен – предхожда се името на променливата със \ (\@array и \%hash връщат препратки към съответния масив и хеш). Разликата е при дереферирането – ако например $ref сочи към масив, то за да достъпим третия елемент можем да ползваме $$ref[2] или $ref->[2] (тук се ползва оператора ->). За да реферираме целия масив можем да ползваме @$ref. Със хешовете можем да работим подобно на масивите:
#!/usr/bin/perl
use warnings;
use strict;
my @array = qw(duck pig horse rooster cow);
my %hash = ( duck => “quack”,
pig => “oink”,
horse => “neigh”,
rooster => “doodle-doo”,
cow => “mow” );
my $arrayRef = \@array;
my $hashRef = \%hash;
print(“@$arrayRef\n”);
print(“\$\$arrayRef[1] = $$arrayRef[1]\n”);
print(“\$arrayRef->[1] = $arrayRef->[1]\n”);
print(“\${returnReference()}[1] = ${returnReference()}[1]\n\n”);
print(“\$\$hashRef{duck} = $$hashRef{duck}\n”);
print(“\$hashRef->{duck} = $hashRef->{duck}\n\n”);
foreach (keys(%$hashRef)) {
print(“The $_ makes $hashRef->{$_}.\n”);
}
#########
sub returnReference {
return \@array;
}
Вижда се, че ако блок от код връща препратка, то тя може да се дереферира директно, както в ${($x > 2) ? $ref1 : $ref2}.
Възможно е и да създадем препратка към съществуваща вече функция. Например, \&someFunction създава препратка към съответната функция. Ако променливата $functionRef съдържа препратка към функция, то функцията може да бъде извикана чрез &$functionRef() или $functionRef->():
#!/usr/bin/perl
use warnings;
use strict;
my $functionRef = \&function;
&$functionRef(“a”, “lot”, “of”, “words”);
$functionRef->(“some”, “other”, “words”);
#############
sub function {
print(“Args passed: @_\n\n”);
}
Също така имаме възможност да създаваме препратки към други препратки, или т.н. вложени препратки, като те могат да се влагат до каквато поискаме дълбочина:
#!/usr/bin/perl
use warnings;
use strict;
my $var = 5;
my $ref1 = \$var;
print(“$var, $ref1, $$ref1\n\n”);
my $ref2 = \$ref1;
print(“$var, $ref1, $$ref1, $ref2, $$ref2, $$$ref2\n\n”);
my $ref3 = \\\\$var;
print(“$ref3, $$ref3, $$$ref3, $$$$ref3, $$$$$ref3\n\n”);
Анонимните структури не се асоциират директно с имена на променливи. С други думи, съществува препратка към данните, но няма име на променлива с което може да се достъпят данните директно. Perl позволява няколко начина за създаване на анонимни структури. Можем да заградим списък вместо с (), с []. Стойността, която се връща е препратка към неименуван масив (anonymous array composer). Със хешовете случаят е аналогичен, но вместо [] ползваме {}. Анонимната структура се достъпва чрез препратка като всяка друга структура:
#!/usr/bin/perl
use warnings;
use strict;
my $array = [ qw(There was an old lady who lived in a shoe...) ];
my $hash = { “I'm a” => “little tea cup”,
“ short” => “and stout...” };
print(“@$array\n”);
print(%$hash, “\n”);
my $array2;
@$array2 = (“Ivan”, “Ivanov”);
$$array2[5] = “wall...”;
$array2->[2] = “sat”;
@$array2[3, 4] = (“on”, “a”);
print(“@$array2\n\n”);
Тук е важно да се спомене, че реда @$array2 = (“Ivan”, “Ivanov”); не е еквивалентен на \(“Ivan”, “Ivanov”). Всъщност \(“Ivan”, “Ivanov”) е еквивалентно на (\“Ivan”, \“Ivanov”), което създава списък от препратки, т.е. не можем да създадем препратка към списък като поставим \ пред списъка.
Декларирането на анонимна функция е аналогично на това на обикновена функция, с разликата че името се пропуска. Една анонимна функция може да се присвои на препратка за по-нататъшна употреба:
#!/usr/bin/perl
use warnings;
use strict;
my $productRef = sub
{
my $product = 1;
foreach (@_) {
$product *= $_;
}
return $product;
};
my $printVal = &$productRef(1, 2, 3, 4);
print( join(' * ', 1, 2, 3, 4), “ = ” );
print(“$printVal\n”);
$printVal = $productRef->(6, 8, -5, 2);
print( join(' * ', 6, 8, -5, 2), “ = ” );
print(“$printVal\n\n”);
Една интересна употреба на препратки и анонимни структури ни позволява да интерполираме връщаната стойност на дадена функция в низ с двойни кавички:
#!/usr/bin/perl
use warnings;
use strict;
print(“The number is square(5).\n”);
print(“The number is ${\square(5)}.\n”);
########
sub square {
my $x = shift;
return $x * $x;
}
По принцип ако се опитаме да викнем функция от низ с двойни кавички, извикването няма да се интерполира и името на функцията ще се третира като обикновен низ. Но, както се вижда в примера, ако създадем препратка към връщаната от функцията стойност и я дереферираме, то ще успеем да покажем съответната връщана стойност.
Досега не беше разгледан начин да избегнем изпращането само на списък със скалари към една функция (аргументите, които се подават се опростяват до списък от скалари, пазен в @_). Една функция може да приеме всичко, което може да се реферира от препратка:
#!/usr/bin/perl
use warnings;
use strict;
my @array1 = (1..8);
my @array2 = ('a'..'e');
my @mixed = arrayMixer(\@array1, \@array2);
print(“@mixed\n\n”);
my @mixed2 = arrayMixer( ['I', 'to', 'park'], ['go', 'the'] );
print(“@mixed2\n\n”);
sub arrayMixer {
my @firstArray = @{ $_[0] };
my @secondArray = @{ $_[1] };
my ($first, $second, @array);
while( ($first = shift @firstArray) &&
($second = shift @secondArray) ) {
push(@array, $first, $second);
}
push(@array, $first, @firstArray) if ($first);
push(@array, @secondArray) if ($second);
return @array;
}
В случая, без препратки, ако искахме да подадем два масива на една функция, то би трябвало да подадем и броя на елементите на масивите преди да подадем самите тях, за да можем да работим със съответните масиви във функцията (защо?). Препратките се подават на функции като всеки друг скалар. В този пример трябва да се забележи употребата на {} като @{ $_[0] } @{ $_[1] }, за да укажем какво точно искаме да дереферираме (ако не бяхме използвали скобите, то @ ще опита да дереферира $_ и квадратните скоби след това ще създадат масивен отрез съдържащ нулевия елемент на дереферирания масив. Това се получава, защото @ има по-висок приоритет от [] ).
Понякога можем да искаме да предприемем различно действие в зависимост от препратката, която сме получили. Функцията ref може да бъде използвана за определяне типа на съответната препратка. Тя приема препратка за аргумент и връща низ, който идентифицира препратката:
#!/usr/bin/perl
use warnings;
use strict;
my @array = qw(hello world);
my %hash = (key => “data”);
print('ref(10) = ', ref(10), “\n”); #returns what?
print('ref(\10) = ', ref(\10), “\n”);
print('ref(\@array) = ', ref(\@array), “\n”);
print('ref(\%hash) = ', ref(\%hash), “\n”);
print('ref(\&function) = ', ref(\&function), “\n”);
print('ref(\\\@array) = ', ref(\\\@array), “\n”);
print('ref(\*hash) = ', ref(\*hash), “\n”);
##########
sub function {
print(“Hello, world!\n”);
}
Следващият по-сложен пример показва как може да се използва функцията ref, за да се определи действието в зависимост от вида на подадената препратка:
#!/usr/bin/perl
use warnings;
use strict;
my @array1 = qw(this is the first array);
my @array2 = qw(this is the second array);
my %hash = ( Tarzan => “Jane”,
Superman => “Lois Lane”,
Batman => “Catwoman”, );
my $array3 = [ “anonymous”, [“array”, “in”, “an”, “array”],
{ “plus” => “a”,
“hash” => “in”,
},
“as”, “well” ];
printStructures(5, \@array1, \%hash, \@array2, $array3);
#######
sub printStructures {
my $indent = shift;
foreach my $element(@_) {
unless (ref ($element)) {
print(' ' x $indent, $element, “\n”);
}
elsif (ref ($element) eq 'SCALAR') {
print(' ' x $indent, $element, “\n”);
}
elsif (ref ($element) eq 'ARRAY') {
foreach (0..$#$element) {
print(' ' x $indent, “[ $_ ]”);
if (ref ($element->[$_])) {
print(“\n”);
printStructures($indent + 3, $element->[$_]);
} else {
print(“$element->[$_]\n”);
}
}
}
elsif (ref ($element) eq 'HASH') {
foreach my $key (keys %$element) {
print (' ' x $indent, $key, ' => ');
if (ref ($element->{$key})) {
print(“\n”);
printStructures($indent + 3, $element->{$key});
} else {
print(“$element->{$key}\n”);
}
}
}
elsif (ref ($element) eq 'CODE') {
print(' ' x $indent, “CODE\n”);
}
elsif (ref ($element) eq 'GLOB') {
print(' ' x $indent, “GLOB\n”);
}
print(“\n”);
}
}
Масивите и хешовете са колекции от скалари. Всеки скалар може да е препратка към всеки друг тип данна, което ни позволява да създаваме комплексни структури от данни, ползвайки само основните. Следващата програма демонстрира как можем да създадем хеш от масиви, което е удобно за асоцииране на няколко стойности към един ключ. С помощта на препратките, можем да асоциираме ключа на хеша със препратка към масив, където да държим повече от една стойност:
#!/usr/bin/perl
use warnings;
use strict;
instructions();
my $choice = prompt();
my %hash;
while ($choice ne 'q') {
addElement() if ($choice eq 'a');
deleteElement() if ($choice eq 'd');
deleteKey() if ($choice eq 'k');
printAll() if ($choice eq 'p');
$choice = prompt();
}
#### SUBS ####
sub instructions
{
print <<DONE;
Enter 'a' to add an element.
Enter 'd' to delete an element.
Enter 'k' to delete a key.
Enter 'p' to print all elements.
Enter 'q' to quit.
DONE
}
sub prompt {
print(“? ”);
chomp(my $answer = <STDIN>);
return $answer;
}
sub addElement {
print(“What is the key you would like to add? ”);
chomp(my $key = <STDIN>);
print(“What is the value? ”);
chomp(my $value = <STDIN>);
push @{$hash{$key}}, $value;
}
sub deleteElement {
print(“What is the key of the element? ”);
chomp(my $key = <STDIN>);
print(“What is the value of the element? ”);
chomp(my $value = <STDIN>);
for ( 0 .. $#{$hash{$key}} ) {
if ($hash{$key}[$_] eq $value) {
print(“Deleting element $hash{$key}[$_]\n”);
splice(@{$hash{$key}}, $_, 1);
return;
}
}
}
sub deleteKey {
print(“What key would you like to delete? ”);
chomp(my $key = <STDIN>);
delete($hash{$key});
}
sub printAll {
foreach (keys %hash) {
print(“ $_ => ”, join(', ', @{$hash{$_}}), “\n”);
}
}