47. Durées de vie et opérations fondamentales▲
Nous verrons bientôt les structures, la fonctionnalité de base qui permet au programmeur de définir des types spécifiques à une application. Les structures sont faites pour combiner des types fondamentaux et d'autres structures pour définir des types de plus haut niveau qui se comportent selon des besoins spécifiques des programmes. Après les structures, nous verrons les classes, qui sont la base de la programmation orientée objet en D.
Avant de parler de structures et de classes, nous allons d'abord aborder des notions importantes. Ces notions aideront à comprendre les structures, les classes et certaines de leurs différences.
Nous avons appelé toute donnée qui représente une idée dans un programme une variable. À certains endroits, nous avons nommé des variables de type structure ou classe des objets. On va continuer à les appeler des variables dans ce chapitre.
Même si ce chapitre n'aborde que les types fondamentaux, les tranches et les tableaux associatifs, ces notions s'appliquent également aux types définis par l'utilisateur.
47-1. Durée de vie d'une variable▲
Le temps écoulé entre la définition d'une variable et sa finalisation est sa durée de vie. Même si c'est le cas pour beaucoup de types, devenir indisponible et être finalisé ne se passent pas nécessairement en même temps.
Si vous vous souvenez du chapitre sur les espaces de nomsEspace de nom (name space), vous vous rappelez de la manière dont les variables deviennent indisponibles. Dans les cas simples, quitter la portée dans laquelle une variable a été définie rend cette variable indisponible.
Considérons l'exemple suivant comme un rappel :
void
testVitesse
(
)
{
int
vitesse; // Variable unique ...
foreach
(
i; 0
.. 10
) {
vitesse =
100
+
i; // ... qui prend 10 valeurs différentes.
// ...
}
}
// ← 'vitesse' est indisponible après ce point.
La vie de la variable vitesse dans ce code prend fin lorsque l'on sort de la fonction testVitesse. Il y a une seule variable dans le code suivant et elle prend 10 valeurs différentes, de 100 à 109.
En ce qui concerne la durée de vie des variables, le code suivant est très différent comparé au précédent :
void
testVitesse
(
)
{
foreach
(
i; 0
.. 10
) {
int
vitesse =
100
+
i; // Dix variables différentes.
// ...
}
// ← La vie de chacune des variables prend fin ici.
}
Il y a 10 variables différentes dans ce code, chacune prenant une valeur unique. À chaque itération de la boucle, une nouvelle variable commence sa vie, qui prend fin à la fin de chaque itération.
47-2. Durée de vie d'un paramètre▲
La durée de vie d'un paramètre dépend de ses qualificateurs :
- ref : le paramètre est seulement un alias de la variable qui est indiquée lors de l'appel de la fonction. Les paramètres refs n'affectent pas la durée de vie des variables ;
- in : pour les types valeur, la vie d'un paramètre commence en entrant dans la fonction et prend fin lorsque l'on en sort. Pour les types référence, la vie du paramètre est la même qu'avec ref ;
- out : pareil que pour ref, le paramètre n'est qu'un alias de la variable qui est indiquée lors de l'appel de la fonction. La seule différence est que la variable est mise à sa valeur .init automatiquement en entrant dans la fonction ;
- lazy : la vie du paramètre commence quand le paramètre est utilisé et prend aussitôt fin.
L'exemple suivant utilise ces quatre types de paramètres et explique leur durée de vie dans les commentaires :
void
main
(
)
{
int
main_in; /* La valeur de main_in est copiée lors
* de l'appel. */
int
main_ref; /* main_ref est passé à la fonction tel quel,
* sans copie. */
int
main_out; /* main_out est passé à la fonction tel quel.
* Sa valeur prend celle de int.init
* en entrant dans la fonction. */
foo
(
main_in, main_ref, main_out, unCalcul
(
));
}
void
foo
(
in
int
p_in, /* La vie de p_in débute lors
* de l'entrée dans la fonction et
* finit en quittant la fonction. */
ref int
p_ref, /* p_ref est un alias de main_ref. */
out
int
p_out, /* p_out est un alias de main_out. Sa
* valeur prend celle de int.init lors
* de l'entrée dans la fonction. */
lazy int
p_lazy) /* La vie de p_lazy commence lors de
* son utilisation et prend fin lorsque
* son utilisation prend fin. Sa valeur
* est recalculée en appelant unCalcul()
* à chaque fois que p_lazy est utilisé
* dans la fonction. */
{
// ...
}
int
unCalcul
(
)
{
int
resultat;
// ...
return
resultat;
}
47-3. Opérations fondamentales▲
Indépendamment de son type, il y a trois opérations fondamentales au cours de la vie d'une variable :
- initialisation : le début de sa vie ;
- finalisation : la fin de sa vie ;
- affectation : changer sa valeur.
Pour être considérée comme un objet, elle doit d'abord être initialisée. Il peut y avoir des opérations finales pour certains types. La valeur d'une variable peut changer au cours de sa vie.
47-3-1. Initialisation▲
Toute variable doit être initialisée avant d'être utilisée. L'initialisation implique deux étapes :
- réservation de l'espace pour la variable : cet espace est l'endroit dans lequel la valeur de la variable est stockée en mémoire ;
- construction : fixer la première valeur de la variable dans cet espace (ou les premières valeurs des membres des structures et des classes).
Toute variable vit à un endroit en mémoire qui lui est réservé. Une partie du code que le compilateur génère sert à réserver de l'espace pour chaque variable.
Considérons la variable suivante :
int
vitesse =
123
;
Le nombre d'octets que la variable vitesse prend dans la mémoire du programme est la taille d'un int. Si on voit la mémoire comme un ruban allant de gauche à droite, on peut imaginer la variable vivant quelque part dans ce ruban :
|
L'endroit dans lequel la variable est placée dans la mémoire a une adresse. En un sens, la variable vit à cette adresse. Quand la valeur d'une variable est changée, la nouvelle valeur est stockée à la même place :
++
vitesse;
La nouvelle valeur se retrouve au même endroit, là où l'ancienne valeur était stockée :
|
La construction est nécessaire pour préparer l'utilisation des variables. Comme une variable ne peut pas être utilisée de façon fiable avant d'avoir été construite, ceci est fait par le compilateur automatiquement.
Les variables peuvent être construites de trois manières :
- par leur valeur par défaut : quand le programmeur ne spécifie pas une valeur explicite ;
- en copiant : quand la variable est construite comme étant la copie d'une variable du même type ;
- par une valeur spécifique : quand le programmeur spécifie une valeur explicitement.
Quand une valeur n'est pas spécifiée, la valeur de la variable est la valeur par défaut de son type, c.-à-d. sa valeur .init.
int
vitesse;
La valeur de vitesse est int . init, qui se trouve être zéro. Naturellement, une variable qui est construite par sa valeur par défaut peut prendre d'autres valeurs durant sa vie (sauf si elle est immutable).
File fichier;
Avec la définition précédente, la variable fichier est un objet File qui n'a pas encore été associée à un fichier du système de fichiers. Elle reste inutilisable jusqu'à ce qu'elle soit modifiée pour être associée à un fichier.
Les variables sont parfois construites comme des copies d'une autre variable :
int
vitesse =
autreVitesse;
vitesse est ici construite par la valeur de autreVitesse.
Comme nous le verrons dans des chapitres ultérieurs, cette opération a une signification différente pour les variables de type classe :
auto
variableClasse =
autreVariableClasse;
Même si variableClasse commence sa vie comme une copie de variableAutreClasse, il y a une différence fondamentale avec les classes : bien que vitesse et autreVitesse soient des valeurs distinctes, variableClasse et variableAutreClasse donnent toutes deux accès à la même valeur. C'est la différence fondamentale entre les types valeurs et les types références. Nous verrons cela dans le chapitre suivant.
Enfin, les variables peuvent être construites par la valeur d'une expression d'un type compatible :
int
vitesse =
unCalcul
(
);
vitesse est ici construite avec la valeur de retour de la fonction unCalcul.
47-3-2. Finalisation▲
La finalisation est l'ensemble des opérations finales qui sont exécutées pour une variable ainsi que la libération de la mémoire qu'elle occupait :
- destruction : les opérations finales qui doivent être exécutées pour la variable ;
- libération de la mémoire : on reprend la case mémoire que la variable occupait pendant sa vie.
Pour les types fondamentaux, il n'y a pas d'opération finale à exécuter. Par exemple, la valeur d'une variable de type int n'est pas remise à zéro. Pour de telles variables, il n'y a que la libération de la mémoire, qui pourra être utilisée pour d'autres variables plus tard.
En revanche, certains types de variables nécessitent des opérations spéciales pendant la finalisation. Par exemple, un objet File aura besoin d'écrire sur le disque les caractères qui sont toujours dans son tampon de sortie et de notifier au système de fichiers le fait qu'il n'utilise plus le fichier. Ces opérations sont la destruction d'un objet File.
Les opérations finales des tableaux sont d'un niveau un peu plus haut : avant de finaliser le tableau, ses éléments doivent d'abord être détruits. Si les éléments sont d'un type fondamental simple comme int, il n'y a alors pas d'opération finale spéciale à effectuer. Si les éléments sont d'un type structure ou classe qui a besoin d'être finalisé, ces opérations sont alors exécutées pour chaque élément.
Les tableaux associatifs sont similaires aux tableaux. De plus, leurs clés peuvent également avoir besoin d'être finalisées si elles sont d'un type qui nécessite une destruction.
Le ramasse-miettes : D est un langage géré par un ramasse-miettes. Dans de tels langages, finaliser un objet ne nécessite pas d'être fait explicitement par le programmeur. Quand la vie d'une variable se termine, sa finalisation est automatiquement gérée par le ramasse-miettes. Nous verrons le ramasse-miettes et la gestion spéciale de la mémoire dans un chapitre ultérieur.
Les variables peuvent être finalisées de deux manières :
- quand leur vie prend fin : la finalisation se passe lors de la fin de vie de la variable ;
- un moment dans le futur : la finalisation se passe à un moment indéterminé du futur et elle est faite par le ramasse-miettes.
La manière de laquelle une variable est finalisée dépend surtout de son type. Certains types de variables comme les tableaux, les tableaux associatifs et les classes sont normalement détruits par le ramasse-miettes à un certain point dans le futur.
47-3-3. Affectation▲
L'autre opération fondamentale qu'une variable subit dans sa vie est l'affectation.
Pour les types simples fondamentaux, l'affectation est simplement le changement de sa valeur. Comme nous l'avons vu ci-dessus dans la représentation de la mémoire, une variable de type int prendrait la valeur 124 au lieu de 123. Cependant, plus généralement, l'affectation consiste en deux étapes, qui ne sont pas forcément exécutées dans l'ordre suivant :
- destruction de l'ancienne valeur ;
- construction de la nouvelle valeur.
Ces deux étapes ne sont pas importantes pour les types fondamentaux puisqu'ils n'ont pas besoin de destruction. Pour les types qui nécessitent une destruction, il est important de se souvenir que l'affectation est une combinaison de ces deux étapes.