Quelle est la meilleure façon de terminer la boucle de lecture?

13

Lorsque vous devez répéter un lecteur dont le nombre d'éléments à lire est inconnu, et la seule façon de le faire est de continuer à lire jusqu'à la fin.

C'est souvent l'endroit où vous avez besoin d'une boucle sans fin.

  1. Il y a toujours truequi indique qu'il doit y avoir une instruction breakou quelque part à l'intérieur du bloc.return

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. Il y a la méthode de la double lecture pour la boucle.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Il y a la boucle de lecture unique. Toutes les langues ne prennent pas en charge cette méthode .

    int offset = 0;
    Record r = null;
    while((r = Read(++offset)) != null)
    {
        // do work
    }

Je me demande quelle approche est la moins susceptible d'introduire un bogue, la plus lisible et la plus utilisée.

Chaque fois que je dois en écrire un, je pense "qu'il doit y avoir une meilleure façon" .

Reactgular
la source
2
Pourquoi maintenez-vous un décalage? La plupart des lecteurs de flux ne vous permettent-ils pas simplement de "lire ensuite?"
Robert Harvey
@RobertHarvey dans mon besoin actuel, le lecteur a une requête SQL sous-jacente qui utilise le décalage pour paginer les résultats. Je ne sais pas combien de temps les résultats de la requête sont jusqu'à ce qu'il renvoie un résultat vide. Mais, pour la question, ce n'est pas vraiment une exigence.
Reactgular
3
Vous semblez confus - le titre de la question concerne les boucles sans fin, mais le texte de la question concerne la terminaison des boucles. La solution classique (à l'époque de la programmation structurée) consiste à faire une pré-lecture, une boucle pendant que vous avez des données, et à relire comme la dernière action de la boucle. C'est simple (répondant à l'exigence de bug), le plus courant (puisqu'il est écrit depuis 50 ans). Le plus lisible est une question d'opinion.
andy256
@ andy256 confused est une condition préalable au café pour moi.
Reactgular
1
Ah, donc la bonne procédure est 1) Boire du café en évitant le clavier, 2) Commencer la boucle de codage.
andy256

Réponses:

49

Je ferais un pas en arrière ici. Vous vous concentrez sur les détails pointilleux du code mais manquez l'image plus grande. Jetons un coup d'œil à l'une de vos boucles d'exemple:

int offset = 0;
while(true)
{
    Record r = Read(offset);
    if(r == null)
    {
        break;
    }
    // do work
    offset++;
}

Quelle est la signification de ce code? La signification est "faire un travail sur chaque enregistrement d'un fichier". Mais ce n'est pas à cela que ressemble le code . Le code ressemble à "maintenir un décalage. Ouvrez un fichier. Entrez une boucle sans condition de fin. Lisez un enregistrement. Testez la nullité." Tout ça avant d'arriver au travail! La question que vous devriez poser est " comment puis-je faire en sorte que l'apparence de ce code corresponde à sa sémantique? " Ce code devrait être:

foreach(Record record in RecordsFromFile())
    DoWork(record);

Maintenant, le code se lit comme son intention. Séparez vos mécanismes de votre sémantique . Dans votre code d'origine, vous mélangez le mécanisme - les détails de la boucle - avec la sémantique - le travail effectué pour chaque enregistrement.

Maintenant, nous devons mettre en œuvre RecordsFromFile(). Quelle est la meilleure façon de mettre cela en œuvre? On s'en fout? Ce n'est pas le code que quiconque va consulter. C'est le code du mécanisme de base et ses dix lignes. Écrivez-le comme vous le souhaitez. Que dis-tu de ça?

public IEnumerable<Record> RecordsFromFile()
{
    int offset = 0;
    while(true)
    {
        Record record = Read(offset);
        if (record == null) yield break;
        yield return record;
        offset += 1;
    }
}

Maintenant que nous manipulons une séquence d'enregistrements calculée paresseusement, toutes sortes de scénarios deviennent possibles:

foreach(Record record in RecordsFromFile().Take(10))
    DoWork(record);

foreach(Record record in RecordsFromFile().OrderBy(r=>r.LastName))
    DoWork(record);

