programmez-en-d/fonctions_speciales.whata

880 lines
36 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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[set
title = "Le constructeur et autres fonctions spéciales"
partAs = chapitre
translator = "Olivier Pisano"
]
Bien que ce chapitre se focalise sur les structures, les sujets abordés s'appliquent pour la plupart également aux classes. Les différences seront abordées dans des chapitres ultérieurs.
Quatre fonctions membres des structures sont spéciales, car elles définissent les opérations fondamentales d'un type :
- Le constructeur : [c this()]
- Le destructeur : [c ~~this()]
- La postcopie : [c this(this)]
- L'opérateur d'affectation : [c opAssign()]
Bien que ces opérations fondamentales soient prises en charge automatiquement pour les structures, et donc n'aient pas besoin d'être définies par le programmeur, elles peuvent être surchargées pour spécialiser le comportement d'une structure.
[ = Le constructeur
Le constructeur a la responsabilité de préparer un objet à être utilisé en assignant des valeurs à ses membres.
Nous avons déjà utilisé des constructeurs dans les chapitres précédents. Quand un nom de type est utilisé comme une fonction, c'est en fait le constructeur de ce type qui est appelé. Nous pouvons le voir sur la partie droite de la ligne suivante :
[code=d <<<
auto arriveeDuBus = MomentDeLaJournee(8, 30);
>>>]
De la même manière, un objet class est construit dans la partie droite de la ligne suivante :
[code=d <<<
auto variable = new UneClasse();
>>>]
Les arguments qui sont spécifiés entre les parenthèses correspondent aux paramètres du constructeur. Par exemple, les valeurs 8 et 30 ci-avant sont passées au constructeur de MomentDeLaJournee comme paramètres.
[ = Syntaxe d'un constructeur
Contrairement aux autres fonctions, les constructeurs n'ont pas de valeur de retour. Le nom d'un constructeur est toujours « this » :
[code=d <<<
struct UneStructure
{
// ...
this(/* Paramètres du constructeur */)
{
// Opérations préparant l'objet à être utilisé
}
}
>>>]
Les paramètres du constructeur incluent des informations qui sont nécessaires à créer un objet utile et cohérent.
]
[ = Constructeur automatique généré par le compilateur
Toutes les structures que nous avons vues jusqu'à présent ont tiré avantage d'un constructeur qui avait été généré automatiquement par le compilateur. Le constructeur automatique assigne les valeurs de ses paramètres à ses membres dans l'ordre dans lequel ils sont spécifiés.
Comme nous l'avons vu au chapitre sur les structures, toutes les valeurs initiales des membres n'ont pas besoin d'être spécifiées. Les membres non spécifiés sont initialisés à la valeur [c .init] de leurs types respectifs. La valeur .init d'un membre peut être spécifiée lors de la déclaration de celui-ci après l'opérateur d'affectation :
[code=d <<<
struct Test
{
int membre = 42;
}
>>>]
Si l'on considère les paramètres par défaut vus dans le chapitre sur les nombres de paramètres variables, on pourrait imaginer que le constructeur automatique de la structure suivante pourrait ressembler à cette fonction this() :
[code=d <<<
struct Test
{
char c;
int i;
double d;
/* L'équivalent du constructeur automatique généré par le compilateur
* (note : Ce code est à but illustratif, le constructeur suivant ne
* sera pas réellement appelé lors de la construction par défaut de
* l'objet via Test()). */
this(in char parametre_c = char.init,
in int parametre_i = int.init,
in double parametre_d = double.init)
{
c = parametre_c;
i = parametre_i;
d = parametre_d;
}
}
>>>]
Pour la plupart des structures, le constructeur automatique est suffisant. Généralement, fournir les valeurs appropriées pour chaque membre est tout ce dont on a besoin pour construire des objets.
]
[ = Accéder aux membres avec this
Dans le code précédent, pour éviter de confondre les paramètres du constructeur et les membres de l'objet, j'ai préfixé tous les noms de paramètres par « parametre ». Sans cela, il y aurait des erreurs de compilation :
[code=d <<<
struct Test
{
char c;
int i;
double d;
this(in char c = char.init,
in int i = int.init,
in double d = double.init)
{
// Une tentative d'assigner un paramètre in à lui-même !
c = c; // Erreur de compilation
i = i;
d = d;
}
}
>>>]
La raison en est la suivante : « c » tout seul désigne le paramètre, pas le membre, et les paramètres ci-dessus sont marqués comme in et ne peuvent être modifiés.
[output <<<
Erreur: Variable Test.this.c impossible de modifier const
>>>]
Une solution est d'ajouter [c this]. avant les noms des membres. Dans les fonctions membres, [c this] veut dire « cet objet », et donc [c this.c] désigne la variable [c c] membre de l'objet courant.
[code=d <<<
this(in char c = char.init,
in int i = int.init,
in double d = double.init)
{
// Une tentative d'assigner un paramètre in à lui-même !
this.c = c; // Erreur de compilation
this.i = i;
this.d = d;
}
>>>]
Désormais, [c content] tout seul signifie le paramètre [c c] et this.c signifie le membre, et le code se compile et [c content] comme on pourrait s'y attendre. Le membre c est initialisé avec la valeur du paramètre [c c].
]
[ = Constructeurs définis par l'utilisateur
Je viens de décrire le comportement du constructeur généré par le compilateur. Puisque ce constructeur est approprié à la plupart des cas, il n'est pas nécessaire de définir manuellement un constructeur.
Néanmoins, il arrive que la construction d'un objet nécessite des opérations plus complexes que l'assignation ordonnée de valeurs à des membres. Par exemple, considérons la structure Duree des chapitres précédents :
[code=d <<<
struct Duree
{
int minute;
}
>>>]
Le constructeur généré par le compilateur est suffisant pour cette structure à un membre :
[code=d <<<
moment.decrementer(Duree(12));
>>>]
Puisque le constructeur prend un nombre de minutes en paramètre, les programmeurs auront parfois besoin de faire de calculs préalables avant de l'utiliser :
[code=d <<<
// 23 heures et 18 minutes plus tôt
moment.decrementer(Duree(23 * 60 + 18));
// 22 heures et 20 minutes plus tard
moment.incrementer(Duree(22 * 60 + 20));
>>>]
Pour éliminer le besoin de faire ces calculs, nous pouvons concevoir un constructeur de Duree qui prend deux paramètres et fait automatiquement ce calcul :
[code=d <<<
struct Duree
{
int minute;
this(int heure, int minute)
{
this.minute = heure * 60 + minute;
}
}
>>>]
Puisquheure et minute sont maintenant des paramètres séparés, les utilisateurs fournissent désormais leurs valeurs sans faire eux-mêmes les calculs :
[code=d <<<
// 23 heures et 18 minutes plus tôt
moment.decrementer(Duree(23, 18));
// 22 heures et 20 minutes plus tard
moment.incrementer(Duree(22, 20));
>>>]
]
[ = Les constructeurs définis par l'utilisateur désactivent le constructeur généré par le compilateur
Un constructeur qui est défini par le programmeur invalide certaines utilisations du constructeur généré par le compilateur. Les objets ne peuvent plus être construits par des valeurs de paramètres par défaut. Par exemple, essayer de construire un objet Duree avec un seul paramètre produit une erreur de compilation :
[code=d <<<
moment.decrementer(Duree(12)); // Erreur de compilation
>>>]
Appeler le constructeur avec un seul paramètre ne correspond pas au constructeur défini par le programmeur et le constructeur généré par le compilateur est désactivé.
Une solution est de surcharger le constructeur en définissant un autre constructeur qui prend un seul paramètre :
[code=d <<<
struct Duree
{
int minute;
this(int heure, int minute)
{
this.minute = heure * 60 + minute;
}
this(int minute)
{
this.minute = minute;
}
}
>>>]
Un constructeur défini par l'utilisateur désactive également la possibilité de construire des objets avec la syntaxe [c { }] :
[code=d <<<
Duree duree = { 5 }; // Erreur de compilation
>>>]
Une initialisation sans paramètre est toujours autorisée :
[code=d <<<
auto d = Duree(); // compile
>>>]
Parce qu'en D, la valeur [c .init] de chaque type doit être connu à la compilation. La valeur de [c d] ci-dessus est égale à la valeur initiale de [c Duree] :
[code=d <<<
assert(d == Duree.init);
>>>]
]
[ = [c static opCall] en place de constructeur par défaut
Puisqu'il faut que la valeur initiale de chaque type soit connue dès la compilation, il est impossible de définir explicitement un constructeur par défaut.
Considérons le constructeur suivant qui essaie d'afficher des informations à chaque fois qu'un objet de ce type est construit :
[code=d <<<
struct Test
{
this() // ← Erreur de compilation
{
writeln("Un objet Test est construit");
}
}
>>>]
La sortie du compilateur :
[output <<<
Erreur: constructeur Test.this le constructeur par défaut pour les structures n'est autorisé qu'avec @disabled et sans corps.
>>>]
[p Note : |
Nous verrons dans des chapitres ultérieurs qu'il est possible de définir le constructeur par défaut pour les classes.
]
Pour pallier ce problème, une fonction statique [c opCall] peut être utilisée pour construire des objets sans fournir de paramètre. Notez que cela n'a aucun effet sur la valeur [c .init] du type.
Pour que cela fonctionne, la fonction statique [c opCall] doit construire et retourner un objet du type de la structure :
[code=d <<<
import std.stdio;
struct Test
{
static Test opCall()
{
writeln("Un objet Test est construit");
Test test;
return test;
}
}
void main()
{
auto test = Test();
}
>>>]
L'appel à [c Test()] dans la fonction [c main()] appelle la fonction statique [c opCall] :
[output <<<
Un objet [c Test] est construit.
>>>]
Notez qu'il n'est possible d'appeler [c Test()] à l'intérieur de la fonction statique [c opCall()]. La syntaxe exécuterait aussi la fonction statique [c opCall()] et causerait une récursion infinie :
[code=d <<<
static Test opCall()
{
writeln("Un objet Test est construit.");
return Test(); // Rappelle static OpCall()
}
>>>]
Le résultat :
[output <<<
Un objet Test est construit.
Un objet Test est construit.
Un objet Test est construit.
... ← Répète le même message
>>>]
]
[ = Appeler un autre constructeur
Les constructeurs peuvent appeler d'autres constructeurs pour éviter de dupliquer du code. Bien que [c Duree] soit trop simple pour montrer l'utilité de cette fonctionnalité, le constructeur à un paramètre qui suit utilise le constructeur à deux paramètres :
[code=d <<<
this(int heure, int minute)
{
this.minute = heure * 60 + minute;
}
this(int minute)
{
this(0, minute); // appelle l'autre constructeur
}
>>>]
Le constructeur qui prend un seul paramètre appelle l'autre constructeur en lui passant [c 0] comme valeur d'heure.
[p Attention : |
il y a une erreur de conception dans les constructeurs ci-avant parce que l'intention n'est pas claire lorsque les objets sont construits avec un seul paramètre.
]
[code=d <<<
// 10 heures ou 10 minutes
auto dureeVoyage = Duree(10);
>>>]
Bien qu'il soit possible de déterminer à la lecture de la documentation ou le code de la structure que le paramètre veut en fait dire « 10 minutes », c'est une incohérence vis-à-vis du premier paramètre du constructeur à deux paramètres qui est le nombre d'heures.
Ce genre d'erreur de conception est source de bugs et doit être évité.
]
[ = Immutabilité des paramètres du constructeur
Dans le chapitre sur l'immutabilité, nous avons vu qu'il n'est pas évident de décider si les paramètres des types référence devraient être déclarés [c const] ou [c immutable]. Bien que les mêmes considérations s'appliquent aux paramètres de constructeurs, [c immutable] est souvent un bien meilleur choix pour les paramètres de constructeur.
La raison en est qu'il est courant d'assigner aux membres des paramètres qui seront utilisés plus tard. Quand un paramètre n'est pas [c immutable], il n'y a aucune garantie que la variable originale n'ait pas été modifiée entre temps.
Considérons un constructeur qui prend un nom de fichier en paramètre. Le nom de fichier sera utilisé ultérieurement lorsqu'on écrira des notes d'élèves. Selon les lignes directrices du chapitre sur l'immutabilité, pour être plus utile, supposons que le paramètre du constructeur soit déclaré comme [c const char~[~]] :
[code=d <<<
import std.stdio;
struct Eleve
{
const char[] nomFichier;
int[] notes;
this(const char[] nomFichier)
{
this.nomFichier = nomFichier;
}
void enregistrer()
{
auto fichier = File(nomFichier.idup, "w");
fichier.writeln("Les notes de l'élève :");
fichier.writeln(notes);
}
// ...
}
void main()
{
char[] nomFichier;
nomFichier ~= "notes_eleve";
auto eleve = Eleve(nomFichier);
/* supposons que le nom de fichier soit modifié plus tard
* peut-être par inadvertance (tous les caractères sont transformés
* en 'A' ici) : */
nomFichier[] = 'A';
// ...
/* les notes sont enregistrées dans le mauvais fichier */
eleve.enregistrer();
}
>>>]
Le programme ci-avant enregistre les notes de l'élève dans un fichier dont le nom est constitué de caractères [c A], pas dans « notes_eleve ». C'est pour cette raison qu'il est parfois plus judicieux de déclarer les paramètres d'un constructeur et les variables membres de types références comme immutable. Nous savons que cela est facile pour les chaînes de caractères en utilisant des alias comme [c string]. Le code suivant montre les parties de la structure qui devraient être modifiées :
[code=d <<<
struct Eleve
{
string nomFichier;
// ...
this(string nomFichier
{
// ...
}
// ...
}
>>>]
Maintenant les utilisateurs de la structure doivent fournir des chaînes immuables, en conséquence de quoi la confusion sur le nom de fichier pourra être évitée.
]
[ = Conversion de types via des constructeurs à un paramètre
Les constructeurs à un seul paramètre peuvent être vus comme des sortes de fonctions de conversion. Ces constructeurs produisent un objet d'un type structure particulier depuis un paramètre de constructeur. Par exemple, le constructeur suivant produit un objet [c Etudiant] depuis une chaîne de caractères :
[code=d <<<
struct Etudiant
{
string nom;
this(string nom)
{
this.nom = nom;
}
}
>>>]
La fonction [c to()] et l'opérateur cast se comportent de la même manière. Pour en voir un exemple, considérons la fonction saluer() suivante. Passer en paramètre une chaîne de caractère alors qu'elle attend un [c Etudiant] causerait bien évidemment une erreur de compilation :
[code=d <<<
void saluer(Etudiant etudiant)
{
writeln("Bonjour ", etudiant.nom);
}
// ...
saluer("Jane"); // Erreur de compilation !
>>>]
D'un autre côté, chacune des lignes suivantes s'assure qu'un objet Etudiant est construit avant d'appeler la fonction :
[code=d <<<
import std.conv;
// ...
saluer(Etudiant("Jane"));
saluer(to!Etudiant("Jean"));
saluer(cast(Etudiant)"Jim");
>>>]
[c to()] et [c cast] utilisent le constructeur à un paramètre pour construire un objet [c Etudiant] temporaire et appeler [c saluer()] avec cet objet.
]
[ = Désactiver le constructeur par défaut
Les fonctions qui sont déclarées avec l'attribut [c @disable] ne peuvent pas être appelées.
Parfois, il n'y a pas de valeurs par défaut pertinentes pour les membres d'un type. Par exemple, il pourrait être illégal pour le type suivant d'avoir un nom de fichier vide :
[code=d <<<
struct Archive
{
string nomFichier;
}
>>>]
Malheureusement, le constructeur par défaut généré par le compilateur l'initialiserait par une chaîne vide :
[code=d <<<
auto archive = Archive(); // nomFichier est vide
>>>]
Le constructeur par défaut peut être explicitement désactivé en le déclarant comme @disable, pour que ces objets doivent être construits par un des autres constructeurs. Il n'y a pas besoin de fournir un corps à une fonction @disabled :
[code=d <<<
struct Archive
{
string nomFichier;
@disable this(); // ne peut être appelé
this(string nomFichier)
{
//...
}
}
// ...
auto archive = Archive(); // Erreur de compilation
>>>]
Cette fois, le compilateur n'autorise pas l'appel à this() :
[output <<<
Erreur: le constructeur Archive.this n'est pas appelable, car il est annoté avec @disable
>>>]
Les objets Archive doivent être construits avec un autre constructeur :
[code=d <<<
auto archive = Archive("enregistrements"); // compile
>>>]
]
]
[ = Le destructeur
Le destructeur contient des opérations qui doivent être exécutées à la fin de fin d'un objet.
Le destructeur généré par le compilateur exécute les destructeurs de tous les membres dans l'ordre. De cette façon, comme pour le constructeur par défaut, il n'est souvent pas nécessaire de définir de destructeur pour la plupart des structures.
Néanmoins, certaines opérations spéciales peuvent nécessiter d'être exécutées à la fin de vie d'un objet. Par exemple, une ressource du système d'exploitation possédée peut devoir être libérée ; une méthode d'un autre objet peut devoir être appelée ; un serveur tournant quelque part sur le réseau peut devoir être informé qu'une connexion va être fermée.
Le nom du destructeur est [c ~~this] et comme les constructeurs, n'a pas de type de retour.
[ = Le destructeur est appelé automatiquement
Le destructeur est exécuté dès que la vie de l'objet se termine. Comme vous pouvez vous en souvenir du chapitre sur les durées de vie et les opérations fondamentales, la durée de vie d'un objet se termine lorsque celui-ci quitte le bloc dans lequel il est défini.
La durée de vie d'une structure arrive à son terme lorsque l'on quitte le bloc de visibilité de l'objet, que ce soit normalement ou parce qu'une exception est lancée.
[code=d <<<
if (uneCondition) {
auto duree = Duree(7)
// ...
} // Le destructeur de 'duree' est appelé à cet endroit
>>>]
Les objets anonymes sont détruits à la fin de l'expression qui les a construits
[code=d <<<
moment.incrementer(Duree(5)); // l'objet Duree(5) est détruit
// à la fin de cette expression
>>>]
Tous les membres d'un objet structure sont détruits lorsque l'objet englobant est détruit.
]
[ = Exemple de destructeur
Concevons un type pour générer des documents XML simples. Les éléments XML sont définis par des chevrons. Ils peuvent contenir des données et d'autres éléments XML. Les éléments XML peuvent aussi avoir des attributs ; nous les ignorerons ici.
Notre but est de nous assurer qu'un élément qui a été ouvert par un tag [c <nom>] sera toujours fermé par un tag [c </nom>] :
[code <<<
<classe1> ← ouverture de l'élément XML extérieur
<note> ← ouverture de l'élément XML intérieur
57 ← les données
</note> ← fermeture de l'élément XML intérieur
</class1> ← fermeture de l'élément XML extérieur
>>>]
Une structure qui peut produire le résultat ci-dessus peut être définie par deux membres qui contiennent le tag de l'élément XML et le niveau d'indentation à utiliser lors de l'affichage.
[code=d <<<
struct ElementXML
{
string nom;
string indentation;
}
>>>]
Si les responsabilités d'ouverture et de fermeture de l'élément XML sont données au constructeur et au destructeur respectivement, le résultat attendu peut être produit en gérant les durées de vie des objets ElementXML. Par exemple, le constructeur peut afficher [c <balise>] et le destructeur peut afficher [c </balise>].
Voici la définition du constructeur pour produire la balise d'ouverture :
[code=d <<<
this(in string nom, in int niveau)
{
this.nom = nom;
this.indentation = chaineIndentation(niveau);
writeln(indentation, '<', nom, '>');
}
>>>]
Et voici la fonction chaineIndentation() :
[code=d <<<
import std.array;
// ...
string chaineIndentation(in int niveau)
{
return replicate(" ", niveau * 2);
}
>>>]
La fonction appelle [c replicate()] du module [c std.array] qui construit et renvoie une nouvelle chaine constituée de la valeur passée en paramètre répétée un certain nombre de fois.
Le destructeur peut être défini de la même manière que le constructeur afin de produire la balise fermante.
[code=d <<<
~this()
{
writeln(indentation, "</", nom, '>');
}
>>>]
Voici un code de test pour montrer l'effet des appels aux constructeur et destructeur automatique.
Voici un code de test pour montrer l'effet automatique des appels au constructeur et au destructeur.
[code=d <<<
import std.conv;
import std.random;
void main()
{
immutable classes = ElementXML("classes", 0);
foreach (idClasse; 0..2) {
immutable tagClasse = "classe" ~ to!string(idClasse);
immutable elementTag = ElementXML(tagClasse, 1);
foreach (i; 0..3)
{
immutable elementNote = ElementXML("note", 2);
immutable noteAleatoire = uniform(50, 101);
writeln(chaineIndentation(3), noteAleatoire);
}
}
}
>>>]
Notez que les objets [c ElementXML] sont créés dans trois blocs de portée dans le programme ci-dessus. Les tags d'ouverture et de fermeture des éléments XML sont produits seulement par le constructeur et le destructeur de ElementXML.
J'ai indiqué l'ouverture et la fermeture des tags des blocs extérieurs, intermédiaires et intérieurs par différentes couleurs :
[code=xml <<<
<classes>
<classe0>
<note>
72
</note>
<note>
97
</note>
<note>
90
</note>
</classe0>
<classe1>
<note>
77
</note>
<note>
87
</note>
<note>
56
</note>
</classe1>
</classes>
>>>]
L'élément classes est produit par la variable classes. Parce que cette variable est construite en premier dans la fonction [c main()], la sortie contient le résultat de sa construction est premier. Puisque c'est également la variable qui est détruite en dernier, avant de quitter [c main()], la sortie contient la sortie de l'appel du destructeur en dernier.
]
[ = Postcopie
Copier un objet consiste à construire un nouvel objet à partir d'un autre existant. La copie se déroule en deux étapes :
# La copie des membres de l'objet existant bit à bit. Cette étape est appelée ''blit'', labréviation anglaise de ''block transfer''.
# Faire d'autres ajustements sur le nouvel objet. Cette étape est appelée postcopie (''postblit'' en anglais).
La première étape est prise en charge automatiquement par le compilateur. Il copie les membres de l'objet existant vers les membres du nouvel objet :
[code=d <<<
auto dureeVoyageRetour = dureeVoyage; // copie
>>>]
Ne faites pas de confusion entre copie et affectation. Le mot-clé [c auto] ci-dessus est une indication qu'un nouvel objet est défini. Le nom du type réel aurait pu être renseigné à la place de auto.
Pour qu'une opération soit une affectation, l'objet à gauche doit être un objet existant. Par exemple, si a variable [c dureeVoyageRetour] avait déjà été défini :
[code=d <<<
dureeVoyageRetour = dureeVoyage; // affectation
>>>]
Il est parfois nécessaire de faire quelques ajustements aux membres du nouvel objet après le ''blit'' automatique. Ces opérations sont définies dans la fonction postcopie de la structure.
Puisqu'il s'agit de construction d'objet, le nom de la fonction postcopie est également [c this]. Pour le dissocier des autres constructeurs, sa liste de paramètre contient le mot-clé [c this] :
[code=d <<<
this(this)
{
// ...
}
>>>]
Nous avons défini un type [c Etudiant] dans le chapitre sur les structures, qui avait un problème sur la copie d'objets de ce type :
[code=d <<<
struct Etudiant
{
int nombre;
int[] notes;
}
>>>]
Étant une tranche, le membre notes de cette structure est un type référence. En conséquence, après la copie d'un objet [c Etudiant], les membres notes des deux objets (la copie et l'original) permettent l'accès aux même tableau d'entiers. La modification d'une note via un de ces objets est visible à travers l'autre objet :
[code=d <<<
auto etudiant1 = Etudiant(1, [70, 90, 85]);
auto etudiant2 = etudiant1; // copie
etudiant2.nombre = 2;
etudiant1.notes[0] += 5; // ceci change aussi la note du second etudiant
assert(etudiant2.notes[0] == 75);
>>>]
Pour éviter cette confusion, les éléments du membre notes du second objet doivent être séparés et n'appartenir qu'à cet objet. Ce genre d'ajustement est fait dans la postcopie :
[code=d <<<
struct Etudiant
{
int nombre;
int[] notes;
this(this)
{
notes = notes.dup;
}
}
>>>]
Souvenez-vous que tous les membres ont déjà été copiés automatiquement avant que [c this(this)] ne soit exécuté. La seule ligne de la postcopie ci-avant crée une copie des éléments du tableau original et en affecte une tranche à notes. Au final le nouvel objet obtient sa propre copie des notes.
Modifier les notes depuis le premier objet n'affecte plus le second objet :
[code=d <<<
etudiant1.notes[0] += 5;
assert(etudiant2.notes[0] == 70);
>>>]
]
[ = Désactiver la postcopie
La fonction postcopie peut également être désactivée avec [c @disable]. Les objets marqués ainsi ne peuvent être copiés :
[code=d <<<
struct Archive
{
// ...
@disable this(this);
}
// ...
auto a = Archive("enregistrements");
auto b = a; // Erreur de compilation
>>>]
Le compilateur n'autorise pas l'appel à la fonction postcopie désactivée
Erreur: struct Archive n'est pas copiable, car elle est annotée avec @disable
]
]
[ = Opérateur d'affectation
L'opérateur d'affectation donne une nouvelle valeur à un objet existant :
[code=d <<<
dureeVoyageRetour = dureeVoyage; // affectation
>>>]
L'affectation est plus compliquée que les autres fonctions spéciales parce qu'elle est une combinaison de deux opérations :
La destruction de l'objet de gauche
La copie de l'objet de droite vers l'objet de gauche.
Néanmoins, appliquer ces deux opérations dans cet ordre est risqué, car l'objet original serait détruit avant même de savoir que la copie sera un succès. Une exception lancée durant l'opération de copie pourrait laisser l'objet destination dans un état incohérent : pleinement détruit, mais en partie copié.
Pour cette raison, l'opérateur d'affectation automatiquement généré par le compilateur se montre prudent en procédant par étapes :
# Copie de l'objet de droite dans un temporaire.
C'est la moitié de l'opération d'affectation. Puisque l'objet de gauche n'est pas encore modifié, il restera intact si une exception est lancée durant cette opération.
# Destruction de l'objet de gauche.
C'est l'autre étape de l'opération d'affectation.
# Transfert de l'objet temporaire vers l'objet de gauche.
Aucune postcopie ni aucun destructeur ne sont exécutés durant cette opération. Au final, l'objet de gauche devient l'équivalent de l'objet temporaire.
Après les étapes ci-avant, l'objet temporaire disparaît et seuls l'objet de droite et sa copie (c'est-à-dire l'objet de gauche) restent.
Bien que l'opérateur d'affectation généré par le compilateur soit suffisant dans la plupart des cas, il peut être défini par le programmeur. Lorsque vous faites cela, gardez à l'esprit les causes potentielles d'exceptions et écrivez l'opérateur d'affectation pour qu'il se comporte correctement en présence d'exceptions.
La syntaxe de l'opérateur d'affectation est la suivante :
- Le nom de la fonction est opAssign.
- Le type du paramètre est le même que celui de la structure. Ce paramètre est souvent appelé [c rhs] (pour ''Right Hand Side'', côté droit en anglais).
- Le type de retour est le même que celui de la structure.
- La fonction se termine par [c return this].
Par exemple, considérons une structure [c Duree] simple, où l'opérateur d'affectation affiche un message :
[code=d <<<
struct Duree
{
int minute;
Duree opAssign(Duree rhs)
{
writefln("minute est modifié de %s à %s",
this.minute, rhs.minute);
this.minute = rhs.minute;
return this;
}
}
// ...
auto duree = Duree(100);
duree = Duree(200); // affectation
>>>]
Le résultat :
[output <<<
minute est modifié de 100 à 200.
>>>]
[ = Affectation d'autres types
Il est parfois commode d'affecter des valeurs de types différents du type de la structure. Par exemple, plutôt que de nécessiter la création d'un objet Duree sur le coté droit, il peut être utile d'affecter directement un entier :
[code=d <<<
duree = 300;
>>>]
C'est possible en définissant un autre opérateur d'affectation qui prend un entier en paramètre :
[code=d <<<
struct Duree
{
int minute;
Duree opAssign(Duree rhs)
{
writefln("minute est modifié de %s à %s",
this.minute, rhs.minute);
this.minute = rhs.minute;
return this;
}
Duree opAssign(int minute)
{
writefln("minute est remplacé par un entier");
this.minute = minute;
return this;
}
}
// ...
duree = Duree(200);
duree = 300;
>>>]
En résultat :
[output <<<
minute est modifié de 100 à 200
minute est remplacé par un entier
>>>]
Note : Bien que ce soit commode, affecter différents types entre eux peut semer la confusion et être source d'erreurs.
]
]
[ = En résumé
- Le constructeur ([c this]) prépare les objets à être utilisés. Le constructeur par défaut généré par le compilateur est suffisant dans la plupart des cas.
- Le comportement du constructeur par défaut ne peut être changé pour les structures. On peut néanmoins utiliser static opCall à la place.
- Les constructeurs avec un seul paramètre peuvent être utilisés pour les conversions de type avec to et cast.
- Le destructeur ([c ~~this]) exécute des opérations à la fin de vie d'un objet.
- La postcopie (this(this)) fait des ajustements à un objet après une copie automatique de ses membres.
- L'opérateur d'affectation (opAssign) permet la modification d'objets existants.
]