octobre
2024
Une notion importante de programmation, du moins si vous vous intéressez à comment ça marche, est celle de référence.
Pour l’ordinateur une variable a deux attributs: sa valeur (on peut y attacher une taille, un type…) et son adresse en mémoire. Pourquoi l’adresse en mémoire? Eh bien, c’est simplement l’information qu’il lui faut pour accéder à la variable quand elle est en mémoire vive.
Du coup, un programme tend à manipuler deux types de données. Il y a les variables qui représentent des valeurs comme des caractères ou des nombres, et les variables qui contiennent… l’adresse d’une autre variable. Nous appelons ces dernières des « pointeurs » ou des « références », des termes qui ne sont pas historiquement équivalents mais en pratique ils désignent le même objet.
Cette longue introduction a pour but de vous familiariser avec la notion de référence. Je voudrais maintenant passer au sujet principal: l’appartenance.
Le language Rust
Si vous suivez les fils d’information sur la programmation vous avez certainement, à un moment ou à un autre, entendu parler de Rust. En effet cette création de la fondation Mozilla a une profonde influence sur les derniers développements de systèmes d’exploitation et de certains logiciels, avec Microsoft et Linux qui s’y investissent.
La nouveauté de ce language est qu’il parvient à gérer la mémoire de façon sûre sans compromettre la performance. Et cela tient à son utilisation du concept d’appartenance pour les références.
Pour Rust, toute référence a une appartenance: une fonction ou un bout de code qui gère la variable pointée par la référence, comment et quand elle peut être modifiée ou désallouée. Le compilateur fait appel au fameux Borrow Checker (contrôleur d’appartenance) pour s’assurer que le code suit bien les règles définies par le language.
Je ne vais pas m’étendre ici sur ces règles, si ce n’est qu’elles inspirent directement la suite.
Références en dodo
Le language dodo a deux types de références, qui sont complètement différents (à la surface).
Il y a les références simples qui apparaissent dans les paramètres des méthodes comme dans cet exemple:
method Mixe(&Fruit[], Vitesse?)
La liste de fruits est passée par référence (marque &) pour pouvoir être modifiée à l’intérieur de la méthode. En effet connaître l’adresse d’une variable permet de la modifier de façon visible à l’extérieur. En contraste, la vitesse n’est pas une référence donc la méthode n’a accès qu’à sa valeur.
Mais c’est bien la limitation de ce type de référence: elle ne peut être utilisée que dans les paramètres d’une méthode ou d’un constructeur, pas dans les paramètres d’une fonction ou comme type d’un champ dans un objet.
Le deuxième type de référence est appelé Link (lien). Il s’apparente peut-être plus au pointeur traditionnel.
A la différence de la référence simple, Link peut être passé comme argument d’une fonction et stocké dans un objet. Cela le rend plus versatile mais aussi plus compliqué en terme de gestion sûre de la mémoire.
Gestion sûre de la mémoire avec Link
A sa création une variable Link pointe vers une adresse mémoire invalide (le fameux pointeur null que vous avez peut-être déjà rencontré). Il n’y a donc pas de variable à modifier ou désallouer à cette adresse. Son état à ce point est null (vide).
Quand on lui donne une valeur deux choses se passent:
- une variable est allouée en mémoire vive pour stocker la valeur
- la référence Link pointe vers cette variable et prend l’état owned (variable lui appartenant)
La variable appartient effectivement au code qui l’a créée en mémoire. Il n’y a pas d’autre code qui a accès à la variable.
*Age _âge # lien initialisé avec null
*_âge = 20 # variable allouée
A ce point la variable peut simplement être désallouée si le code s’arrête ici, ou recevoir une nouvelle valeur. Il n’y a pas vraiment de différence avec une variable ordinaire.
Elle peut aussi être passée à une autre fonction.
Il y a quatre façons de passer la variable à une autre fonction:
- de façon définitive: l’appartenance de la variable change, la fonction appelée en prend possession
- comme un lien passif, pour observation seulement
- temporaire, la variable peut être modifiée par l’appel mais l’appelant en retient l’appartenance
- finalement comme une référence simple si c’est une méthode ou un constructeur
Dans le premier cas l’appelant n’a plus rien à voir avec la variable et le lien reprend l’état null quand l’appel finit. Il n’y a pas de complication par rapport à l’utilisation de la mémoire puisque l’appelé en est responsable.
Dans le deuxième cas, l’état du lien passif reçu par la fonction est listener (variable suivie). L’appelé peut simplement consulter la variable sans la modifier.
La variable reprend son état initial quand l’appel finit.
Dans le troisième cas l’état du lien reçu par la fonction appelée est borrowed (variable prêtée). La fonction a la permission de modifier la variable. Elle peut aussi consulter la valeur de la variable ou la prêter à une autre fonction. Mais ça s’arrête ici.
En particulier, la fonction appelée ne peut pas désallouer la variable. Comme avant, la variable reprend son état initial quand l’appel finit.
method Remplace(&*Age)
Avec une référence simple le lien lui-même peut changer, alors qu’avec le prêt c’est juste la variable.
- Si le lien est actif, l’appartenance de la variable revient à l’appelant mais la variable originelle peut être désallouée par la méthode.
- Si le lien est passif le lien sortant doit avoir au moins la même durée de vie que le lien entrant (voir la prochaine section).
Référence Link comme attribut d’objet
Jusqu’ici nous n’avons vu que des références locales, ce qui signifie que la référence Link ne s’échappe pas du bloc d’instructions courant (fonction, méthode…)
A partir d’ici nous allons parler de références en état de partage (shared).
Si le lien est stocké comme un lien actif dans l’attribut d’un objet, cet objet devient le propriétaire de la variable (owned). Elle peut être consultée ou prêtée tant que l’objet est défini. Mais elle ne peut changer de propriétaire que quand l’objet est désalloué, donc en valeur de retour à la sortie de la fonction. Cela est similaire à une variable prêtée (borrowed).
Si le lien est stocké comme un lien passif dans l’attribut de l’objet le propriétaire de la variable ne change pas. Il faut donc prendre garde que la durée de vie de l’objet n’excède pas celle du propriétaire. La variable ne peut pas changer de propriétaire tant que l’objet est défini, car le nouveau propriétaire pourrait la désallouer alors qu’il y a toujours une référence. C’est une variable partagée.
Pour retourner un tel objet depuis une fonction il faut définir une exigence sur sa durée de vie à travers l’attribut lifetime de son type, comme dans l’exemple ci-dessous:
template (param: $A, retour: $S) [where A = Age(lifetime: $l) && S = Suivi(lifetime: $l)]
def suivi(Personne, A) → S
Cela indique que la durée de vie de l’objet retourné doit correspondre à la durée de vie du paramètre de type Age. Si l’âge passé en paramètre est stocké dans la valeur de retour de la fonction, le compilateur ne va pas produire d’erreur car il y a une garantie que l’âge sera défini pour toute la durée de vie de l’objet.
Une durée de vie peut avoir une valeur static. Cela indique qu’elle est de durée indéfinie et signifie que l’objet va occuper de la mémoire jusqu’à la fin du programme.