48. Types valeur et types référence▲
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 &.
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.
48-1. 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 int a une valeur entière :
int
vitesse =
123
;
Le nombre d'octets que la variable vitesse occupe est la taille d'un int. Si on représente la mémoire comme un ruban allant de gauche à droite, on peut imaginer la variable résidant dans une partie de ce ruban :
|
Quand des variables de types valeur sont copiées, elles reçoivent leurs propres valeurs :
int
nouvelleVitesse =
vitesse;
La nouvelle variable a une place et une valeur propres :
|
Naturellement, les modifications qui sont apportées à ces variables sont indépendantes :
vitesse =
200
;
La valeur de l'autre variable ne change pas :
|
48-1-1. Note sur l'utilisation des assertions dans ce chapitre▲
Les exemples qui suivent contiennent des assertions pour indiquer que leurs 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 assert(vitesse == nouvelleVitesse) dans la section suivante signifie que vitesse est égale à nouvelleVitesse.
48-1-2. Identité entre valeurs▲
- Égalité entre valeurs : l'opérateur == 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ù elles ont chacune des valeurs indépendantes, vitesse et nouvelleVitesse ne sont pas identiques. Même si leurs valeurs sont égales, ce sont des variables différentes.
int
vitesse =
123
;
int
nouvelleVitesse =
vitesse;
assert
(
vitesse ==
nouvelleVitesse);
vitesse =
200
;
assert
(
vitesse !=
nouvelleVitesse);
48-1-3. Opérateur de déréférencement (« adresse de ») &▲
Jusqu'à maintenant, nous avons utilisé l'opérateur & avec readf(). L'opérateur & indique à 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 :
int
vitesse =
123
;
int
nouvelleVitesse =
vitesse;
writeln
(
"vitesse : "
, vitesse, " adresse : "
, &
vitesse);
writeln
(
"nouvelleVitesse : "
, nouvelleVitesse, " adresse : "
, &
nouvelleVitesse);
vitesse et nouvelleVitesse ont la même valeur, mais leurs adresses sont différentes :
vitesse : 123
adresse : 7FFF4B39C738
nouvelleVitesse : 123
adresse : 7FFF4B39C73C
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 en hexadécimal est 12, et 12−8=4).
48-2. Variables référence▲
Avant d'arriver aux types référence, commençons par définir les variables par référence.
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êmes 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 :
-
ref dans les boucles foreach : quand le mot-clé ref est utilisé dans les boucles foreach, la variable de boucle est l'élément correspondant à l'itération lui-même. Sans le mot-clé ref, la variable de boucle est une copie de cet élément.
Ceci peut être démontré avec l'opérateur &. Si leurs adresses sont les mêmes, deux variables référencent la même valeur (ou le même élément) :Sélectionnezint
[] 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 element et tranche[i] soient les mêmes montre qu'elles sont identiques en valeur. En d'autres termes, element et 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 i vaut 3 :
-
Paramètres de fonction ref et out : les paramètres ref et 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 ref et out distincts. Encore une fois, l'opérateur & permet de voir que les deux paramètres pointent vers la même valeur :Sélectionnezimport
std.stdio;void
main
(
){
int
variableOriginale;writeln
(
"adresse de variableOriginale : "
,&
variableOriginale);foo
(
variableOriginale, variableOriginale);}
void
foo
(
refint
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 parametreRef et parametreOut sont des alias de variableOriginale :
adresse de variableOriginale : 7FFF24172958
adresse de parametreRef : 7FFF24172958
adresse de parametreOut : 7FFF24172958
48-3. Types référence▲
Les variables des types référence ont des identités propres, mais n'ont pas de valeurs propres. 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 :
void
main
(
)
{
// Même si elle est nommée 'tableau' ici, cette variable est
// aussi une tranche. Elle donne accès à 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 :
int
[] tranche2 =
tranche;
Ces deux tranches ont chacune leur propre adresse. Autrement dit, elles ont leur propre identité :
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 dont tranche et tranche2 vivent en mémoire peut être illustrée comme suit :
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 :
class
MaClasse
{
int
membre;
}
Les objets de type classe sont construits avec le mot-clé new :
auto
variable =
new
MaClasse;
variable est une référence d'un objet MaClasse anonyme qui a été construit avec new :
|
Tout comme avec les tranches, quand variable est copiée, la copie devient une autre référence du même objet. La copie a sa propre adresse :
auto
variable =
new
MaClasse;
auto
variable2 =
variable;
assert
(
variable ==
variable2);
assert
(&
variable !=
&
variable2);
Elles sont égales dans le sens où elles référencent le même objet, mais elles sont des variables distinctes :
|
Cela peut aussi être vu en modifiant le membre de l'objet :
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 deux 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éments :
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"
);
48-3-1. 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 :
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ée pointe vers une autre valeur. Par exemple, l'affectation de la variable tranche3 dans le code suivant ne change la valeur d'aucun élément ; elle change les éléments que tranche3 référence :
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 MaClasse :
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 uneCopie référence d'abord le même objet que variable1, puis le même objet que variable2. Par conséquent, le .membre qui est modifié à travers uneCopie est d'abord celui de variable1 puis celui de variable2.
48-3-2. 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 MaClasse peut être définie sans avoir créé d'objet avec new :
MaClasse variable;
De telles variables ont la valeur spéciale null. Nous verrons null et le mot-clé is dans un chapitre ultérieurLa valeur null et l'Opérateur is.
48-4. 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êmes leurs éléments et se comportent comme des valeurs individuelles :
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
);
tableau1 est un tableau à taille fixe parce que sa taille est indiquée lors de sa définition. Comme auto infère le type de tableau2, tableau2 est également un tableau à taille fixe. Les valeurs des éléments de tableau2 sont copiées depuis les valeurs des éléments de tableau1. Chaque tableau a ses propres éléments. Modifier un élément d'un tableau n'affecte pas l'autre tableau.
48-5. Expérimentation▲
Le programme suivant applique l'opérateur == à 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 :
Sortie :
Cette table a été générée par le programme suivant :
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 replicate du module std.array prend un tableau (la chaîne "=" dans le code précédent) et le répète le nombre de fois donné.
48-6. Résumé▲
Les variables de types valeur ont leurs propres valeurs et adresses.
- Les références n'ont ni 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, mais quelle valeur est pointée.
- Les variables de types référence peuvent être nulles (null).