foreach(Record record in RecordsFromFile().Where(r=>r.City == "London")
    DoWork(record);

Etc.

Chaque fois que vous écrivez une boucle, demandez-vous "cette boucle se lit-elle comme un mécanisme ou comme le sens du code?" Si la réponse est "comme un mécanisme", essayez de déplacer ce mécanisme dans sa propre méthode et écrivez le code pour rendre la signification plus visible.

Eric Lippert
la source
3
+1 enfin une réponse sensée. C'est exactement ce que je vais faire. Merci.
Reactgular
1
"essayez de déplacer ce mécanisme vers sa propre méthode" - cela ressemble beaucoup au refactoring de la méthode d'extraction, n'est-ce pas? "Transformez le fragment en une méthode dont le nom explique le but de la méthode."
moucher
2
@gnat: Ce que je suggère est un peu plus compliqué que la "méthode d'extraction", qui je pense simplement déplacer un morceau de code vers un autre endroit. L'extraction de méthodes est certainement une bonne étape pour rendre le code plus semblable à sa sémantique. Je suggère que l'extraction des méthodes se fasse de manière réfléchie, en veillant à maintenir les politiques et les mécanismes séparés.
Eric Lippert
1
@gnat: Exactement! Dans ce cas, le détail que je veux extraire est le mécanisme par lequel tous les enregistrements sont lus à partir du fichier, tout en conservant la politique. La politique étant "nous devons travailler sur chaque disque".
Eric Lippert
1
Je vois. De cette façon, il est plus facile à lire et à entretenir. En étudiant ce code, je peux me concentrer séparément sur la politique et le mécanisme, cela ne m'oblige pas à diviser l'attention
moucher
19

Vous n'avez pas besoin d'une boucle sans fin. Vous ne devriez jamais en avoir besoin dans les scénarios de lecture C #. C'est mon approche préférée, en supposant que vous avez vraiment besoin de maintenir un décalage:

Record r = Read(0);
offset=1;
while(r != null)
{
    // Do work
    r = Read(offset);
    offset++
}

Cette approche reconnaît le fait qu'il existe une étape de configuration pour le lecteur, il y a donc deux appels de méthode de lecture. La whilecondition est en haut de la boucle, juste au cas où il n'y aurait aucune donnée dans le lecteur.

Robert Harvey
la source
6

Eh bien, cela dépend de votre situation. Mais l'une des solutions les plus "C # -ish" à laquelle je peux penser est d'utiliser l'interface IEnumerable intégrée et une boucle foreach. L'interface pour IEnumerator appelle uniquement MoveNext avec true ou false, donc la taille peut être inconnue. Ensuite, votre logique de terminaison est écrite une fois - dans l'énumérateur - et vous n'avez pas à répéter à plusieurs endroits.

MSDN fournit un exemple d'IEnumerator <T> . Vous devrez également créer un IEnumerable <T> pour renvoyer l'IEnumerator <T>.

J Trana
la source
Pouvez-vous donner un exemple de code.
Reactgular
merci, c'est ce que j'ai décidé de faire. Bien que je ne pense pas que la création de nouvelles classes chaque fois que vous avez une boucle sans fin résout la question.
Reactgular
Ouais, je sais ce que tu veux dire - beaucoup de frais généraux. Le vrai génie / problème des itérateurs est qu'ils sont définitivement configurés pour pouvoir faire réitérer deux choses sur une collection en même temps. Tant de fois que vous n'avez pas besoin de cette fonctionnalité, vous pouvez simplement avoir le wrapper autour des objets implémentant à la fois IEnumerable et IEnumerator. Une autre façon de voir les choses est que la collection sous-jacente de trucs sur lesquels vous faites une itération n'a pas été conçue avec le paradigme C # accepté à l'esprit. Et c'est OK. Un bonus supplémentaire des itérateurs est que vous pouvez obtenir gratuitement tous les trucs LINQ parallèles!
J Trana
2

Quand j'ai des opérations d'initialisation, de condition et d'incrémentation, j'aime utiliser les boucles for de langages comme C, C ++ et C #. Comme ça:

for (int offset = 0, Record r = Read(offset); r != null; r = Read(++offset)){
    // loop here!
}

Ou ceci si vous pensez que c'est plus lisible. Personnellement, je préfère le premier.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
la source