33. Boucle foreach ▲
Une des structures de contrôle les plus communes du D est la boucle foreach. Elle est utilisée pour appliquer la même opération à tous les éléments d'un conteneur (ou d'un intervalle).
Les opérations qui sont appliquées aux éléments d'un conteneur sont très répandues en programmation. Nous avons vu dans le chapitre sur la boucle forBoucles for que l'on peut accéder aux éléments d'un tableau dans une boucle for par une valeur d'indice qui est incrémentée à chaque itération :
for
(
int
i =
0
; i !=
tableau.length; ++
i) {
writeln
(
tableau[i]);
}
Voici les étapes d'une itération sur tous les éléments :
- définir une variable compteur, souvent nommée i ;
- itérer la boucle jusqu'à la valeur de la propriété .length du tableau ;
- incrémenter i ;
- accéder à l'élément.
foreach a essentiellement le même comportement, mais simplifie le code en gérant ces étapes automatiquement :
foreach
(
element; tableau) {
writeln
(
element);
}
Une partie du pouvoir de foreach vient du fait qu'elle peut être utilisée de la même manière indépendamment du type du conteneur. Comme nous l'avons vu dans le chapitre précédent, une manière d'itérer sur les valeurs d'un tableau associatif dans une boucle for est d'utiliser la propriété .values du tableau :
auto
valeurs =
aa.values;
for
(
int
i =
0
; i !=
valeurs.length; ++
i) {
writeln
(
valeurs[i]);
}
foreach ne nécessite rien de particulier pour les tableaux associatifs ; elle est utilisée de la même façon qu'avec les tableaux :
foreach
(
valeur; aa) {
writeln
(
valeur);
}
33-1. La syntaxe de foreach▲
foreach consiste en trois sections :
foreach
(
noms ; conteneur_ou_intervalle) {
// opérations
}
- conteneur_ou_intervalle indique où sont les éléments ;
- // opérations indique les opérations à appliquer à chaque élément ;
- noms indique le nom de l'élément et potentiellement d'autres variables dépendant du type du conteneur ou de l'intervalle. Même si le choix de noms appartient au programmeur, le nombre et les types de ces noms dépendent du type du conteneur.
33-2. continue et break▲
Ces mots-clés ont le même sens que celui qu'ils ont avec la boucle for : continue mène à l'itération suivante au lieu de finir celle qui est en cours et break sort de la boucle.
33-3. foreach avec les tableaux▲
Quand il y a un seul nom dans la section noms, c'est la valeur de l'élément à chaque itération :
foreach
(
element; tableau) {
writeln
(
element);
}
Quand deux noms sont indiqués dans la section noms, il y a respectivement un compteur automatique et la valeur de l'élément :
foreach
(
i, element; tableau) {
writeln
(
i, ": "
, element);
}
Le compteur est incrémenté automatiquement par foreach. Son nom est choisi par le programmeur.
33-4. foreach avec les chaînes et std.range.stride▲
Comme les chaînes sont des tableaux de caractères, foreach fonctionne de la même manière qu'avec les tableaux : un nom unique est le caractère, deux noms sont le compteur et le caractère :
foreach
(
c; "salut"
) {
writeln
(
c);
}
foreach
(
i, c; "salut"
) {
writeln
(
i, ": "
, c);
}
Cependant, étant des unités de stockage UTF, char et wchar itèrent sur les unités de stockage, pas sur les points de code Unicode :
foreach
(
i, code; "abcçd"
) {
writeln
(
i, ": "
, code);
}
On accède aux deux unités de stockage UTF-8 qui forment ç par des éléments séparés :
0
: a
1
: b
2
: c
3
:
4
: �
5
: d
Une manière d'itérer sur les caractères Unicode des chaînes dans une boucle foreach est la fonction stride du module std.range. stride présente la chaîne comme un conteneur constitué de caractères Unicode. Il prend la taille de son pas en second paramètre :
import
std.range;
// ...
foreach
(
c; stride
(
"abcçd"
, 1
)) {
writeln
(
c);
}
Quel que soit le type de caractère de la chaîne, stride présente toujours ses éléments comme des caractères Unicode :
a
b
c
ç
d
J'expliquerai plus loin pourquoi cette boucle ne peut pas inclure de compteur automatique.
33-5. foreach avec les tableaux associatifs▲
Un seul nom indique la valeur, deux noms indiquent la clé et la valeur :
foreach
(
valeur; ta) {
writeln
(
valeur);
}
foreach
(
clé, valeur; ta) {
writeln
(
clé, ": "
, valeur);
}
Les tableaux associatifs peuvent également donner leurs clés et leurs valeurs comme des intervalles. Nous verrons les intervalles dans un chapitre ultérieur. .byKey() et .byValue() retournent des objets intervalle efficaces qui sont aussi utiles dans d'autres contextes. .byValue() n'a pas grand intérêt dans les boucles foreach par rapport aux itérations classiques que l'on a déjà décrites. En revanche, .byKey() est la seule manière efficace d'itérer seulement sur les clés d'un tableau associatif :
foreach
(
clé; ta.byKey
(
)) {
writeln
(
clé);
}
33-6. foreach avec les intervalles de nombres▲
Nous avons vu les intervalles de nombres dans le chapitre « Tranches (slices) et autres fonctionnalités des tableaux ». Il est possible d'indiquer un intervalle de nombres dans la section conteneur_ou_intervalle :
foreach
(
nombre; 10
..15
) {
writeln
(
nombre);
}
Rappel : 10 est inclus dans l'intervalle, mais pas 15.
33-7. foreach avec les structures, les classes et les intervalles▲
foreach peut également être utilisé avec des objets d'un type de l'utilisateur qui définit sa propre itération dans les boucles foreach. Comme le type lui-même définit sa propre façon d'itérer, il n'est pas possible de dire grand-chose ici. Les programmeurs doivent se référer à la documentation de ce type particulier.
Les structures et les classes apportent une prise en charge de l'itération foreach soit avec leur méthode opApply(), soit par un ensemble de méthodes d'intervalle. Nous verrons ces fonctionnalités dans des chapitres ultérieurs.
33-8. Le compteur n'est automatique que pour les tableaux▲
Le compteur automatique est fourni seulement quand on itère sur les tableaux. Quand un compteur est nécessaire lors d'une itération sur d'autres types de conteneurs, le compteur peut être défini et incrémenté de façon explicite :
int
i;
foreach
(
element; conteneur) {
// ...
++
i;
}
Une telle variable est aussi nécessaire quand on compte une condition spécifique. Par exemple, le code suivant compte seulement les valeurs qui sont divisibles par 10 :
import
std.stdio;
void
main
(
)
{
auto
nombres =
[ 1
, 0
, 15
, 10
, 3
, 5
, 20
, 30
];
int
compteur;
foreach
(
nombre; nombres) {
if
((
nombre %
10
) ==
0
) {
++
compteur;
write
(
compteur);
}
else
{
write
(
' '
);
}
writeln
(
" : "
, nombre);
}
}
La sortie :
: 1
1
: 0
: 15
2
: 10
: 3
: 5
3
: 20
4
: 30
33-9. La copie de l'élément, pas l'élément lui-même▲
La boucle foreach fournit normalement une copie de l'élément, pas l'élément qui est stocké dans le conteneur. Ceci peut être la cause de bogues.
Pour voir un exemple de ceci, jetons un œil sur le programme suivant qui essaie de doubler les valeurs des éléments d'un tableau :
import
std.stdio;
void
main
(
)
{
double
[] nombres =
[ 1
.2
, 3
.4
, 5
.6
];
writefln
(
"Avant : %s"
, nombres);
foreach
(
nombre; nombres) {
nombre *=
2
;
}
writefln
(
"Après : %s"
, nombres);
}
La sortie du programme montre que l'affectation faite à chaque élément à l'intérieur du corps de foreach n'a aucun effet sur les éléments du conteneur :
Avant : [1
.2
, 3
.4
, 5
.6
]
Après : [1
.2
, 3
.4
, 5
.6
]
Ceci s'explique par le fait que nombre n'est pas un élément du tableau, mais une copie d'élément. Quand on a besoin de modifier les éléments eux-mêmes, le nom doit être défini comme une référence à l'élément par le mot-clé ref :
foreach
(
ref nombre; nombres) {
nombre *=
2
;
}
La nouvelle sortie montre que maintenant, les affectations modifient les éléments du tableau :
Avant : [1
.2
, 3
.4
, 5
.6
]
Après : [2
.4
, 6
.8
, 11
.2
]
Le mot-clé ref fait de nombre un alias de l'élément à chaque itération. De ce fait, les modifications apportées à nombre sont apportées à cet élément du conteneur.
33-10. L'intégrité du conteneur doit être préservée▲
Même s'il est correct de modifier les éléments du conteneur à travers des variables ref, la structure du conteneur ne doit pas changer. Par exemple, les éléments ne doivent pas être supprimés ou ajoutés au conteneur pendant une boucle foreach.
De telles modifications peuvent perturber le fonctionnement interne de l'itération de la boucle et mettre le programme dans un état incohérent.
33-11. foreach_reverse pour itérer dans la direction inverse▲
foreach_reverse fonctionne de la même manière que foreach, mais itère dans la direction inverse :
auto
conteneur =
[ 1
, 2
, 3
];
foreach_reverse (
element; conteneur) {
writefln
(
"%s "
, element);
}
La sortie :
3
2
1
L'utilisation de foreach_reverse n'est pas répandue parce que la fonction d'intervalle retro() fait la même chose. Nous verrons cette fonction dans un chapitre suivant.
33-12. Exercice▲
Nous savons que les tableaux associatifs proposent une relation de type clé-valeur. Cette relation est unidirectionnelle : on accède aux valeurs par les clés, mais ce n'est pas vrai dans l'autre sens.
Supposons que l'on ait ce tableau associatif :
string[int
] noms =
[ 1
:"un"
, 7
:"sept"
, 20
:"vingt"
];
Utilisez ce tableau associatif et une boucle foreach pour remplir un tableau associatif nommé valeurs. Ce nouveau tableau associatif doit donner les valeurs qui correspondent aux noms. Par exemple, la ligne suivante devrait afficher 20 :
writeln
(
valeurs["vingt"
]);