Doubles possibles:
Alors que contre
Quand dois-je utiliser les boucles do-while au lieu de while?
Je programme depuis un certain temps maintenant (2 ans de travail + 4,5 ans de diplôme + 1 an de pré-universitaire) et je n'ai jamais utilisé une boucle continue tant que le cours d'introduction à la programmation ne m'y obligeait pas. J'ai de plus en plus le sentiment que je programme mal si je ne rencontre jamais quelque chose d'aussi fondamental.
Se pourrait-il que je ne sois tout simplement pas dans les bonnes circonstances?
Quels sont quelques exemples où il serait nécessaire d’utiliser un do-while au lieu d’un temps?
(Ma formation était presque entièrement en C/C++ et mon travail est en C #, donc s'il y a un autre langage où cela a du sens, parce que les méthodes fonctionnent différemment, ces questions ne s'appliquent pas vraiment.)
Pour clarifier ... Je connais la différence entre un while
et un do-while
. While vérifie la condition de sortie, puis effectue des tâches. do-while
effectue des tâches puis vérifie la condition de sortie.
Si vous voulez toujours que la boucle s'exécute au moins une fois. Ce n'est pas courant, mais je l'utilise de temps en temps. Vous voudrez peut-être l’utiliser pour essayer d’accéder à une ressource qui pourrait nécessiter une nouvelle tentative, par exemple.
do
{
try to access resource...
put up message box with retry option
} while (user says retry);
do-while est préférable si le compilateur n'est pas compétent en optimisation. do-while n'a qu'un seul saut conditionnel, par opposition à for et while qui ont un saut conditionnel et un saut inconditionnel. Pour les CPU qui sont en pipeline et qui ne font pas de prédiction de branche, cela peut faire une grande différence dans les performances d'une boucle serrée.
De plus, étant donné que la plupart des compilateurs sont suffisamment intelligents pour effectuer cette optimisation, toutes les boucles trouvées dans le code décompilé seront généralement faites de temps à autre (si le décompilateur veut même reconstruire des boucles à partir de bases de données locales en arrière).
Faire pendant est utile lorsque vous voulez exécuter quelque chose au moins une fois. En ce qui concerne un bon exemple d’utilisation de do tant que contre tant, disons que vous voulez faire ce qui suit: Une calculatrice.
Vous pouvez résoudre ce problème en utilisant une boucle et en vérifiant après chaque calcul si la personne souhaite quitter le programme. Maintenant, vous pouvez probablement supposer qu'une fois le programme ouvert, la personne souhaite le faire au moins une fois pour pouvoir effectuer les opérations suivantes:
do
{
//do calculator logic here
//Prompt user for continue here
} while(cont==true);//cont is short for continue
Je l'ai utilisé dans une fonction TryDeleteDirectory. C'était quelque chose comme ça
do
{
try
{
DisableReadOnly(directory);
directory.Delete(true);
}
catch (Exception)
{
retryDeleteDirectoryCount++;
}
} while (Directory.Exists(fullPath) && retryDeleteDirectoryCount < 4);
C'est une sorte de réponse indirecte, mais cette question m'a fait réfléchir à la logique derrière cela, et j'ai pensé que cela pourrait valoir la peine d'être partagé.
Comme tout le monde l’a dit, vous utilisez une boucle do ... while
lorsque vous souhaitez exécuter le corps au moins une fois. Mais dans quelles circonstances voudriez-vous faire cela?
Eh bien, la catégorie de situations la plus évidente à laquelle je puisse penser serait lorsque la valeur initiale ("non primée") de la condition de vérification est la même que lorsque vous souhaitez quitter. Cela signifie que vous devez exécuter le corps de boucle une fois pour amorcer la condition à une valeur non existante, puis effectuer la répétition réelle en fonction de cette condition. Avec des programmeurs si paresseux, quelqu'un a décidé de résumer cela dans une structure de contrôle.
Ainsi, par exemple, la lecture des caractères d'un port série avec un délai d'attente peut prendre la forme suivante (en Python):
response_buffer = []
char_read = port.read(1)
while char_read:
response_buffer.append(char_read)
char_read = port.read(1)
# When there's nothing to read after 1s, there is no more data
response = ''.join(response_buffer)
Notez la duplication du code: char_read = port.read(1)
. Si Python avait une boucle do ... while
, j'aurais peut-être utilisé:
do:
char_read = port.read(1)
response_buffer.append(char_read)
while char_read
L'avantage supplémentaire pour les langues qui créent une nouvelle portée pour les boucles: char_read
ne pollue pas l'espace de nom de la fonction. Mais notez également qu'il existe un meilleur moyen de procéder, en utilisant la valeur None
de Python:
response_buffer = []
char_read = None
while char_read != '':
char_read = port.read(1)
response_buffer.append(char_read)
response = ''.join(response_buffer)
Voici donc le noeud de mon propos: dans les langues avec des types nullables, la situation initial_value == exit_value
est beaucoup moins fréquente et que peut être la raison pour laquelle vous ne la rencontrez pas. Je ne dis pas que cela ne se produit jamais, car il arrive encore qu'une fonction renvoie None
pour indiquer une condition valide. Mais dans mon opinion pressée et brièvement considérée, cela se produirait beaucoup plus si les langages que vous utilisiez ne permettaient pas une valeur qui signifie: cette variable n’a pas encore été initialisée.
Ce n'est pas un raisonnement parfait: en réalité, maintenant que les valeurs nulles sont communes, elles ne forment qu'un élément supplémentaire de l'ensemble des valeurs valides qu'une variable peut prendre. Mais dans la pratique, les programmeurs ont le moyen de faire la distinction entre une variable se trouvant dans un état sensible, qui peut inclure l’état de sortie de la boucle, et une variable dans un état non initialisé.
Je les ai utilisées un peu quand j'étais à l'école, mais pas beaucoup depuis.
En théorie, ils sont utiles lorsque vous souhaitez que le corps de la boucle s'exécute une fois avant la vérification des conditions de sortie. Le problème est que, dans les rares cas où je ne veux pas la vérification en premier, en général, je veux la vérification de sortie au milieu du corps de la boucle plutôt qu'à la toute fin. Dans ce cas, je préfère utiliser le for (;;)
bien connu avec un if (condition) exit;
quelque part dans le corps.
En fait, si je suis un peu fragile en ce qui concerne la condition de sortie de la boucle, je trouve parfois utile de commencer à écrire la boucle sous la forme d'une for (;;) {}
avec une instruction de sortie, le cas échéant, et lorsque cela est fait, je peux voir si elle peut l'être "nettoyé" en déplaçant les initilisations, les conditions de sortie et/ou le code d'incrémentation entre les parenthèses de for
.
do while
est si vous voulez exécuter le bloc de code au moins une fois. while
par contre ne fonctionnera pas toujours en fonction des critères spécifiés.
Une situation dans laquelle vous devez toujours exécuter un morceau de code une fois et éventuellement, plusieurs fois. La même chose peut être produite avec une boucle while
régulière également.
rc = get_something();
while (rc == wrong_stuff)
{
rc = get_something();
}
do
{
rc = get_something();
}
while (rc == wrong_stuff);
C'est aussi simple que ça:
condition préalable ou postcondition
Maintenant que vous connaissez le secret, utilisez-les judicieusement :)
Je vois que cette question a reçu une réponse adéquate, mais je voudrais ajouter ce scénario de cas d'utilisation très spécifique. Vous pourriez commencer à utiliser do ... tandis que plus fréquemment.
do
{
...
} while (0)
est souvent utilisé pour #defines multi-lignes. Par exemple:
#define compute_values \
area = pi * r * r; \
volume = area * h
Cela fonctionne bien pour:
r = 4;
h = 3;
compute_values;
-mais il y a un piège pour:
if (shape == circle) compute_values;
comme cela se développe à:
if (shape == circle) area = pi *r * r;
volume = area * h;
Si vous l'enroulez dans un do ... while (0), la boucle est étendue à un seul bloc:
if (shape == circle)
do
{
area = pi * r * r;
volume = area * h;
} while (0);
Je l'ai utilisé pour un lecteur qui lit la même structure plusieurs fois.
using(IDataReader reader = connection.ExecuteReader())
{
do
{
while(reader.Read())
{
//Read record
}
} while(reader.NextResult());
}
Les réponses à ce jour résument l’usage général du do-while. Mais l'OP a demandé un exemple, alors voici un exemple: obtenir les entrées de l'utilisateur. Mais la saisie de l'utilisateur peut être invalide - vous devez donc demander une saisie, la valider, continuer si elle est valide, sinon répéter.
Avec do-while, vous obtenez l'entrée alors que l'entrée n'est pas valide. Avec une boucle while régulière, vous obtenez l'entrée une fois, mais si elle est invalide, vous l'obtenez encore et encore jusqu'à ce qu'elle soit valide. Il n'est pas difficile de voir que le premier est plus court, plus élégant et plus simple à maintenir si le corps de la boucle devient plus complexe.
Je programme environ 12 ans et il y a seulement 3 mois, j'ai rencontré une situation où il était vraiment pratique d'utiliser do-tandis qu'une itération était toujours nécessaire avant de vérifier une condition. Alors, devinez que votre grand temps est devant vous :).
Voici ma théorie pourquoi la plupart des gens (y compris moi-même) préfèrent les boucles while () {} à faire {} while (): une boucle while () {} peut facilement être adaptée pour fonctionner comme une boucle do..while () tandis que ce n'est pas vrai. Une boucle while est en quelque sorte "plus générale". Aussi les programmeurs aiment facile à saisir les modèles. Une boucle while indique au début ce que son invariant est et c'est une bonne chose.
Voici ce que je veux dire par "plus général". Prenez cette boucle do..while:
do {
A;
if (condition) INV=false;
B;
} while(INV);
La transformer en une boucle while est simple:
INV=true;
while(INV) {
A;
if (condition) INV=false;
B;
}
Maintenant, nous prenons un modèle en boucle:
while(INV) {
A;
if (condition) INV=false;
B;
}
Et transformer cela en une boucle do..while, donne cette monstruosité:
if (INV) {
do
{
A;
if (condition) INV=false;
B;
} while(INV)
}
Nous avons maintenant deux vérifications aux extrémités opposées et si l'invariant change, vous devez le mettre à jour à deux endroits. D'une certaine manière, faites comme les tournevis spécialisés de la boîte à outils que vous n'utilisez jamais, car le tournevis standard fait tout ce dont vous avez besoin.
Je ne peux pas imaginer comment vous êtes resté aussi longtemps sans utiliser une boucle do...while
.
Il y en a un sur un autre moniteur en ce moment et ce programme comporte plusieurs boucles. Ils sont tous de la forme:
do
{
GetProspectiveResult();
}
while (!ProspectIsGood());
Le scénario le plus courant que je rencontre où j'utilise une boucle do
/while
se trouve dans un petit programme de console qui s'exécute en fonction de certaines entrées et se répète autant de fois que l'utilisateur le souhaite. Évidemment, cela n’a aucun sens pour un programme de console de s’exécuter en un rien de temps; mais au-delà de la première fois, c'est l'utilisateur qui décide - donc do
/while
au lieu de simplement while
.
Cela permet à l'utilisateur d'essayer plusieurs entrées si il le souhaite.
do
{
int input = GetInt("Enter any integer");
// Do something with input.
}
while (GetBool("Go again?"));
Je soupçonne que les développeurs de logiciels utilisent do
/while
de moins en moins de nos jours, maintenant que pratiquement tous les programmes du système Sun ont une interface graphique. Cela est plus logique avec les applications de la console, car il est nécessaire d'actualiser continuellement la sortie pour fournir des instructions ou d'inviter l'utilisateur à fournir de nouvelles informations. En revanche, avec une interface graphique, le texte qui fournit ces informations à l’utilisateur peut simplement s’asseoir sur un formulaire et n’a jamais besoin d’être répété par programme.
Je l'ai utilisé dans une fonction qui a renvoyé la position du caractère suivant dans une chaîne utf-8:
char *next_utf8_character(const char *txt)
{
if (!txt || *txt == '\0')
return txt;
do {
txt++;
} while (((signed char) *txt) < 0 && (((unsigned char) *txt) & 0xc0) == 0xc0)
return (char *)txt;
}
Notez que cette fonction est écrite de l’esprit et n’a pas été testée. Le fait est que vous devez quand même faire le premier pas et le faire avant de pouvoir évaluer la condition.
Les boucles while
vérifient la condition avant la boucle, les boucles do...while
vérifient la condition après la boucle. Cela est utile si vous souhaitez baser la condition sur les effets secondaires de la boucle en cours ou, comme d’autres affiches, si vous souhaitez que la boucle soit exécutée au moins une fois.
Je comprends d’où vous venez, mais le do-while
est quelque chose que la plupart des utilisateurs utilisent rarement, et je ne m’ai jamais utilisé moi-même. Vous ne le faites pas mal.
Vous ne le faites pas mal. C'est comme dire que quelqu'un fait le mal parce qu'ils n'ont jamais utilisé la primitive byte
. Ce n'est tout simplement pas celui couramment utilisé.
C’est mon opinion personnelle, mais cette question appelle une réponse ancrée dans l’expérience:
Je programme en C depuis 36 ans et je n’utilise jamais de boucles do
while
dans du code normal.
La seule utilisation intéressante de cette construction est dans les macros où elle peut envelopper plusieurs instructions dans une seule instruction via un do { multiple statements } while (0)
J'ai vu d'innombrables exemples de boucles do
/while
avec détection d'erreur erronée ou appels de fonction redondants.
Mon explication à cette observation est que les programmeurs ont tendance à modéliser les problèmes de manière incorrecte lorsqu'ils pensent en termes de boucles do
/while
. Ils omettent soit une condition de fin importante, soit l’échec possible de la condition initiale à laquelle ils se déplacent.
Pour ces raisons, j’en suis venu à croire que _/s’il existe une boucle do
/while
, il ya un bogue et j’encourage régulièrement les programmeurs débutants à me montrer une boucle do
/while
où je ne peux pas repérer un bogue à proximité.
Ce type de boucle peut être facilement évité: utilisez une for (;;) { ... }
et ajoutez les tests de terminaison nécessaires, le cas échéant. Il est assez courant qu'il faille plus d'un test de ce type.
Voici un exemple classique:
/* skip the line */
do {
c = getc(fp);
} while (c != '\n');
Cela échouera si le fichier ne se termine pas par une nouvelle ligne. Un exemple trivial d'un tel fichier est le fichier vide.
Une meilleure version est la suivante:
int c; // another classic bug is to define c as char.
while ((c = getc(fp)) != EOF && c != '\n')
continue;
Cette version cache également la variable c
:
for (;;) {
int c = getc(fp);
if (c == EOF || c == '\n')
break;
}
Essayez de chercher while (c != '\n');
dans n'importe quel moteur de recherche, et vous trouverez des bogues comme celui-ci (récupéré le 24 juin 2017):
Dans ftp://ftp.dante.de/tex-archive/biblio/tib/src/streams.c , la fonction getword(stream,p,ignore)
, a une do
/while
et bien au moins 2 bogues:
c
est défini comme un char
etwhile (c!='\n') c=getc(stream);
Conclusion: évitez les boucles do
/while
et recherchez les bogues lorsque vous en voyez une.
J'utilise tout le temps des boucles Do-While lors de la lecture de fichiers. Je travaille avec beaucoup de fichiers texte qui incluent des commentaires dans l'en-tête:
# some comments
# some more comments
column1 column2
1.234 5.678
9.012 3.456
... ...
je vais utiliser une boucle do-while pour lire jusqu'à la ligne "column1 column2" afin que je puisse rechercher la colonne d'intérêt. Voici le pseudocode:
do {
line = read_line();
} while ( line[0] == '#');
/* parse line */
Ensuite, je ferai une boucle while pour lire le reste du fichier.
J'ai utilisé un do while
lorsque je lisais une valeur sentinelle au début d'un fichier, mais à part cela, je ne pense pas qu'il soit anormal que cette structure ne soit pas trop utilisée - les do-while
s sont vraiment situationnels.
-- file --
5
Joe
Bob
Jake
Sarah
Sue
-- code --
int MAX;
int count = 0;
do {
MAX = a.readLine();
k[count] = a.readLine();
count++;
} while(count <= MAX)
N'importe quelle sorte d'entrée de console fonctionne bien avec do-while parce que vous faites une invite pour la première fois et ré-invite chaque fois que la validation d'entrée échoue.
En tant que programmeur Geezer, bon nombre de mes projets de programmation scolaire utilisaient des interactions pilotées par un menu texte. Pratiquement tous ont utilisé quelque chose comme la logique suivante pour la procédure principale:
do
display options
get choice
perform action appropriate to choice
while choice is something other than exit
Depuis l’école, j’ai découvert que j’utilisais plus souvent la boucle while.
L’une des applications que j’ai vue se trouve dans Oracle lorsque nous examinons des ensembles de résultats.
Une fois que vous avez un résultat, vous le récupérez d'abord (do) et ensuite à partir de .. vérifiez si le retour renvoie un élément ou non (pendant que l'élément est trouvé ..) .. La même chose pourrait s'appliquer à tout autre fetch-like "implémentations.
C'est une structure assez commune sur un serveur/consommateur:
DOWHILE (no shutdown requested)
determine timeout
wait for work(timeout)
IF (there is work)
REPEAT
process
UNTIL(wait for work(0 timeout) indicates no work)
do what is supposed to be done at end of busy period.
ENDIF
ENDDO
_REPEAT UNTIL(cond)
étant un do {...} while(!cond)
Parfois, l’attente de travail (0) peut être moins coûteuse en ressources de traitement (même éliminer le calcul du délai d’attente pourrait être une amélioration avec des taux d’arrivée très élevés). De plus, il existe _ {plusieurs} _ résultats de la théorie de la mise en file d'attente qui font du nombre servi en période de pointe une statistique importante. (Voir par exemple Kleinrock - Vol 1.)
De même:
DOWHILE (no shutdown requested)
determine timeout
wait for work(timeout)
IF (there is work)
set throttle
REPEAT
process
UNTIL(--throttle<0 **OR** wait for work(0 timeout) indicates no work)
ENDIF
check for and do other (perhaps polled) work.
ENDDO
où check for and do other work
peut être extrêmement coûteux à mettre dans la boucle principale ou peut-être un noyau qui ne prend pas en charge une opération de type waitany(waitcontrol*,n)
efficace ou peut-être une situation dans laquelle une file d'attente hiérarchisée pourrait priver l'autre travail et où étranglement est utilisé comme contrôle de famine.
Ce type d’équilibrage peut sembler être un hack, mais il peut être nécessaire. L'utilisation aveugle de pools de threads annulerait totalement les avantages en termes de performances de l'utilisation d'un thread de maintenance avec une file d'attente privée pour une structure de données complexe avec un taux de mise à jour élevé, car l'utilisation d'un pool de threads plutôt que d'un thread de maintenance nécessiterait une implémentation sécurisée des threads.
Je ne veux vraiment pas entrer dans un débat sur le pseudo-code (par exemple, si l'arrêt demandé doit être testé dans UNTIL) ou des threads de maintenance par rapport aux pools de threads - ceci est simplement destiné à donner une idée d'un cas d'utilisation particulier de la structure du flux de contrôle.