Portée (informatique)

Un article de Wikipédia, l'encyclopédie libre.

En informatique, la portée (scope en anglais) d'un identifiant est l'étendue au sein de laquelle cet identifiant est lié. Cette portée peut être lexicale ou dynamique.

Portée lexicale[modifier | modifier le code]

Une portée lexicale est définie par une portion du code source. Au sein de cette portion, l'identifiant n'a qu'une seule liaison.

Un identifiant à portée globale est lié dans l'ensemble du code source (parfois seulement après sa déclaration ou sa définition). Dans de nombreux langages de programmation, toutes les fonctions ont une portée globale (exemple : C). Lorsqu'un identifiant à portée globale désigne une variable, on parle alors de variable globale. Celles-ci servent typiquement à stocker des données utiles à de multiples endroits du programme, un cas particulier étant les verrous.

Un identifiant à portée locale n'est lié qu'au sein d'une construction syntaxique du langage, généralement celle où il est déclaré. Lorsqu'un identifiant à portée locale désigne une variable, on parle alors de variable locale. De plus, dans la plupart des langages, un identifiant à portée locale masque tout éventuel identifiant de même nom, mais de plus grande portée. Déclarer deux fois le même identifiant dans la même portée peut être considéré comme une erreur ou comme une redéclaration, selon le langage et le contexte.

Exemple en Scheme :

 REPL> (define foo 42)             ; la variable globale foo contient la valeur 42
 REPL> foo                         ; accès à la variable foo
 42
 REPL> (let ((foo -1))             ; ouverture d'une forme let où est définie une variable locale foo qui contient la valeur -1
         foo)                      ; fin de la forme let, qui renvoie la valeur de la variable foo
 -1
 REPL> foo                         ; accès à la variable foo
 42

Historique[modifier | modifier le code]

Le principe de portée lexicale a été introduit pour la première fois dans LISP 1.5. Il fut ajouté à Algol 60, dont les descendants sont typiquement à portées uniquement lexicales (C, Pascal). Scheme, qui en fut un promoteur, est un dialecte Lisp qui n'a également de portées que lexicales. Common Lisp, lui, dispose à la fois de portées lexicales, importées de Scheme, et de portées dynamiques.

Portée dynamique[modifier | modifier le code]

Une portée dynamique est définie dans une étendue dynamique délimitée par un point d'entrée et un point de sortie pendant l'exécution. La liaison d'un identifiant à portée dynamique masque une liaison précédente au sein de l'étendue dynamique de la forme qui réalise cette nouvelle liaison. Une variable à portée dynamique, appelée variable dynamique, permet donc de propager dans la pile d'appels une modification à un environnement.

Exemple en Common Lisp :

 CL-USER> (defvar *foo* 42)        ; définition d'une variable à portée dynamique *foo* contenant la valeur 42
 *FOO*
 CL-USER> (defun return-foo ()     ; définition d'une fonction renvoyant la valeur de *foo*
            *foo*)
 RETURN-FOO
 CL-USER> (return-foo)             ; accès à la variable *foo*
 42
 CL-USER> (let ((*foo* -1))        ; ouverture d'une forme let re-liant *foo* à la valeur -1,
            (return-foo))          ; dont l'exécution constitue l'étendue dynamique de cette liaison
 -1
 CL-USER> (return-foo)             ; accès à la variable *foo*
 42

Et le même exemple en Scheme, sans portée dynamique :

 REPL> (define foo 42)
 REPL> (define (return-foo)
         foo)
 REPL> (return-foo)
 42
 REPL> (let ((foo -1))
         (return-foo))             ; ici l'exécution de return-foo accèdera à la variable foo dans sa portée lexicale
 42
 REPL> (return-foo)
 42

Des langages non fonctionnels supportent aussi une portée dynamique, notamment ceux dérivés de Forth (un langage à pile) dont PostScript. La méthode employée est d'utiliser une seconde pile (indépendante de la pile des paramètres ou de la pile de retour des fonctions, les deux piles étant souvent communes) contenant pour chaque position empilée une référence à un dictionnaire de variables. Si une variable ne peut être trouvée dans le premier dictionnaire référencé au sommet de la pile, la recherche se poursuit dans le (ou les) dictionnaires plus bas dans la pile.

