programmez-en-d/valeur_vs_reference.whata

558 lines
24 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = Types valeur et types référence
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Gouget"
]
Ce chapitre introduit les notions de types valeur et types référence. Ces notions sont particulièrement importantes pour comprendre les différences entre les structures et les classes.
Ce chapitre décrit également plus en détail l'opérateur [c &].
Le chapitre se clôt avec un tableau qui contient les résultats des deux types de comparaisons suivants pour chaque type de variable~ :
- comparaison par valeur~ ;
- comparaison par adresse.
[ = Types valeur
Les types valeur sont faciles à décrire~ : les variables de types valeur contiennent des valeurs. Par exemple, tous les types entiers et les flottants sont des types valeur. Même si ce n'est pas immédiatement évident, les tableaux à taille fixe sont aussi des types valeur.
Par exemple, une variable de type [c int] a une valeur entière~ :
[code=d <<<
int vitesse = 123;
>>>]
Le nombre d'octets que la variable [c vitesse] occupe est la taille d'un [c int]. Si on représenter la mémoire comme un ruban allant de gauche à droite, on peut imaginer la variable résidant dans une partie de ce ruban~ :
[pre <<<
vitesse
---+-----+---
| 123 |
---+-----+---
>>>]
Quand des variables de types valeur sont copiées, elles reçoivent leurs propres valeurs~ :
[code=d <<<
int nouvelleVitesse = vitesse;
>>>]
La nouvelle variable a une place et une valeur propres~ :
[pre <<<
vitesse nouvelleVitesse
---+-----+--- ---+-----+---
| 123 | | 123 |
---+-----+--- ---+-----+---
>>>]
Naturellement, les modifications qui sont apportées à ces variables sont indépendantes~ :
[code=d <<<
vitesse = 200;
>>>]
La valeur de l'autre variable ne change pas~ :
[pre <<<
vitesse nouvelleVitesse
---+-----+--- ---+-----+---
| 200 | | 123 |
---+-----+--- ---+-----+---
>>>]
[ = Note sur l'utilisation des assertions dans ce chapitre
Les exemples qui suivent contiennent des assertions pour indiquer que leur conditions sont vraies. En d'autres termes, ce ne sont pas des vérifications au sens propre du temps, plutôt un moyen d'indiquer au lecteur que «~ ceci est vrai~ ».
Par exemple, l'assertion [c assert(vitesse == nouvelleVitesse)] dans la section suivante signifie que [c vitesse] est égale à [c nouvelleVitesse].
]
[ = Identité entre valeurs
- [** égalité entre valeurs]~ : l'opérateur [c ==] qui apparaît dans beaucoup d'exemples à travers le livre compare les variables par leurs valeurs. Quand deux variables sont dites [* égales] dans ce sens, leurs valeurs sont égales.
- [** identité entre valeurs]~ : dans le sens où elle ont chacune des valeurs indépendantes, [c vitesse] et [c nouvelleVitesse] ne sont pas identiques. Même si leurs valeurs sont égales, ce sont des variables différentes.
[code=d <<<
int vitesse = 123;
int nouvelleVitesse = vitesse;
assert(vitesse == nouvelleVitesse);
vitesse = 200;
assert(vitesse != nouvelleVitesse);
>>>]
]
[ = Opérateur de déréférencement («~ adresse de~ ») [c &]
Jusqu'à maintenant, nous avons utilisé l'opérateur [c &] avec [c readf()]. L'opérateur [c &] indique à [c readf()] où ranger la donnée d'entrée.
Les adresses des variables peuvent être utilisées pour d'autres choses. Le code suivant affiche simplement les adresses des deux variables~ :
[code=d <<<
int vitesse = 123;
int nouvelleVitesse = vitesse;
writeln("vitesse : ", vitesse, " adresse : ", &vitesse);
writeln("nouvelleVitesse : ", nouvelleVitesse, " adresse : ", &nouvelleVitesse);
>>>]
[c vitesse] et [c nouvelleVitesse] ont la même valeur mais leurs adresses sont différentes~ :
[output <<<
vitesse : 123 adresse : 7FFF4B39C738
nouvelleVitesse : 123 adresse : 7FFF4B39C73C
>>>]
[p Note~ : | Il est normal que les adresses aient des valeurs différentes à chaque fois que le programme est lancé. Les variables résident aux endroits où la mémoire est disponible au moment où le programme est exécuté.]
Les adresses sont normalement affichées au format hexadécimal.
De plus, le fait que la différence entre les deux adresses vaille 4 indique que ces deux entiers sont placés l'un à côté de l'autre en mémoire (la valeur de [c C] en hexadécimal est 12, et [m 12 - 8 = 4]).
]
]
[ = Variables référence
Avant d'arriver aux types référence, commençons par définir les variables par référence.
[p Terminologie~ : | jusqu'à maintenant, nous avons utilisé l'expression ''donner accès à'' dans divers contextes à travers le livre. Par exemple, les tranches et les tableaux associatifs ne stockent pas eux-même d'éléments mais donnent accès à des éléments qui sont stockés par l'environnement d'exécution (''runtime'') du D. Une autre expression qui veut dire la même chose est ''être une référence de'' comme dans «~ les tranches [** sont des références de] zéro, un ou plusieurs éléments~ », qui est même raccourci en «~ cette tranche [* référence] deux éléments~ ». Finalement, accéder à une valeur à travers une référence est le [** déréférencement].]
Les variables référence agissent comme des alias d'autres variables. Même si elles ressemblent et sont utilisées comme des variables, elles n'ont pas de valeur propre.
Nous avons déjà utilisé des variables référence dans deux contextes~ :
- [
[p [c ref] dans les boucles [c foreach]~ : | quand le mot-clé [c ref] est utilisé dans les boucles [c foreach], la variable de boucle est l'élément correspondant à l'itération lui-même. Sans le mot-clé [c ref], la variable de boucle est une [* copie] de cet élément.]
Ceci peut être démontré avec l'opérateur [c &]. Si leurs adresses sont les mêmes, deux variables référencent la même valeur (ou le ''même élément'')~ :
[code=d <<<
int[] tranche = [ 0, 1, 2, 3, 4 ];
foreach (i, ref element; tranche) {
assert(&element == &tranche[i]);
}
>>>]
Même si ce sont des variables différentes, le fait que les adresses de [c element] et [c <<<tranche[i]>>>] soient les mêmes montre qu'elles sont identiques en termes de valeur.
En d'autres termes, [c element] et [c <<<tranche[i]>>>] sont des références de la même valeur. Modifier l'une ou l'autre affecte la valeur. Voici une représentation de la mémoire lors de l'itération pour laquelle [c i] vaut 3~ :
[code <<<
tranche[0] tranche[1] tranche[2] tranche[3] tranche[4]
⇢ ⇢ ⇢ (element)
--+----------+----------+----------+----------+---------+--
| 0 | 1 | 2 | 3 | 4 |
--+----------+----------+----------+----------+---------+--
>>>]
]
- [
[p paramètres de fonction [c ref] et [c out]~ : | les paramètres [c ref] et [c out] sont des alias de la variable avec laquelle la fonction a été appelée.]
L'exemple suivant montre ceci en passant la même variable à une fonction, avec deux paramètres [c ref] et [c out] distincts. Encore une fois, l'opérateur [c &] permet de voir que les deux paramètres [* pointent vers] la même valeur~ :
[code=d <<<
import std.stdio;
void main()
{
int variableOriginale;
writeln("adresse de variableOriginale : ", &variableOriginale);
foo(variableOriginale, variableOriginale);
}
void foo(ref int parametreRef, out int parametreOut)
{
writeln("adresse de parametreRef : ", &parametreRef);
writeln("adresse de parametreOut : ", &parametreOut);
assert(&parametreRef == &parametreOut);
}
>>>]
Même si elles sont définies comme des paramètres différents, les variables [c parametreRef] et [c parametreOut] sont des alias de [c variableOriginale]~ :
[output <<<
adresse de variableOriginale : 7FFF24172958
adresse de parametreRef : 7FFF24172958
adresse de parametreOut : 7FFF24172958
>>>]
]
]
[ = Types référence
Les variables des types référence ont des identités propres mais n'ont pas de valeurs propre. Elles [* donnent accès] à des variables existantes.
Nous avons vu cette idée avec les tranches. Les tranches ne stockent pas leurs éléments, elles donnent accès à des éléments existants~ :
[code=d <<<
void main()
{
// Même si elle est nommée 'tableau' ici, cette variable est
// aussi une tranche. elle donne acces à tous les éléments
// initiaux :
int[] tableau = [ 0, 1, 2, 3, 4 ];
// Une tranche qui donne accès aux éléments, sans le premier ni
// le dernier :
int[] tranche = tableau[1 .. $ - 1];
// À cet endroit, tranche[0] et tableau[1] donnent accès à la même
// valeur :
assert(&tranche[0] == &tableau[1]);
// Changer tranche[0] modifie également tableau[1] :
tranche[0] = 42;
assert(tableau[1] == 42);
}
>>>]
Contrairement aux variables par référence, les types référence ne sont pas simplement des alias. Pour voir cette distinction définissons une autre tranche comme copie de la tranche existante~ :
[code=d <<<
int[] tranche2 = tranche;
>>>]
Ces deux tranches ont chacune leur propre adresse. Autrement dit, elles ont leur propre identité~ :
[code=d <<<
assert(&tranche != &tranche2);
>>>]
La liste suivante est le résumé des différences entre les variables par référence et les types référence~ :
- les variables par référence n'ont pas d'identité, elles sont des alias de variables existantes~ ;
- les variables de types référence ont une identité mais n'ont pas de valeur propre~ ; elles donnent accès à des valeurs existantes.
La manière par laquelle [c tranche] et [c tranche2] vivent en mémoire peut être illustrée comme suit~ :
[code <<<
tranche tranche2
---+---+---+---+---+---+--- ---+---+--- ---+---+---
| 0 | 1 | 2 | 3 | 4 | | o | | o |
---+---+---+---+---+---+--- ---+-|-+--- ---+-|-+---
▲ | |
| | |
+--------------------+------------+
>>>]
Une des différences entre le C++ et le D est que les classes sont des types référence en D. Même si nous verrons les classes dans des chapitres ultérieurs, ce qui suit est un petit exemple qui démontre ce fait~ :
[code=d <<<
class MaClasse
{
int membre;
}
>>>]
Les objets de type classe sont construits avec le mot-clé [c new]~ :
[code=d <<<
auto variable = new MaClasse;
>>>]
[c variable] est une référence d'un object [c MaClasse] anonyme qui a été construit avec [c new]~ :
[code <<<
(objet MaClasse anonyme) variable
---+-------------------+--- ---+---+---
| ... | | o |
---+-------------------+--- ---+-|-+---
▲ |
| |
+--------------------+
>>>]
Tout comme avec les tranches, quand [c variable] est copiée, la copie devient une autre référence du même objet. La copie a sa propre adresse~ :
[code=d <<<
auto variable = new MaClasse;
auto variable2 = variable;
assert(variable == variable2);
assert(&variable != &variable2);
>>>]
Elles sont égales dans le sens où elle référencent le même objet, mais elles sont des variables distinctes~ :
[code <<<
(objet MaClasse anonyme) variable variable2
---+-------------------+--- ---+---+--- ---+---+---
| ... | | o | | o |
---+-------------------+--- ---+-|-+--- ---+-|-+---
▲ | |
| | |
+--------------------+------------+
>>>]
Cela peut aussi être vu en modifiant le membre de l'objet~ :
[code=d <<<
auto variable = new MaClasse;
variable.membre = 1;
auto variable2 = variable; // Elles partagent le même objet
variable2.membre = 2;
assert(variable.membre == 2); // L'objet que les 2 variables référencent a changé.
>>>]
Un autre type référence est le tableau associatif. Comme pour les tranches et les classes, quand une variable de type tableau associatif est copiée dans une autre variable, les deux donnent accès au même ensemble d'élément~ :
[code=d <<<
string[int] parNom =
[
1 : "un",
10 : "dix",
100 : "cent",
];
// Les deux tableaux associatifs vont partager le même ensemble d'éléments :
string[int] parNom2 = parNom;
// La nouvelle association ajoutée via le second tableau...
parNom2[4] = "quatre";
// ...se retrouve dans le premier.
assert(parNom[4] == "quatre");
>>>]
[ = La différence dans l'opération d'affectation
L'opération d'affectation est différente pour les types valeur et les types référence~ ; avec les types valeur et les variables référence, l'affectation change la vraie valeur~ :
[code=d <<<
void main()
{
int nombre = 8;
diviserParDeux(nombre); // La valeur change
assert(nombre == 4);
}
void diviserParDeux(ref int dividende)
{
dividende /= 2;
}
>>>]
D'un autre côté, avec les types référence, l'opération d'affectation change l'accès~ : la valeur pointée par la variable affectée ne change pas, mais la variable affecté pointe vers une autre valeur. Par exemple l'affectation de la variable [c tranche3] dans le code suivant ne change la valeur d'aucun élément~ ; elle change quels éléments [c tranche3] référence~ :
[code=d <<<
int[] tranche1 = [ 10, 11, 12, 13, 14 ];
int[] tranche2 = [ 20, 21, 22 ];
int[] tranche3 = tranche1[1 .. 3]; // Accès aux éléments de tranche1
// avec les indices 1 et 2
tranche3[0] = 777;
assert(tranche1 == [ 10, 777, 12, 13, 14 ]);
// Cette affectation ne modifie pas les éléments que tranche3 référence,
// elle fait référencer d'autres éléments par tranche3.
tranche3 = tranche2[$ - 1 .. $]; // Accès au dernier élément.
tranche3[0] = 888;
assert(tranche2 == [ 20, 21, 888 ]);
>>>]
Montrons le même effet avec, cette fois, deux objets de type [c MaClasse]~ :
[code=d <<<
auto variable1 = new MaClasse;
variable1.membre = 1;
auto variable2 = new MaClasse;
variable2.membre = 2;
auto uneCopie = variable1;
uneCopie.membre = 3;
uneCopie = variable2;
uneCopie.membre = 4;
assert(variable1.membre == 3);
assert(variable2.membre == 4);
>>>]
La variable [c uneCopie] référence d'abord le même objet que [c variable1], puis le même objet que [c variable2]. Par conséquent, le [c .membre] qui est modifié à travers [c uneCopie] est d'abord celui de [c variable1] puis celui de [c variable2].
]
[ = Les variables de types référence peuvent ne pas référencer d'objet
Une variable référence est toujours l'alias d'une autre variable, elle ne peut pas commencer sa vie sans variable. En revanche, les variables de types référence peuvent commencer leur vie sans référencer aucun objet.
Par exemple, une variable [c MaClasse] peut être définie sans avoir créé d'objet avec [c new]~ :
[code=d <<<
MaClasse variable;
>>>]
De telles variables ont la valeur spéciale [c null]. Nous verrons [c null] et le mot-clé [c is] dans un [[part:null_is | chapitre ultérieur]].
]
]
[ = Les tableaux à taille fixe sont des types valeur, les tranches sont des types référence
Les tableaux et les tranches du D divergent lorsqu'on considère la différence entre type valeur et type référence.
Nous l'avons déjà vu, les tranches sont des types référence. Par contre, les tableaux à taille fixe sont des types valeur. Ils stockent eux même leurs éléments et se comportent comme des valeurs individuelles~ :
[code=d <<<
int[3] tableau1 = [ 10, 20, 30 ];
auto tableau2 = tableau1; // Les éléments de tableau2 sont différents
// des éléments de tableau1
tableau2[0] = 11;
// Le premier tableau n'est pas affecté :
assert(tableau1[0] == 10);
>>>]
[c tableau1] est un tableau à taille fixe parce que sa taille est indiquée lors de sa définition. Comme [c auto] infère le type de [c tableau2], [c tableau2] est également un tableau à taille fixe. Les valeurs des éléments de [c tableau2] sont copiées depuis les valeurs des éléments de [c tableau1]. Chaque tableau a ses propre éléments. Modifier un élément d'un tableau n'affecte pas l'autre tableau.
]
[ = Expérimentation
Le programme suivant applique l'opérateur [c ==] à des types différents. Il applique l'opérateur à deux variables d'un certain type et aux adresses de ces variables. Le programme produit la sortie suivante~ :
[output <<<
Type de variable a == b &a == &b
==================================================================================
variables avec des valeurs égales (type valeur) true false
variables avec des valeurs différentes (type valeur) false false
[c foreach] avec une variable 'ref' true true
[c foreach] sans variable 'ref' true false
fonction avec paramètre 'out' true true
fonction avec paramètre 'ref' true true
fonction avec paramètre 'in' true false
tranches donnant accès aux mêmes éléments true false
tranches donnant accès à des éléments différents false false
variables MaClasse vers le même objet (type référence) true false
variables MaClasse vers des objets différents (type référence) false false
>>>]
Cette table a été générée par le programme suivant~ :
[code=d <<<
import std.stdio;
import std.conv;
import std.tableau;
int variableModule = 9;
class MaClasse
{
int membre;
}
void afficherEntete()
{
immutable dchar[] entete =
" Type de variable"
" a == b &a == &b";
writeln();
writeln(entete);
writeln(replicate("=", entete.length));
}
void afficherInfo(const dchar[] etiquette,
bool egaliteValeur,
bool egaliteAdresse)
{
writefln("%55s%9s%9s",
etiquette,
to!string(egaliteValeur),
to!string(egaliteAdresse));
}
void main()
{
afficherEntete();
int nombre1 = 12;
int nombre2 = 12;
afficherInfo("variables avec des valeurs égales (type valeur)",
nombre1 == nombre2,
&nombre1 == &nombre2);
int nombre3 = 3;
afficherInfo("variables avec des valeurs differentes (type valeur)",
nombre1 == nombre3,
&nombre1 == &nombre3);
int[] tranche = [ 4 ];
foreach (i, ref element; tranche) {
afficherInfo("foreach avec variable 'ref'",
element == tranche[i],
&element == &tranche[i]);
}
foreach (i, element; tranche) {
afficherInfo("foreach sans variable 'ref'",
element == tranche[i],
&element == &tranche[i]);
}
parametreOut(variableModule);
parametreRef(variableModule);
parametreIn(variableModule);
int[] longueTranche = [ 5, 6, 7 ];
int[] tranche1 = longueTranche;
int[] tranche2 = tranche1;
afficherInfo("tranches donnant accès aux mêmes éléments",
tranche1 == tranche2,
&tranche1 == &tranche2);
int[] tranche3 = tranche1[0 .. $ - 1];
afficherInfo("tranches donnant accès à des éléments différents",
tranche1 == tranche3,
&tranche1 == &tranche3);
auto variable1 = new MaClasse;
auto variable2 = variable1;
afficherInfo(
"variables MaClasse vers le même objet (type référence)",
variable1 == variable1,
&variable1 == &variable2);
auto variable3 = new MaClasse;
afficherInfo(
"variables MaClasse vers des objets différents (type référence)",
variable1 == variable3,
&variable1 == &variable3);
}
void parametreOut(out int parametre)
{
afficherInfo("fonction avec paramètre 'out'",
parametre == variableModule,
&parametre == &variableModule);
}
void parametreRef(ref int parametre)
{
afficherInfo("fonction avec paramètre 'ref'",
parametre == variableModule,
&parametre == &variableModule);
}
void parametreIn(in int parametre)
{
afficherInfo("fonction avec paramètre 'in'",
parametre == variableModule,
&parametre == &variableModule);
}
>>>]
Notes~ :
- Le programme utilise une variable module pour comparer différents types de paramètres de fonctions. Les variables module sont définies au niveau des modules, hors de toute fonction. Elles sont accessibles globalement à tout le code du module.
- La fonction [c replicate] du module [c std.array] prend un tableau (la chaîne [c "="] dans le code précédent) et le répète le nombre de fois donné.
]
[ = Résumé
- Les variables de types valeur ont leurs propres valeurs et adresses.
- Les références n'ont pas leur propre valeur ni adresse. Elles sont des alias de variables existantes.
- Les variables de types référence ont leurs propres adresses mais les valeurs qu'elles référencent ne leur appartiennent pas.
- Avec les types référence, l'affectation ne change pas la valeur, elle change quelle valeur est pointée.
- Les variables de types référence peuvent être nulles ([c null]).
]