Introduction à la programmation en Perl

Cinquième partie : références

© 2002 - Sylvain Lhullier
Paru dans LinuxMagazine-France en decembre 2002

Chapeau de l'article

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 ...

Introduction

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.

Références sur 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 :

 schéma de références

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

Usage des références sur scalaire

À 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.

 schéma de références

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 ;-)

Références sur tableau

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 :

 schéma de références

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.

Références sur table de hachage

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 :

 schéma de références

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";
}

Réflexions à propos des références

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 :

 schéma de références

Références anonymes vers scalaire

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";
 schéma de références

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.

Références anonymes vers tableau

Pour créer une référence anonyme vers un tableau, il faut utiliser la notation suivante : [élément1élément2élément3etc] 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) :

 schéma de références

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 :

 schéma de références

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" ];
 schéma de références

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].

Références anonymes vers table de hachage

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=>valeur1clef2=>valeur2etc} 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) :

 schéma de références

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' }
        };
 schéma de références

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

Références anonymes diverses

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 !) :

 schéma de références

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 :

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.

Références circulaires

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 :

 schéma de références

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 ...

Références sur fichiers

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.

Références sur fonctions

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 ...

Conclusion

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.