Chaque dictionnaire de variables est généralement modélisé par une table de hachage afin d'obtenir un temps de recherche optimal au sein de chaque dictionnaire (le temps de recherche devient quasiment indépendant du nombre de variables stockées dans le dictionnaire). Pour cela, le nom de la variable est converti en une clé de recherche numérique dont tous les bits d'information sont équidistribués à l'aide d'une fonction de hachage; cette clé numérique est alors réduite à l'intervalle de la taille de la table de hachage, pour obtenir l'emplacement où la variable est stockée. Comme des collisions sont possibles, la table de hachage contient à chaque position utilisée à la fois une entrée pour le nom de la variable (afin de pouvoir vérifier par égalité que la bonne variable est stockée à cet endroit). Il existe alors différentes stratégies pour résoudre la collision de variables partageant la même clé calculée :

  • Augmenter la taille de la table de hachage et redistribuer toutes les variables qui y sont déjà stockées. Cette opération est coûteuse lors de l'ajout de variable mais permet de limiter le nombre de collisions.
  • Enchaîner dans une liste les variables partageant la même position dans la table de hachage. La liste de collision peut être externe à la table, ou bien occuper des positions choisies aléatoirement parmi les positions libres dans la table de hachage.
  • Stocker, à la position déterminée par la clé de hachage commune, une autre variable telle qu'une référence à une autre table de hachage (contenant toutes les variables partageant la même clé dans la première table), la seconde table utilisant une fonction de hachage distincte (dans ce cas, le temps de recherche devient partiellement logarithmique en fonction du nombre total de variables, et non partiellement linéaire).

PostScript et Forth ont ceci de particulier que tous les dictionnaires (référencés dans la pile de portée) sont eux-mêmes des variables que l'on peut nommer, que l'on peut soit utiliser directement soit rechercher par leur nom recherché lui aussi dans la pile de portée, avant d'en obtenir la référence, qu'on peut alors empiler dans la pile de portée. Il n'y a donc dans ces langages aucun nom de variable réservé, tous les noms d'objets prédéfinis étant en fait référencés dans une pile de portée qui n'est jamais vide mais référence au minimum un premier dictionnaire "système" de portée contenant sa propre référence et son propre nom. De plus, il est possible de retirer sélectivement certaines variables de toutes les résolutions ultérieures de portée, en supprimant leur référence dans un des dictionnaires référencés dans la pile de portée (ceci permet de masquer certaines variables d'un niveau de portée donné pour utiliser ensuite la variable définie dans un niveau de portée inférieur). On peut également replacer une des entrées par une autre (la variable définie dans la portée initiale n'y est plus accessible mais sera remplacée par une autre de même nom, mais éventuellement de type différent, ceci pouvant rendre ensuite totalement inaccessible une variable système que l'on veut cacher).

La portée de noms n'est pas réservée aux seuls langages pour le stockage et le référencement de valeurs. On la retrouve aussi dans la plupart des systèmes de fichiers hiérarchiques pour présenter des vues différentes des noms de fichiers accessibles. Seuls les anciens systèmes de fichiers très simples, ne comportant qu'un seul dictionnaire de noms de fichiers pour le système tout entier (et tous les processus hébergés par le système), ne supportent qu'une seule portée pour les noms de fichiers (on peut toutefois les utiliser tels quels pour les étendre, en y stockant sous forme d'un fichier des dictionnaires de noms supplémentaires).

  • Par exemple sous Unix et Linux, les noms de fichiers sont résolus en objets fichiers accessibles au moyen d'une variable de portée (dans ce cas, les répertoires de fichiers sont des dictionnaires), et permettent aussi de référencer le même fichier sous des noms multiples et différents, y compris dans des répertoires multiples et différents. La résolution de portée utilise une variable globale spécifique au processus qui accède au fichier par son nom, modifiable par chroot et on référence implicitement ce dictionnaire « racine » en recherchant un nom commençant par "/".
  • Une autre variable contenant la référence à un dictionnaire courant supportant la notion de sous-répertoires et la navigation dynamique d'un niveau de portée à l'autre, à l'aide d'opérations comme chdir. Cette facilité de nommage est supportée par pratiquement tous les systèmes de fichiers des systèmes d'exploitation actuels.
  • Certains systèmes de fichiers permettent d'utiliser plusieurs dictionnaires racines simultanément en leur attribuant à chacun un nom; pour les systèmes de fichiers CPM, DOS ou Windows chaque racine est désignée par une lettre unique (désignant généralement un lecteur ou un volume contenant les fichiers référencés bien que ce ne soit pas obligatoire); pour chaque lecteur ainsi nommé existe un répertoire courant propre à chaque processus, et chaque processus est aussi associé à un lecteur courant.
  • D'autres systèmes (comme VMS) vont plus loin et ne se limitent pas à une seule lettre de lecteur, mais permettent de nommer une quantité virtuellement infinie de volumes courants, chacun par leur propre nom. Dans ce cas il existe dans chaque processus un dictionnaire des volumes accessibles, dans lequel on trouve soit des fichiers, soit d'autres répertoires, lesquels peuvent être mémorisés dans le contexte d'un processus au sein de son propre dictionnaire de volumes. Cela combine les avantages des deux méthodes de résolution dynamique de portée: hiérarchique, ou contextuelle.