Voici le cinquième article de notre série introductive sur la programmation Perl. Cette fois nous abordons le sujet des références. Les références sont les pointeurs de Perl, elles permettent de bâtir des structures complexes et composées : tableau de tableaux ou de tables de hachage et inversement, table de hachage de tableaux ou de tables de hachage ...
Le terme de référence en Perl correspond à peu près à celui de pointeur en C
et C++ et à celui de référence en Java.
Les habitués du C ou C++ noterons que les calculs sur références sont
interdits en Perl, ce qui permet d'éviter toutes sortes de problèmes
dus à des accès mémoire erronés (plus de Segmentation fault
).
Chaque variable, qu'elle soit scalaire, tableau ou table de hachage, est présente à une position donnée dans la mémoire. Une référence vers une variable est (schématiquement) l'adresse mémoire de cette variable. Une telle référence peut elle-même être stockée dans une variable scalaire.
L'opérateur qui permet de prendre la référence d'une variable
est l'anti-slash (\
) :
\$v
est la référence de la variable
$v
my $refv = \$v;
Ici la variable $refv
(on aurait pu choisir
un tout autre nom pour cette variable) est une référence vers
la variable $v
.
Une variable de type référence, quelque soit celui de la variable
qu'elle référence (scalaire, tableau ou table de hachage),
est un scalaire.
On peut représenter la relation entre ces deux variables de la façon
suivante :
La variable $refv
contient l'adresse
de la variable $v
(ainsi que l'information
consistant à savoir que $v
est une variable
scalaire). On dit que $refv
pointe vers $v
, car on
va pouvoir manipuler $v
(l'afficher, la modifier etc)
en utilisant $refv
; on représente
ce lien par une flèche de $refv
vers
$v
.
Il nous est alors possible de manipuler $v
au travers de $refv
. La notation
$$refv
(donc avec deux dollars) est équivalente
à $v
tant que $refv
pointe
vers $v
. Autrement dit, la notation
$$refv
équivaut à la variable scalaire pointée
par la référence $refv
. Dit de manière plus
prosaïque, on va accèder à la variable qu'il y a "au bout" de la référence
(au bout de la flèche du schéma).
On dit alors que l'on déréférence
la variable $refv
. Pour faire un parallèle avec
le langage C, il s'agit de l'équivalent de l'étoile (*
)
appliquée à un pointeur.
Revenons sur notre exemple. Nous déclarons une variable scalaire
$v
que nous initialisons. Nous déclarons
ensuite une autre variable scalaire $refv
à laquelle on affecte l'adresse de $v
;
$refv
est donc une référence sur
$v
:
my $v = -43.5; my $refv = \$v;
On affiche la valeur de la référence :
print "$refv\n"; # affiche SCALAR(0x80ff4f0)
On voit bien alors que la référence pointe vers une variable
de type scalaire (SCALAR
) dont
l'adresse mémoire est affichée en hexadécimal.
Affichons maintenant la variable pointée par $refv
(c'est-à-dire $v
ici) :
print "$$refv\n"; # affiche -43.5
L'affichage effectué est -43.5
(c'est-à-dire la valeur de $v
). Cette
notation $$refv
est équivalente
à $v
puisque $refv
est
une référence sur $v
.
Cette équivalence vaut aussi bien lorsque l'on a besoin de la
valeur de $v
(affichage etc) que lorsque
l'on veut affecter une nouvelle valeur à $v
:
$$refv = 56; # affecte 56 à $v
On affecte 56 à $$refv
, c'est-à-dire à
$v
. Si on affiche cette variable, on
voit bien qu'elle contient cette nouvelle valeur :
print "$$refv\n"; # affiche 56 print "$v\n"; # affiche 56
À quoi peut bien servir une référence sur un scalaire ? Par exemple à le modifier dans une fonction :
sub f { my ($ref) = @_; $$ref = 0; }
Cette fonction prend en argument une référence et affecte 0 à la variable scalaire pointée par cette référence. On pourrait l'utiliser ainsi :
f( $refv );
Ce qui aurait pour effet de mettre la variable $v
à la valeur 0. On pourrait directement écrire :
f( \$v );
Voici un autre exemple simple d'utilisation des références :
sub f2 { my $w = 43; return \$w; }
Cette fonction f2
déclare une variable locale
$w
et renvoie une référence vers cette variable.
Contrairement à ce qu'il se passe en C, ceci est tout à fait légal
et sans risque en Perl. Voici comment utiliser cette fonction :
my $reff = f2();
La variable scalaire $reff
devient donc
une référence vers une variable scalaire valant 43.
Cette variable scalaire valant 43 est l'ancienne variable
$w
de la fonction f2
.
La variable $reff
pointe donc vers une variable
qui n'a plus de nom : dans f2
elle s'appelait
$w
, mais en dehors de l'appel à cette fonction
qui l'a créée, elle n'a plus de nom.
En temps normal, une variable locale à une fonction est détruite lorsque l'on sort de la fonction. Mais tant qu'il existe une référence vers la variable, elle est conservée en mémoire. C'est le garbage-collector (ramasse-miette ou glaneur de cellules) qui la libérera lorsque plus aucune référence sur la variable n'existera.
Il faut noter que lors d'un prochain appel à la fonction
f2
, une autre variable $w
sera créée indépendante de la première ; il n'y aura donc
pas d'effet de bord sur la première référence renvoyée.
Nous sommes ici en présence d'une fonction qui peut faire office
de générateur de références sur scalaire ;-)
Il est possible de créer une référence sur un tableau.
L'opérateur qui permet cela est le même que pour les scalaires ;
il s'agit de l'anti-slash (\
) appliqué à une
variable de type tableau :
my @t = (23, "ab", -54.4); my $reft = \@t;
La variable scalaire $reft
est donc une référence
vers le tableau @t
:
Pour déréférencer une telle référence, il convient d'utiliser
une arobase (@
) : @$reft
est équivalent à @t
. On peut ainsi utiliser
la valeur de @t
en utilisant
$reft
:
my @t2 = @$reft; foreach my $e (@$reft) { .... }
On peut aussi modifier @t
de la sorte :
@$reft = (654.7, -9, "bonjour");
Si, pour accéder au ième élément de @t
,
il était possible d'écrire $t[
i]
,
il est maintenant possible d'écrire
$$reft[
i]
.
On peut alors dire, de manière schématique et pour fixer les choses,
que la notation $reft
est équivalente
au nom t
de la variable dans toute les syntaxes
utilisant ce tableau (partout où l'on peut écrire t
,
on peut écrire $reft
à la place, tant que
$reft
pointe sur @t
).
Voici, en effet, un récapitulatif des équivalences de notations :
Tableau | Référence |
t |
$reft |
@t |
@$reft |
$t[i] |
$$reft[i] |
$reft->[i] |
Cette dernière notation $reft->[i]
est équivalente
à $$reft[i]
et correspond donc au ième élément
du tableau référencé par $reft
. C'est la notation
la plus souvent utilisée pour cela ; elle rappelle la même
notation flèche (->
) du langage C.
L'expression
$reft->[1] = "coucou";
affecte donc à l'élément d'indice 1 du tableau pointé par
$reft
une nouvelle valeur.
De la même façon que pour les scalaires et pour les tableaux,
la création d'une référence vers une table de hachage utilise
l'opérateur anti-slash (\
) :
my %h = ( 'Paul' => 21, 'Julie' => 19 ); my $refh = \%h;
La variable scalaire $refh
est donc une référence
vers la table de hachage %h
:
Pour déréférencer une référence vers une table de hachage, il faut
utiliser le caractère pourcentage (%
) ;
%$refh
est équivalent à %h
:
my %h2 = %$refh; foreach my $k (keys %$refh) { ... }
Les deux notations suivantes permettent d'accéder à la valeur associée
à la clef Paul
:
$$refh{Paul}
et $refh->{Paul}
sachant que la seconde est la plus utilisée pour des raisons
identiques aux cas des tableaux.
$refh->{Jacques} = 33;
Voici un récapitulatif des équivalences :
Hash | Référence |
h |
$refh |
%h |
%$refh |
$h{Paul} |
$$refh{Paul} |
$refh->{Paul} |
Voici une façon d'afficher tous les couples clef/valeur de la table de
hachage référencée par $refh
:
foreach my $k (keys %$refh) { print "$k $refh->{$k}\n"; }
On a vu que pour déréférencer une référence vers un scalaire
on utilise le caractère dollar ($
), vers un tableau
le caractère arobase (@
) et vers une table de hachage
le caractère pourcentage (%
).
Mais que se passerait-il s'il nous venait à l'idée de ne pas choisir
le bon caractère de déréférencement, par exemple d'utiliser le dollar
pour une référence sur tableau ou bien le pourcentage pour une référence
sur scalaire ? N'arriverons-nous pas à des incohérences
comme en C ?
La réponse est non, car l'interpréteur veille au grain. Il refusera de considérer comme un tableau une variable scalaire par exemple. En cas d'incompatibilité de type, un de ces trois messages sera affiché lors de l'exécution et le programme prendra fin :
Not a SCALAR reference at script.pl line 23. Not an ARRAY reference at script.pl line 23. Not a HASH reference at script.pl line 23.
Comme une référence peut pointer vers n'importe quel type de structure (scalaire, tableau, table de hachage), cette vérification ne peut avoir lieu qu'au moment de l'exécution.
Les habitués du langage C seront invités à se mettre une bonne fois pour toute dans la tête :-) qu'il n'y a pas d'arithmétique possible sur les références en Perl. Ajouter 1 à une référence ne correspond pas à pointer vers l'élément d'indice 1 d'un tableau, mais à faire perdre le caractère référence à la variable (elle devient un scalaire comme un autre) :
my $v = 45; my $r = \$v; print "$r\n"; # affiche SCALAR(0x80fd4c4) $r++; print "$r\n"; # affiche 135255237
On voit bien ici que la variable $r
perd son
caractère spécifique de référence ; elle a toujours pour valeur l'adresse
de la variable (ici incrémentée de 1 : 80fd4c4 est la représentation
hexadécimale du nombre 135255236), mais n'est plus une référence,
il sera donc impossible de la déréférencer.
Notez aussi qu'une référence peut changer de valeur (c'est-à-dire de variable pointée) :
my $v = 45; my @t = ("ee",-2); my $r = \$v; $$r = -32; # modification de $v $r = \@t; $r->[0] = 28; # modification de $t[0]
Dernière chose importante, si vous passez une référence à une fonction, vous devez bien voir que la copie de la seule référence est effectuée, la structure pointée par la référence ne l'est pas ; vous pourrez donc la modifier dans la fonction :
sub f3 { my ($reftab) = @_; $reftab->[2] = 32.3; } my @t = ( 49, "hello", -2 ); my $r = \@t; f3( $r ); f3( \@t ); # équivalent # @t est modifié
On peut schématiser ce qu'il se passe de la manière suivante :
Une référence anonyme est une référence vers une variable qui n'a pas
de nom. Nous avons déjà vu comment faire cela pour un scalaire
avec une fonction (fonction f2
un peu avant),
mais cela n'est pas la façon de faire la plus simple.
La syntaxe pour créer directement une référence anonyme vers un scalaire
est la suivante : \
valeur-constante
my $ref1 = \34; my $ref2 = \"er"; print "$$ref1 $$ref2\n";
Une telle valeur est une constante, il est impossible de modifier
la valeur pointée par la référence (différence avec le mécanisme de la
fonction f2
) :
$$ref1 = "hello"; Modification of a read-only value attempted at script.pl line 24.
Ce n'est pas dans le cas des scalaires que les références anonymes sont les plus utilisées ; elles le sont bien plus avec les tableaux et les tables de hachage.
Pour créer une référence anonyme vers un tableau, il faut utiliser la
notation suivante :
[
élément1,
élément2,
élément3,
etc]
est une référence vers un tableau comportant les éléments en question.
my $r = [ 34.4, "ac", -71 ]; my @t = ( 34.4, "ac", -71 );
La variable $r
est une référence vers un tableau
comportant trois éléments alors que la variable @t
est un tableau à trois éléments (cette dernière notation nous est déjà
familière) :
On accède aux éléments de cette référence anonyme comme pour une référence normale :
print "$r->[0]\n"; $r->[2] = 901; foreach my $e (@$r) { ... }
Attention à ne pas écrire \(2,"er",$v)
si vous voulez
créer une référence vers un tableau, car cette syntaxe fait toute autre chose :
elle est en fait équivalente à (\2,\"er",\$v)
, donc
à une liste de références, ce qui est fort différent.
Munis des références, il va maintenant nous être possible de créer des tableaux de tableaux. Cela était pour le moment impossible en raison de l'aplatissement des listes (cf article numéro 2). Une référence étant un scalaire, il va nous être possible de stocker une référence comme valeur dans un tableau :
my @t1 = ( 16, -33 ); my @t2 = ( "el", 0.3, 4 ); my @t = ( 6, \@t1, \@t2, "s" );
Le tableau @t
comporte donc un premier élément
valant 6, un deuxième étant une référence vers un tableau à deux éléments,
un troisième une référence vers un tableau à trois éléments
et enfin un élément valant la chaîne "s"
.
On écrit cela de façon plus simple avec des références anonymes :
my @t = ( 6, [ 16, -33 ], [ "el", 0.3, 4 ], "s" );
Ce qui peut se représenter sous la forme suivante :
Vous noterez bien que la syntaxe suivante correspond à la création d'un tableau à sept éléments (aplatissement des listes) :
my @t2 = ( 6, ( 16, -33 ), ( "el", 0.3, 4 ), "s" );
On peut pousser le vice jusqu'à utiliser une référence pour le premier tableau :
my $r = [ 6, [ 16, -33 ], [ "el", 0.3, 4 ], "s" ];
Comment accéder aux éléments des différentes profondeurs d'une telle construction ? Il suffit de suivre les références ...
print "$r->[0]\n"; # affiche 6 # $r->[1] est une référence vers tableau print "$r->[1]->[0]\n"; # affiche 16 print "$r->[1]->[1]\n"; # affiche -33 # $r->[2] est une référence vers tableau print "$r->[2]->[0]\n"; # affiche el print "$r->[2]->[1]\n"; # affiche 0.3 print "$r->[2]->[2]\n"; # affiche 4 print "$r->[3]\n"; # affiche s
Ce n'est pas si complexe qu'il n'y paraît pour peu que nous
ayons un bon schéma sous la main ...
Vous noterez que nous faisons usage de l'opérateur flèche (->
)
plutôt que de la syntaxe double-dollar ($$
).
Si $r->[1]
est une référence vers tableau,
@{$r->[1]}
est le tableau en question.
On peut donc écrire :
foreach my $e ( @{$r->[1]} ) { ... }
Il faut noter qu'en cas de déréférencements successifs, seule
la première flèche est nécessaire :
$r->[2][1]
est équivalent à
$r->[2]->[1]
.
De la même façon il est possible de créer une référence anonyme
vers une table de hachage. La notation
{
clef1=>
valeur1,
clef2=>
valeur2,
etc}
est une référence vers une table de hachage comportant les couples
clef/valeur en question.
my $r = { 'Paul' => 21, 'Julie' => "e" }; my %h = ( 'Paul' => 21, 'Julie' => "e" );
La variable $r
est une référence vers une table
de hachage alors que la variable %h
est une table
de hachage (notation familière) :
De la même façon qu'avec des tableaux, nous allons mettre sur pied des tables de hachage de tables de hachage :
my %h1 = ( 'rue' => 'Pasteur', 'tel' => '06461341' ); my %h2 = ( 'rue' => 'Jaures', 'tel' => '03729103' ); my $r = { 'Paul' => \%h1, 'Julie' => \%h2 };
Ou plus directement :
my $r = { 'Paul' => { 'rue' => 'Pasteur', 'tel' => '06461341' }, 'Julie' => { 'rue' => 'Jaures', 'tel' => '03729103' } };
Voici comment accéder à tous les éléments d'une telle construction :
# $r->{Paul} est une référence vers une table de hachage print "$r->{Paul}->{tel}\n"; # affiche 06461341 print "$r->{Paul}{tel}\n"; # équivalent print "$r->{Paul}{rue}\n"; # affiche Pasteur # $r->{Julie} est une référence vers une table de hachage print "$r->{Julie}{tel}\n"; # affiche 03729103 print "$r->{Julie}{rue}\n"; # affiche Jaures
Il est tout à fait possible de mélanger les références vers les tableaux et vers les tables de hachages :
my $r = [ ['a',4], 'b', [1,'z'], {'P'=>[-2,"er",0],'A'=>7}, 8 ];
Voici le schéma correspondant à cette structure (essayer de vous en convaincre !) :
Voici comment accéder à un élément d'une telle structure :
print "$r->[3]->{P}->[1]\n"; # affiche "er" print "$r->[3]{P}[1]\n"; # équivalent
Les crochets correspondent à une prise d'indice d'un tableau ; les accolades à la clef d'une table de hachage.
Je peux parcourir le premier tableau du deuxième niveau (celui qui
comporte a
et 4
)
de la manière suivante :
my $reft = $r->[0]; foreach my $v (@$reft) { print "$v\n; }
Je crée une variable $reft
qui est une référence
vers ce tableau, je peux ensuite parcourir @$reft
qui représente le tableau en question.
Il est possible d'écrire cela sans créer de variable temporaire,
la syntaxe est la suivante :
@{
référence}
Ces accolades n'ont rien à voir avec les tables de hachage
elles permettent juste de délimiter la référence à laquelle on
applique l'arobase. Voici ce que cela donne :
foreach my $v (@{$r->[0]}) { print "$v\n"; }
On fait de la même façon pour un table de hachage, les accolades délimitent la référence à laquelle on applique le pourcentage :
foreach my $k (keys %{$r->[3]}) { print "$k $r->[3]{$k}\n"; }
La question qui pourrait maintenant venir à l'esprit est la suivante :
comment pourrait-on écrire une boucle sur les éléments du tableau
référencé par $r
et surtout comment savoir
de quel type ils sont pour pouvoir les utiliser ?
Pour répondre à cette question, voici un nouvel opérateur :
ref()
. Il permet de connaître le type d'une référence.
Cette fonction renvoie :
"SCALAR"
si son argument est une référence sur scalaire,
"ARRAY"
si son argument est une référence sur tableau,
"HASH"
si son argument est une référence
sur table de hachage,
faux si son argument n'est pas une référence (c'est un scalaire classique).
On peut alors écrire le code suivant :
foreach my $p (@$r) { if( ref($p) eq "ARRAY" ) { print "( "; foreach my $v (@$p) { print "$v "; } print ")\n"; } elsif( ref($p) eq "HASH" ) { foreach my $k (keys(%$p)) { print "$k : $p->{$k}\n"; } } elsif( !ref($p) ) { print "$p\n"; } }
L'affichage suivant est effectué :
( a 4 ) b ( 1 z ) P : ARRAY(0x8100c20) A : 7 8
Dans cet exemple, un seul premier niveau de références est exploré.
Pour aller au-delà, c'est-à-dire, afficher le tableau associé à
la clef P
, il faudrait concevoir un ensemble
de fonctions s'appelant les unes les autres en fonction du type
des références rencontrées. Ne vous fatiguez pas à les écrire,
il existe déjà une telle fonctionnalité en Perl :
use Data::Dumper; print Dumper($r);
La première ligne ajoute des fonctions à Perl (c'est un peu
le #include
du langage C) et ne doit donc être
présente qu'une seule fois dans le programme (plutôt au début).
La seconde consiste en l'affichage de la valeur de retour
de la fonction Dumper
: cette fonction
renvoie une (longue) chaîne de caractères représentant toute
la structure référencée par $r
:
$VAR1 = [ [ 'a', 4 ], 'b', [ 1, 'z' ], { 'P' => [ -2, 'er', 0 ], 'A' => 7 }, 8 ];
(L'affichage a été condensé pour des raisons de place dans l'article.) Vous noterez que l'affichage effectué est directement intégrable dans un code Perl.
La notion de référence circulaire n'a rien de compliqué ; c'est juste le fait que plusieurs tableaux et/ou tables de hachage et/ou scalaires peuvent se référencer entre elles. Par exemple :
On voit bien alors qu'un cycle de références existe entre ces références. Voici comment écrire cela en Perl :
my $r = [ 71, { "Hello" => -34.7, "Ptt" => { "R5" => "As4" } } ]; $r->[1]{Ptt}{Od} = $r;
On comprend bien qu'il n'est pas possible de créer une telle structure
en une seule instruction. Comment se comporte Data::Dumper
dans une telle situation ? Ne va-t-il pas boucler à l'infini ?
Et bien non : il se comporte bien :
print Dumper($r); $VAR1 = [ 71, { 'Ptt' => { 'R5' => 'As4', 'Od' => $VAR1 }, 'Hello' => '-34.7' } ];
Data::Dumper
se rend compte qu'il passe sur une
référence qu'il à déjà parcouru et l'affiche comme telle
($VAR1
).
Pourquoi vous parler de références circulaires ? Premièrement parce qu'il faut savoir qu'il est possible d'en faire (cela peut être utile de créer des listes chaînées circulaires etc). Mais surtout parce qu'elles posent des problèmes au garbage-collector dans sa tâche de libération de la mémoire. À la suite à l'exemple précédent, je pourrais écrire :
$r = undef;
En temps normal, tout ce que $r
référençait
serait libéré. Mais ici, ce n'est pas le cas. En effet, chacun
des tableaux et des tables de hachage ont encore au moins une référence
vers elle, le garbage-collector ne se rend pas compte, qu'en fait,
les trois objets peuvent être libérés. Nous sommes donc en présence de
zones mémoires inaccessibles (aucune variable ne nous permet
d'y accéder) et non libérables : cette mémoire est perdue
pour le programme ! Elle sera bien sûr libérée quand le programme
prendra fin, mais s'il s'agit d'un démon qui tourne en permanence,
cette fuite mémoire n'est pas forcément à négliger.
La solution pour éviter cela est de "casser" la circularité avant de modifier
la valeur de la variable $r
:
$r->[1] = undef;
Je viens de casser le lien indiqué par un astérisque, il n'y a plus de boucle dans les références. Maintenant et seulement maintenant, je puis sans risque écrire :
$r = undef;
Et la mémoire sera libérée ...
Une ouverture de fichier crée une variable dont il est possible de prendre l'adresse. Voici la syntaxe pour cela :
open(FILE,">toto") or die("$!"); my $reff = \*FILE;
La variable scalaire $reff
est une référence vers
le descripteur de fichier FILE
. Il nous est aussi
possible de créer une référence vers un des trois fichiers pré-existant :
my $refo = \*STDOUT;
Voici des exemples d'utilisation de ces références :
open(FILE,">toto") or die("$!"); my $reff = \*FILE; print $reff "ok\n"; sub affiche { my ($ref) = @_; print $ref "ok\n"; } affiche( $reff ); affiche( \*FILE ); # équivalent close( $reff );
Cette dernière ligne est équivalente à close(FILE);
On peut sans restrictions stocker une référence vers fichier dans une
table de hachage ou dans un tableau.
Comme dans le langage C, une fonction a elle-aussi une adresse mémoire. Cette adresse correspond à l'endroit de la mémoire où le code de la fonction est stocké. On peut obtenir une référence vers une fonction de la manière suivante :
sub affcoucou { my ($p) = @_; print "Coucou $p\n"; } my $ref = \&affcoucou;
La variable scalaire $ref
est une référence vers
la fonction affcoucou
. Elle peut s'utiliser des
façons suivantes :
&$ref("Larry"); # appel $ref->("Larry"); # équivalent sub f { my ($f,$p) = @_; $f->( $p ); } f( $ref, "Larry" ); f( \&affcoucou, "Larry" ); # équivalent
Notez bien qu'il est tout à fait possible de stocker une telle référence dans un tableau ou dans une table de hachage ...
De telles structures nous donnent envie de faire de la programmation objet (champs et méthodes pourraient être stockées dans une table de hachage). N'allons pas trop vite car Perl a été prévu pour la programmation objet et propose les fonctionnalités nécessaires (héritage ...) en se basant sur les modules.
Dans la suite, nous aborderons le sujet des modules (package), comment les utiliser et comment en écrire un. Nous glisserons ensuite tout naturellement vers la programmation objet.