programmez-en-d/struct.whata

783 lines
32 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 = "Structures"
partAs = chapitre
translator = "Raphaël Jakse"
proofreader = "Stéphane Goujet"
]
Comme nous l'avons vu plusieurs fois dans les précédents chapitres de ce livre, les types fondamentaux ne sont pas suffisants pour représenter des concepts de plus haut niveau. Par exemple, même si une valeur de type [c int] peut être utilisée pour représenter une heure de la journée, deux variables [c int] seraient ensemble plus appropriées pour représenter un point dans le temps~ : une pour l'heure, une pour la minute.
Les structures sont la fonctionnalité qui permet de définir de nouveaux types en combinant d'autres types déjà existant. Le nouveau type est défini par le mot-clé [c struct]. Le gros de ce chapitre est également directement applicable aux classes~ ; en particulier, l'idée de [i combiner les types existant pour définir un nouveau type] est exactement la même pour celles-ci.
Ce chapitre ne couvre que les fonctionnalités basiques des structures. Nous en verrons plus sur les structures dans les chapitres suivant:
- [[part:fonctions_membres]]
- [[part:fonctions_membres_const]]
- [[part:fonctions_speciales]]
- [[part:surcharge_operateurs]]
- [[part:encapsulation]]
- [[part:propriete]]
- [[part:invariant]]
- [[part:foreach_opapply]]
Pour comprendre l'utilité des structures, considérons la fonction [c ajouterDuree] que nous avons définie plus tôt dans le [[doc:assert | chapitre sur les assertions et [c enforce]]]. La définition suivante est tirée des solutions de l'exercice de ce chapitre~ :
[code=d <<<
void ajouterDuree(in int heuresDepart, in int minutesDepart,
in int dureeHeures, in int dureeMinutes,
out int heuresResultat, out int minutesResultat)
{
heuresResultat = heuresDepart + dureeHeures;
minutesResultat = minutesDepart + dureeMinutes;
heuresResultat += minutesResultat / 60;
minutesResultat %= 60;
heuresResultat %= 24;
}
>>>]
[p Note~ : | je vais ignorer les blocs [c in], [c out] et [c unittest] dans ce chapitre pour garder les codes exemples courts.]
Même s'il est clair que cette fonction prend six paramètres, quand on regarde les trois paires de paramètres de plus près, elle ne prend que trois informations~ : le moment de départ, la durée et le résultat.
[ = Définition
Le mot-clé [c struct] définit un nouveau type en combinant des variables qui ont un certain rapport~ :
[code=d <<<
struct MomentDeLaJournee
{
int heure;
int minute;
}
>>>]
Ce code définit un nouveau type nommé [c MomentDeLaJournee], qui consiste en deux variables nommées [c heure] et [c minute]. Cette définition permet d'utiliser le nouveau type [c MomentDeLaJournee] dans le programme comme n'importe quel autre type. Le code suivant démontre comment son utilisation est similaire à celle d'un [c int]~ :
[code=d <<<
int nombre; // une variable
nombre = autreNombre; // prenant la valeur de autreNombre
MomentDeLaJournee moment; // un objet
moment = autreMoment; // prenant la valeur de autreMoment
>>>]
La syntaxe de la définition [c struct] est la suivante~ :
[code=d <<<
struct NomType
{
// ... fonctions et variables membres
}
>>>]
Nous verrons les fonctions membres dans des chapitres ultérieurs.
Les variables qu'une structure combine sont appelés ses [* membres]. Selon cette définition, [c MomentDeLaJournee] a deux membres~ : [c heure] et [c minute].
[ = [c struct] définit un type, pas une variable
Il y a une distinction importante ici : surtout après les chapitres sur les [[doc:espace_de_nom | Espaces de Noms] et sur [[doc:durees_vie | les Durées de Vie et Opérations Fondamentales]], les accolades des définitions de structures peuvent donner la fausse impression que les membres des structures commencent et finissent leurs vies à l'intérieur de ce bloc. Ce n'est pas le cas.
Les définitions de membres ne sont pas des definitions de variables :
[comment <<< La structure des paragraphes et exemples qui suivent légèrement changé en VO. >>>]
[code=d <<<
struct MomentDeLaJournee
{
int heure; // ← pas une définition de variable !
int minute; // ← pas une définition de variable !
}
>>>]
La définition d'une structure détermine les types et les noms des membres que les objets de cette structure auront~ :
[code=d <<<
struct MomentDeLaJournee
{
int heure; // ← fera partie d'une variable
int minute; // ← fera partie d'une variable
}
>>>]
Ces variables membres seront construites comme parties des objets [c MomentDeLaJournee] qui seront utilisés dans le programme~ :
[code=d <<<
MomentDeLaJournee sieste; // Cet objet contient ses propres
// variables membres heure et minute.
MomentDeLaJournee reveil; // Cet objet contient également ses propres
// variables membres heure et minute.
// Les variables membres de cet objet
// n'ont pas de rapport avec les variables
// membres de l'objet précédent.
>>>]
]
[ = Confort de programmation
La possibilité de combiner les idées d'heure et de minute ensemble dans un nouveau type est très pratique. Par exemple, la fonction précédente peut être réécrite de façon plus parlante en prenant trois paramètres de type [c MomentDeLaJournee] au lieu des six paramètres existant~ :
[code=d <<<
void ajouterDuree(in MomentDeLaJournee depart,
in MomentDeLaJournee duree,
out MomentDeLaJournee resultat)
{
// ...
}
>>>]
[p Note~ : | il n'est pas normal d'ajouter deux variables qui représentent deux points du temps. Par exemple, il n'y a pas de sens à ajouter le moment du déjeuner (12h00) au moment du petit-déjeuner (7h30). Il serait plus sensé de définir un autre type, convenablement appelé [c Duree], et d'ajouter les objets de ce type aux objets [c MomentDeLaJournee]. Malgré cette erreur de conception, on continuera d'utiliser seulement [c MomentDeLaJournee] dans ce chapitre et on introduira [c Duree] dans un chapitre ultérieur.]
Comme nous l'avons vu, les fonction retournent au plus une valeur. Ceci était précisément la raison pour laquelle la fonction [c ajouterDuree] prenait deux paramètres résultats ([c out])~ : elle ne pouvait pas retourner à la fois l'heure et la minute.
Les structures enlèvent cette limitation : comme de multiples valeurs peuvent être combinées en un seul type [c struct], les fonction peuvent retourner un objet d'une telle structure, en retournant effectivement plusieurs valeurs à la fois. La fonction [c ajouterDuree] peut maintenant retourner son résultat~ :
[code=d <<<
MomentDeLaJournee ajouterDuree(in MomentDeLaJournee depart,
in MomentDeLaJournee duree)
{
// ...
}
>>>]
Par conséquent, [c ajouterDuree] devient une fonction qui produit une valeur, au lieu d'être une fonction qui a des effets de bord. Comme vu dans le [[doc:fonctions | chapitre sur les fonctions], il est préférable de produire des valeurs plutôt qu'avoir des effets de bord.
Les structures peuvent être des membres d'autres structures. Par exemple, la structure suivante a deux membres de type [c MomentDeLaJournee]~ :
[©ode=d <<<
struct Reunion
{
string sujet;
size_t nombreDeParticipants;
MomentDeLaJournee debut;
MomentDeLaJournee fin;
}
>>>]
La structure ``Reunion`` peut à son tour servir de membre d'une autre structure. En supposant qu'on ait aussi une structure [c Repas]~ :
[code=d <<<
struct planningDeLaJournee
{
Reunion reunionDeProjet;
Repas dejeuner;
Reunion reunionBudget;
}
>>>]
]
]
[ = Accéder aux membres
Les membres de structures peuvent être utilisés comme n'importe quelle autre variable. La seule différence est que le nom de la structure et un [* point] doivent être indiqués avant le nom du membre~ :
[code=d <<<
depart.heure = 10;
>>>]
La ligne ci-dessus affecte la valeur 10 au membre [c heure] de l'objet [c depart].
Réécrivons la fonction [c ajouterDuree] avec ce que nous avons vu jusqu'ici~ :
[code=d <<<
MomentDeLaJournee ajouterDuree(in MomentDeLaJournee depart,
in MomentDeLaJournee duree)
{
MomentDeLaJournee resultat;
resultat.minute = depart.minute + duree.minute;
resultat.heure = depart.heure + duree.heure;
resultat.heure += resultat.minute / 60;
resultat.minute %= 60;
resultat.heure %= 24;
return resultat;
}
>>>]
On remarque que les noms des variables sont maintenant beaucoup plus courts dans cette version de la fonction~ : [c depart], [c duree] et [c resultat]. De plus, au lieu d'utiliser des noms complexes comme [c heuresDepart], il est possible d'accéder aux membres des structures à travers leurs structures respectives comme dans [c depart.heure].
Voici un code qui utilise la nouvelle fonction [c ajouterDuree]. Étant donné le moment de début et la durée, le code suivant calcule l'horaire de fin d'un cours dans une école~ :
[code=d <<<
void main()
{
MomentDeLaJournee debutCours;
debutCours.heure = 8;
debutCours.minute = 30;
MomentDeLaJournee dureeCours;
dureeCours.heure = 1;
dureeCours.minute = 15;
immutable finCours = ajouterDuree(debutCours, dureeCours);
writefln("Fin du cours : %sh%s", finCours.heure, finCours.minute);
}
>>>]
La sortie~ :
[output <<<
Fin du cours : 9h45
>>>]
La fonction [c main] a été écrite en utilisant uniquement ce que nous avions vu jusqu'à maintenant. Nous rendrons bientôt ce code encore plus clair et succinct.
]
[ = Construction
Les trois premières lignes de [c main] servent à construire l'objet [c debutCours] et les trois lignes suivantes servent à construire l'objet [c dureeCours]. Dans chacun de ces blocs de 3 lignes, un objet est d'abord défini, et ensuite ses membres ``heure`` et ``minute`` sont affectés.
Pour qu'une variable soit utilisée de manière sûre, cette variable doit d'abord être construite dans un état cohérent. La construction étant una action très commune, il existe une syntaxe spéciale pour construire les structures~ :
[code=d <<<
MomentDeLaJournee debutCours = MomentDeLaJournee(8, 30);
MomentDeLaJournee dureeCours = MomentDeLaJournee(1, 15);
>>>]
Les valeurs sont automatiquement affectées aux membres dans l'ordre dans lequel ils sont définis : comme [c heure] est défini en premier dans la structure, la valeur 8 est affectée à [c debutCours.heure] et 30 est affecté à [c debutCours.minute].
Comme nous le verrons dans un [[part:cast | chapitre ultérieur]], la syntaxe de construction peut aussi être utilisée pour d'autres types~ :
[code=d <<<
auto u = ubyte(42); // u est un ubyte
auto i = int(u); // i est un int
>>>]
[ = Construire des objets [c immutable]
Pouvoir construire l'objet en spécifiant les valeurs de ses membres au même moment permet de définir des objets [c immutable]~ :
[code=d <<<
immutable debutCours = MomentDeLaJournee(8, 30);
immutable dureeCours = MomentDeLaJournee(1, 15);
>>>]
Autrement, il ne serait pas possible de marquer un objet comme [c immutable] et ensuite de modifier ses membres~ :
[code=d <<<
immutable MomentDeLaJournee debutCours;
debutCours.heure = 8; // ERREUR de compilation
debutCours.minute = 30; // ERREUR de compilation
>>>]
]
[ = Les membres restant n'ont pas besoin d'être spécifiés
Il peut y avoir moins de valeurs spécifiées qu'il n'y a de membres. Dans ce cas, les membres restant sont initialisés par les valeurs [c .init] de leur type respectif.
Le programme suivant construit des objets [c Test] avec un paramètre de moins à chaque construction. Les assertions indiquent que les membres non spécifiés sont automatiquement initialisés par leur valeur [c .init] (l'utilisation de la fonction [c isNaN] est expliquée après le programme)~ :
[code=d <<<
import std.math;
struct Test
{
char c;
int i;
double d;
}
void main()
{
// Les valeurs initiales de tous les membres sont spécifiées
auto t1 = Test('a', 1, 2.3);
assert(t1.c == 'a');
assert(t1.i == 1);
assert(t1.d == 2.3);
// Il manque la dernière
auto t2 = Test('a', 1);
assert(t2.c == 'a');
assert(t2.i == 1);
assert(isNaN(t2.d));
// Il manque les deux dernières
auto t3 = Test('a');
assert(t3.c == 'a');
assert(t3.i == int.init));
assert(isNaN(t3.d));
// Aucune valeur initiale n'est spécifiée
auto t4 = Test();
assert(t4.c == char.init);
assert(t4.i == int.init);
assert(isNaN(t4.d));
// Identique à la précédente
Test t5;
assert(t5.c == char.init);
assert(t5.i == int.init);
assert(isNaN(t5.d));
}
>>>]
Comme nous l'avons vu dans le [[doc:virgule_flottante | chapitre sur les virgules flottantes]], la valeur initiale de [c double] est [c double.nan]. Comme la valeur [c .nan] n'est pas ordonnée, l'utiliser dans des comparaisons d'égalité n'a pas de sens. C'est pourquoi appeler [c std.math.isNaN] est la manière correcte de déterminer si une valeur est égale à [c .nan] ou non.
]
[ = Spécifier les valeurs par défaut des membres
Il est important que les variables membres soient automatiquement initialisées avec des valeurs initiales connues. Ceci évite au programme de continuer avec des valeurs indéterminées. Cependant, la valeur [c .init] de leurs types respectifs peut ne pas convenir pour tous les types. Par exemple, [c char.init] n'est même pas une valeur valide.
Les valeurs initiales des membres d'une structure peuvent être spécifiées lors de la définition de la structure. Ceci peut servir, par exemple, à initialiser les membres à virgule flottante à [c 0.0], plutôt qu'à [c .nan] qui est souvent inexploitable.
Les valeurs par défaut sont spécifiées avec la syntaxe de l'affectation lors de la définition des membres~ :
[code=d <<<
struct Test
{
char c = 'A';
int i = 11;
double d = 0.25;
}
>>>]
Veuillez noter que cette syntaxe n'est pas vraiment une affectation. Le code se contente de déterminer les valeurs par défaut qui seront utilisées quand les objets de cette structure seront construits plus tard dans le programme.
Par exemple, l'objet [c Test] suivant est construit sans valeur spécifique~ :
[code=d <<<
Test t; // aucune valeur n'est spécifiée pour les membres de t
writefln("%s,%s,%s", t.c, t.i, t.d);
>>>]
Tous les membres sont initialisés avec leur valeur par défaut~ :
[code=bash <<<
A,11,0.25
>>>]
]
[ = Construction avec la syntaxe [c { }]
Les structures peuvent aussi être construites avec la syntaxe suivante~ :
[code=d <<<
MomentDeLaJournee debutCours = { 8, 30 };
>>>]
Comme avec la syntaxe précédente, les valeurs indiquées sont affectées aux membres dans l'ordre dans lequel ceux-ci sont définis. Les membres restant seront initialisés à leurs valeurs par défaut.
Cette syntaxe est héritée du langage C~ :
[code=d <<<
auto debutCours = MomentDeLaJournee(8, 30); // ← recommandé
MomentDeLaJournee finCours = { 9, 30 }; // ← façon C
>>>]
Cette syntaxe autorise les initialisateurs désignés. Les initialisateurs désignés sont utilisés pour spécifier le membre associé à une valeur d'initialisation. Il est même possible d'initialiser les membres dans un ordre différent de celui dans lequel ils sont définis dans la structure~ :
[code=d <<<
MomentDeLaJournee m = { minute: 42, heure: 7 };
>>>]
]
]
[ = Copie et affectation
Les structures sont des types valeur. Comme décrit dans le [[doc:valeur_vs_reference | chapitre sur les types valeur et types référence]], ceci veut dire que chaque objet [c struct] a sa propre valeur. Les objets reçoivent leurs propres valeurs quand ils sont construits et leurs valeurs changent quand on leur affecte de nouvelles valeurs.
[code=d <<<
auto votreHeureDuDejeuner = MomentDeLaJournee(12, 0);
auto monHeureDuDejeuner = votreHeureDuDejeuner;
// Seul monHeureDuDejeuner passe à 12h05 :
monHeureDuDejeuner.minute += 5;
// ... votreHeureDuDejeuner reste inchangé :
assert(yourLunchTime.minute == 0);
>>>]
Pendant une copie, tous les membres de l'objet source sont automatiquement copiés vers les membres correspondant dans l'objet de destination. De manière similaire, l'affectation implique l'affectation de chaque membre de la source au membre de destination correspondant.
Les membres de structures qui sont d'un type référence demandent une attention particulière.
[ = Attention avec les membres qui sont de types référence !
Comme vous le savez, le fait de copier ou d'affecter les variables de types référence ne modifie aucune valeur, cela ne fait que changer [* quel] objet est référencé. Ainsi, copier ou affecter une structure qui a des membres de type référence va créer dans l'objet de destination des références supplémentaires aux objets référencés dans l'objet source. Ceci implique que des membres de deux structures distinctes peuvent donner accès à la même valeur.
Pour voir un exemple de ceci, regardons une structure dont l'un des membres est de type référence. Cette structure est utilisée pour stocker le numéro et les notes d'un étudiant~ :
[code=d <<<
struct Etudiant
{
int numero;
int[] notes;
}
>>>]
Le code suivant construit un second objet [c Etudiant] en copiant un objet existant~ :
[code=d <<<
// On construit le premier objet :
auto etudiant1 = Etudiant(1, [ 70, 90, 85 ]);
// On construit le deuxième objet en copiant le premier
// et en changeant le numéro :
auto etudiant2 = etudiant1;
etudiant2.numero = 2;
// AVERTISSEMENT: Les notes sont maintenant partagées par les deux objets !
// Changer les notes du premier étudiant...
etudiant1.notes[0] += 5;
// ... affecte les notes du second étudiant :
writeln(etudiant2.notes[0]);
>>>]
Quand [c etudiant2] est construit, ses membres prennent les valeurs des membres de [c etudiant1]. Comme [c int] est un type valeur, le second objet obtient sa propre valeur [c numero].
Les deux objets [c Etudiant] ont aussi un membre [c notes] chacun. Cependant, comme les tranches sont des types référence, les éléments que les tranches partagent sont les mêmes. Ainsi, une modification faite depuis une des tranches sera reflétée sur l'autre tranche.
La sortie du code indique que la note du second étudiant a été augmentée également~ :
[output <<<
75
>>>]
Pour cette raison, une meilleure approche serait de construire le deuxième objet en copiant les notes du premier~ :
[code=d <<<
// Le deuxième Etudiant est construit en copiant les notes du premier :
auto etudiant2 = Etudiant(2, etudiant1.notes.dup);
// Changer les notes du premier étudiant...
etudiant1.notes[0] += 5;
// ... n'affecte pas les notes du deuxième :
writeln(etudiant2.notes[0]);
>>>]
Comme les notes ont été copiées avec [c .dup], les notes du deuxième étudiant ne sont cette fois pas affectées~ :
[output <<<
70
>>>]
[p (Note : | il existe néanmoins une possibilité de faire en sorte que les membres références soit également copiés. Nous verrons comment plus tard, quand on abordera les membres fonctions.)]
]
[ = Littéraux structure
De la même manière que l'on peut utiliser des valeurs littérales entières (comme 10) dans les expressions sans avoir besoin de définir de variable, des littéraux de structures peuvent également être utilisés~ :
Les littéraux de structures sont construits avec la syntaxe de construction d'objets.
[code=d <<<
MomentDeLaJournee(8, 30) // ← littéral de structure
>>>]
Réécrivons la dernière fonction [c main()] que nous avons défini, avec ce qu'on a appris depuis sa dernière version. Les variables sont construites avec la syntaxe de construction et sont cette fois immuables~ :
[code=d <<<
void main()
{
immutable debutCours = MomentDeLaJournee(8, 30);
immutable dureeCours = MomentDeLaJournee(1, 15);
immutable finCours = ajouterDuree(debutCours,
dureeCours);
writefln("Fin du cours : %s:%s",
finCours.heure, finCours.minute);
}
>>>]
Notez qu'il n'est pas nécessaire de définir [c debutCours] et [c dureeCours] comme des variables nommées dans ce cas. Elles sont en fait des variables temporaires dans ce programme simple, qui sont utilisées seulement pour calculer la variable [c finCours]. Elles pourraient être passées à [c ajouterDuree()] comme des valeurs littérales~ :
[code=d <<<
void main()
{
immutable finCours = ajouterDuree(MomentDeLaJournee(8, 30),
MomentDeLaJournee(1, 15));
writefln("Fin du cours : %s:%s",
finCours.heure, finCours.minute);
}
>>>]
]
[ = Membres [c static]
Même si les objets ont surtout besoin de copies individuelles de leurs membres, il peut être utile pour un type de structure particulier de partager certaines variables. Ce peut être nécessaire quand par exemple une information générale sur ce type de structure a besoin d'être maintenue.
Par exemple, imaginons un type qui affecte un identificateur différent pour chaque objet de ce type~ :
[code=d <<<
struct Point
{
// L'identificateur de chaque objet
size_t id;
int ligne;
int colonne;
}
>>>]
Afin de pouvoir affecter un identificateur différent à chaque objet, il faut une variable distincte pour garder la prochaine valeur que l'on peut utiliser, variable qui sera incrémentée à chaque foit qu'un nouvel objet est créé. Supposons que [c idSuivant] est définie autre part et accessible à la fonction suivante~ :
[code=d <<<
Point creerPoint(int ligne, int colonne)
{
size_t id = idSuivant;
++idSuivant;
return Point(id, ligne, colonne);
}
>>>]
Une décision doit être prise quant à l'endroit où l'on définit la variable commune [c idSuivant]. Les membres statiques sont utiles dans de tels cas. Une information de cette sorte est définie comme un membre statique ([c static]) de la structure. Contrairement aux membres classiques, il y a une seule variable pour chaque membre statique dans tout le fil d'exécution. Cette variable unique est partagée par tous les objets de cette structure~ :
[code=d <<<
import std.stdio;
struct Point
{
// L'identificateur de chaque objet
size_t id;
int ligne;
int colonne;
// L'identificateur de l'objet suivant
static size_t idSuivant;
}
Point creerPoint(int ligne, int colonne)
{
size_t id = Point.idSuivant;
++Point.idSuivant;
return Point(id, ligne, colonne);
}
void main()
{
auto haut = creerPoint(7, 0);
auto milieu = creerPoint(8, 0);
auto bas = creerPoint(9, 0);
writeln(haut.id);
writeln(milieu.id);
writeln(bas.id);
}
>>>]
Comme [c idSuivant] est incrémenté lors de la construction de chaque objet, les objets ont chacun un identificateur unique~ :
[output <<<
0
1
2
>>>]
Comme les membres statiques appartiennent au type lui-même, il n'y a pas besoin d'objet pour y accéder. Comme nous l'avons vu ci-dessus, on peut accéder à de tels objets par le nom du type, mais on peut aussi bien le faire à travers le nom de tout objet de ce type~ :
[code=d <<<
++Point.idSuivant;
++bas.idSuivant; // même effet que la ligne précédente
>>>]
]
[ = [c static this()] pour l'initialisation et [c static ~~this()] pour la finalisation
Au lieu d'affecter explicitement une valeur initiale à [c idSuivant] ci-dessus, nous avons utilisé sa valeur initiale par défaut, zéro. Nous aurions pu utiliser n'importe quelle autre valeur~ :
[code=d <<<
static size_t idSuivant = 1000;
>>>]
Cependant, une telle initialisation est possible seulement quand la valeur initiale est connue lors de la compilation. De plus, certains codes spéciaux peuvent nécessiter d'être exécutés avant qu'une structure soit utilisée dans un fil d'exécution. De tels codes peuvent être écrits dans des portées [c static this()].
Par exemple, le code suivant lit la valeur initiale dans un fichier s'il existe~ :
[code=d <<<
import std.file;
struct Point {
// ...
enum fichierIdSuivant = "Point_fichier_id_suivant"
static this() {
if (exists(fichierIdSuivant)) {
auto fichier = File(fichierIdSuivant, "r");
fichier.readf(" %s", &idSuivant);
}
}
}
>>>]
Le contenu des blocs [c static this()] est exécuté une fois par fil d'exécution avant que le type de struct soit utilisé dans ce fil d'exécution. Le code qui devrait être exécuté seulement une fois durant toute l'exécution du programme (par exemple, pour initialiser des variables [c shared] ou [c immutable]) doivent être définis dans des blocs [c shared static this()] et [c shared static ~this()], qui seront vus dans le [[part:concurrence_shared | chapitre sur les données partagées et la concurrence]].
De manière similaire, [c static ~this()] est utilisé pour les opérations finales d'un fil d'exécution et [c shared static ~this()] est utilisé pour les opérations finales du programme entier.
L'exemple suivant complète le bloc [c static this()] en écrivant la valeur de [c idSuivant] dans le même fichier, ce qui permet de rendre les identifiants des objets persistants entre les exécutions du programme~ :
[code=d <<<
struct Point {
// ...
static ~this() {
auto fichier = File(fichierIdSuivant, "w");
file.writeln(idSuivant);
}
}
>>>]
Le programme initialise maintenant [c idSuivant] à partir de l'identifiant de l'exécution précédente. Par exemple, ce qui suit est la sortie de la deuxième exécution du programme~ :
[output <<<
3
4
5
>>>]
]
[ = Exercices
# Concevez une structure [c Carte] pour représenter une carte à jouer.
Cette structure peut avoir deux membres~ : un pour la couleur et un pour la valeur. Il peut être judicieux d'utiliser un [c enum] pour représenter la couleur, ou vous pouvez simplement utiliser les caractères ♠, ♡, ♢ et ♣.
Une valeur [c int] ou [c dchar] peut être utilisée pour la valeur de la carte. Si vous décidez d'utiliser un [c int], les valeurs 1, 11, 12 et 13 peuvent représenter les cartes qui ne sont pas des nombres (as, valet, dame et roi).
Il y a d'autres choix de conception à faire. Par exemple, les valeurs des cartes peuvent aussi être représentées par un type [c enum].
La manière dont les objets de cette structure peuvent être construits dépendra des choix de types de ses membres. Par exemple, si les deux membres sont des [c dchar], alors les objects de type [c Carte] pourront être construits ainsi~ :
[code=d <<<
auto carte = Carte('♣', '2');
>>>]
# Définissez une fonction nommée [c afficherCarte()], qui prend un objet carte en paramètre et l'affiche~ :
[code=d <<<
struct Carte
{
// ... définir la structure ...
}
void afficherCarte(in Carte carte)
{
// ... écrire le corps de la fonction ...
}
void main()
{
auto carte = Carte(/* ... */);
afficherCarte(carte);
}
>>>]
Par exemple, la fonction peut afficher le deux de trèfle de cette manière~ :
[output <<<
♣2
>>>]
L'implémentation de cette fonction peut dépendre des choix de types des membres.
# Définissez une fonction nommée [c nouveauJeu()] qui retourne les 52 cartes d'un jeu en tant que tranche d'objets Carte~ :
[code=d <<<
Carte[] nouveauJeu()
out (resultat)
{
assert(resultat.length == 52);
}
body
{
// ... écrire le corps de la fonction ...
}
>>>]
Il doit être possible d'appeler [c nouveauJeu()] comme dans le code suivant~ :
[code=d <<<
void main()
{
Carte[] jeu = nouveauJeu();
foreach (carte; jeu) {
afficherCarte(carte);
write(' ');
}
writeln();
}
>>>]
La sortie devrait ressembler à ce qui suit, avec 52 cartes distinctes~ :
[output <<<
♠2 ♠3 ♠4 ♠5 ♠6 ♠7 ♠8 ♠9 ♠0 ♠V ♠D ♠R ♠A ♡2 ♡3 ♡4
♡5 ♡6 ♡7 ♡8 ♡9 ♡0 ♡V ♡D ♡R ♡A ♢2 ♢3 ♢4 ♢5 ♢6 ♢7
♢8 ♢9 ♢0 ♢V ♢D ♢R ♢A ♣2 ♣3 ♣4 ♣5 ♣6 ♣7 ♣8 ♣9 ♣0
♣V ♣D ♣R ♣A
>>>]
# Écrivez une fonction qui mélange le jeu. Une manière de le faire est de prendre deux cartes au hasard avec [c std.random.uniform], les échanger et répéter ce processus un nombre de fois suffisant. La fonction devrait prendre le nombre de répétitions en paramètre~ :
[code=d <<<
void melanger(Carte[] jeu, in int repetitions)
{
// ... écrire le corps de la fonction ...
}
>>>]
Voici comment elle doit pouvoir être utilisée~ :
[code=d <<<
void main()
{
Carte[] jeu = nouveauJeu();
melanger(jeu, 1);
foreach (carte; jeu) {
afficherCarte(carte);
write(' ');
}
writeln();
}
>>>]
La fonction doit échanger les cartes [c répétitions] fois. Par exemple, un appel avec 1 doit donner une sortie similaire à ceci~ :
[output <<<
♠2 ♠3 ♠4 ♠5 ♠6 ♠7 ♠8 ♠9 ♠0 ♠V ♠D ♠R ♠A ♡2 ♡3 ♡4
♡5 ♡6 ♡7 ♡8 ♣4 ♡0 ♡V ♡D ♡R ♡A ♢2 ♢3 ♢4 ♢5 ♢6 ♢7
♢8 ♢9 ♢0 ♢V ♢D ♢R ♢A ♣2 ♣3 ♡9 ♣5 ♣6 ♣7 ♣8 ♣9 ♣0
♣V ♣D ♣R ♣A
>>>]
Une valeur plus grande de [c repetitions] devrait résulter en un jeu plus mélangé~ :
[code=d <<<
melanger(jeu, 100);
>>>]
La sortie~ :
[output <<<
♠4 ♣7 ♢9 ♢6 ♡2 ♠6 ♣6 ♢A ♣5 ♢8 ♢3 ♡D ♢V ♣R ♣8 ♣4
♡V ♣D ♠D ♠9 ♢0 ♡A ♠A ♡9 ♠7 ♡3 ♢R ♢2 ♡0 ♠V ♢7 ♡7
♠8 ♡4 ♣V ♢4 ♣0 ♡6 ♢5 ♡5 ♡R ♠3 ♢D ♠2 ♠5 ♣2 ♡8 ♣A
♠R ♣9 ♠0 ♣3
>>>]
[p Note : | une meilleure façon de mélanger le jeu est expliquée dans les solutions.]
[[part:corrections/struct | … Les solutions]]
]