Cours complet pour apprendre à programmer en D


précédentsommairesuivant

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 :

 
Sélectionnez
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 :

Image non disponible

Quand des variables de types valeur sont copiées, elles reçoivent leurs propres valeurs :

 
Sélectionnez
int nouvelleVitesse = vitesse;

La nouvelle variable a une place et une valeur propres :

Image non disponible

Naturellement, les modifications qui sont apportées à ces variables sont indépendantes :

 
Sélectionnez
vitesse = 200;

La valeur de l'autre variable ne change pas :

Image non disponible

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.
 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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électionnez
    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 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 :

    Image non disponible

  • 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électionnez
    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 parametreRef et parametreOut sont des alias de variableOriginale :
 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
int[] tranche2 = tranche;

Ces deux tranches ont chacune leur propre adresse. Autrement dit, elles ont leur propre identité :

 
Sélectionnez
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 :

Image non disponible

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 :

 
Sélectionnez
class MaClasse
{
    int membre;
}

Les objets de type classe sont construits avec le mot-clé new :

 
Sélectionnez
auto variable = new MaClasse;

variable est une référence d'un objet MaClasse anonyme qui a été construit avec new :

Image non disponible

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 :

 
Sélectionnez
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 :

Image non disponible

Cela peut aussi être vu en modifiant le membre de l'objet :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

Image non disponible

Cette table a été générée par le programme suivant :

 
Sélectionnez
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).

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+