Boucle While sans évaluer les données deux fois

Je rencontre souvent le schéma suivant.

while(GetValue(i) != null)
{
    DoSomethingWith(GetValue(i++));
}

Ici, GetValue est exécuté deux fois. Il serait beaucoup plus pratique d’utiliser un modèle permettant d’évaluer ET de stocker le résultat de GetValue . Dans certains cas (flux), il est impossible d'obtenir la valeur deux fois (voir les solutions de rechange ci-dessous pour des solutions de contournement). Existe-t-il un modèle ou une construction de boucle que nous pouvons utiliser?

Certaines alternatives que j'ai pensé avec leurs propres inconvénients.

Alternative 1

// Variable outside of loop scope, extra if
var value = null;
do
{
    value = GetValue(i++);
    if(value != null) { DoSomethingWith(value); }
} while(value != null);

Alternative 2

// Two get values, variable outside of loop
var value = GetValue(i);
while(value != null)
{
  DoSometingWith(value);
  value = GetValue(++i);
}

Alternative 3

// Out only works on reference types, most enumerables do not have
// a TryGet like method so we need to create our own wrapper
Object value;
while(TryGetValue(i++, out value))
{
    DoSomethingWith(value);
}

Ideal world scenario (not valid C#)

while((var value = GetValue(i++)) != null)
{
    DoSomethingWith(value);
}
5
ajouté édité
Vues: 1
Je ne connais pas très bien C#, mais l'option "idéale" ne peut-elle pas omettre le contrôle nul, comme while (var valeur = GetValue (i ++)) ?
ajouté l'auteur Daniel McPherson, source
BTW dans le scénario du monde idéal, je pense que vous vouliez écrire "while ((var valeur = GetValue (i ++))!! = = Null)", N'est-ce pas vrai?
ajouté l'auteur Pieter, source
ajouté l'auteur gnat, source
Vous pouvez vous approcher du "scénario du monde idéal", il vous suffit de déclarer valeur en dehors de la boucle.
ajouté l'auteur Joel, source
@JimmyJames Oui, cela fonctionne, sauf avec var . Vous devriez en faire une réponse.
ajouté l'auteur Joel, source
Votre "scénario idéal" est utilisé tout le temps avec les flux, sauf que le type de données n'est pas var. C'est donc une bonne question.
ajouté l'auteur Frank Hileman, source
@Theraot Vous devez donc déclarer un type spécifique? J'aurais trop l'impression d'être un faux pour essayer de poster une réponse en C #. J'ai écrit exactement un programme C #. N'hésitez pas à l'ajouter à votre réponse.
ajouté l'auteur JimmyJames, source
Tu ne peux pas faire ça en C #? pour (valeur var; (valeur = GetValue (i ++))!! = null;)
ajouté l'auteur JimmyJames, source
@Zalomon vous avez absolument raison, corrigé la faute de frappe.
ajouté l'auteur Naim Zard, source
Pourquoi le scénario du monde idéal n’est-il pas valide c #? À prendre en compte
ajouté l'auteur candied_orange, source

7 Réponses

Solution proche du "monde idéal"

Ce qui suit est valide C #

public static void Main()
{
    var i = 0;
    string value;
    while ((value = GetValue(i++)) != null)
    {
        DoSomethingWith(value);
    }
}

private static string GetValue(int input)
{
    if (input > 20)
    {
        return null;
    }
    return input.ToString();
}

private static void DoSomethingWith(string value)
{
    Console.WriteLine(value);
}

Comparez avec votre "scénario du monde idéal":

var i = 0;
while((var value = GetValue(i++)) != null)
{
    DoSomethingWith(value);
}

Tout ce que je devais faire était d'extraire valeur à l'extérieur de la boucle. Vous semblez vouloir faire cela pour toutes les alternatives présentées. Ainsi, je ne pense pas que cela soit trop exagéré.


"Infini" en alternative

J'ai joué avec les alternatives ... en voici une autre:

var i = 0;
while (true)
{
    var value = GetValue(i++);
    if (value == null)
    {
        break;
    }
    DoSomethingWith(value);
}

Dans ce cas, vous pouvez déclarer valeur à l'intérieur de la boucle (et en utilisant var ), vous n'avez pas besoin de rechercher deux fois null . Il n'est pas nécessaire d'appeler deux fois GetValue avec la même entrée et vous n'avez pas besoin de créer un wrapper avec un paramètre out .


Alternative utilisant For

Nous pouvons essayer d’exprimer la même chose en utilisant une boucle pour . Bien que l'approche naïve ne fonctionne pas:

// I repeat, this does not work:
var i = 0;
for (string value = null; value != null; value = GetValue(i++))
{
    DoSomethingWith(value);
}

Le problème avec cette version est qu’elle commence par valeur qui est null , ce qui répond au critère de sortie et ne donne donc aucune itération.


Comme JimmyJames indique vous pouvez l'écrire comme ceci:

var i = 0;
for(string value; (value = GetValue(i++)) != null;)
{
    DoSomethingWith(value);
}

Le seul inconvénient que je vois est que vous devez écrire le type (vous ne pouvez pas utiliser var ).


Addendum: This is another variant suggested by Maliafo:

var i = 0;
for (var value = GetValue(i); value != null; value = GetValue(++i))
{
    DoSomethingWith(value);
}

Alors que cette version nécessite d'écrire GetValue , elle ne l'appelle pas deux fois avec la même valeur.


Voulez-vous également déclarer i dans la portée? Regardez ça:

for (var i = 0; ; i++)
{
    var value = GetValue(i);
    if (value == null)
    {
        break;
    }
    DoSomethingWith(value);
}

C'est la même boucle que nous voyons dans l'infini alors que la solution que j'ai postée ci-dessus. Pourtant, comme je n’avais pas de condition en cours ... pourquoi ne pas utiliser le changement en pour pour incrémenter i ?


Addendum: Also see NetMage's answer for an interesting use of C# 7.0 features.

12
ajouté
@Maliafo Oui, vous avez raison, cela fonctionne! Je n'y ai même pas pensé, car j'évitais d'écrire deux fois GetValue . Cela m'a complètement manqué. Addendum: Je vais ajouter cela à la réponse.
ajouté l'auteur Joel, source
J'ai un peu expérimenté une syntaxe similaire à while, mais je pensais que cela ne fonctionnait pas. Je n'ai jamais pensé à déplacer la déclaration de variable à l'extérieur. Je suppose que c’est le plus près possible de ma solution idéale. De plus, je n'ai jamais pensé à utiliser une boucle comme celle-là. Très bonne réponse!
ajouté l'auteur Naim Zard, source
Vous arrivez un peu en retard sur celui-ci, mais n'est-il pas possible d'utiliser un plus idiomatique pour (var valeur = GetValue (i); valeur! = Null; valeur = GetValue (++ i)) en C #?
ajouté l'auteur Maliafo, source

En C# 7.0, on peut simplement (ab) utiliser:

while (GetValue(i++) is var value && value != null) {
    DoSomethingWith(value);
}
4
ajouté
J'espère vraiment que les expressions de déclaration (plus quelque chose comme l'opérateur de virgule C) le feront bientôt dans une version de C #.
ajouté l'auteur Faré, source
Je n'ai pas pensé à combiner les fonctionnalités de C# 7.0 avec la suggestion de Theraot. Soigné!
ajouté l'auteur Naim Zard, source

Utilisez une fonction d'ordre supérieur

Sortez du livre de jeu de la programmation fonctionnelle: si vous avez un motif récurrent dans la structure de votre code, créez une fonction contenant les parties qui restent identiques et transmettez les parties qui varient en tant que paramètres de fonction C #). De cette façon, peu importe sa laideur, il suffit de l'écrire une fois et de le cacher dans une bibliothèque quelque part.

(Vous devrez me pardonner d'entrer dans Java ici; mes compétences C# sont un peu rouillées)

public static  void untilNull (Function supplier, int i, Action  action) {
    T v;
    while ((v = supplier.apply(i++)) != null)
        action.perform(v);
}

....
untilNull (this::getValue, 1, this::doSomethingWithValue);

Mise à jour - maintenant avec la version C #:

public static  void UntilNull (Func supplier, int i, Action action) {
    T v;
    while ((v = supplier(i++)) != null) action(v);
}

....
UntilNull (this.GetValue, 1, this.DoSomethingWithValue);
4
ajouté
Je pense que le problème des modèles de programmation fonctionnels dans des langages tels que C# est qu’ils sont moins efficaces que le code de procédure, parfois beaucoup moins, car il n’ya pas d’extension en ligne de la méthode.
ajouté l'auteur Faré, source

La solution simple

Cette solution est certainement meilleure que vos alternatives 1 et 2 si vous avez besoin d’un minimum de code pour résoudre votre problème.

while(true){
    var value = GetValue(i++);
    if(value != null) { 
        DoSomethingWith(value); 
    }
    else{
        break;
    }
}

Solution compatible DReach à base d'itérateurs

À mon avis, pour la meilleure solution globale propre, cet itérateur avec le paramètre de fonction solution basée fonctionnerait le meilleur. Pas besoin de classer des wrappers spécifiques et du code dupliqué fournis par votre troisième option et fonctionne pour chaque problème comme celui-ci (donc je pense que c'est toujours mieux que l'implémentation en boucle While plus courte en ligne). Et mieux encore, si vous recherchez des itérateurs de toute façon, pourquoi ne pas mettre la valeur nullcheck dans l’itérateur lui-même? De cette façon, vous pouvez utiliser ceci pour n’importe quelle fonction prenant une position et pouvant renvoyer null.

public static System.Collections.Generic.IEnumerable  
    FunctionSequence(Func f)  
{  
    int i = 0;
    while(true){
        value = f(i++);
        if(value != null) { 
            yield return value; 
        }
        else{
            break;
        }
    }
}  

foreach(GetValueType value in FunctionSequence(GetValue)){
    DoSomethingWith(value);
}

Notez que vous n'avez maintenant besoin d'aucun code de plaque de chaudière pour une fonction , utilisez simplement la fonction avec l'itérateur FunctionSequence et vous effectuerez une itération à la première valeur null. élément, il vous suffit de l'écrire une fois, cette


Généraliser davantage la solution basée sur Itérateur

Si vous ne voulez pas commencer à 0, vous pouvez toujours changer la signature de la fonction comme suit:

public static System.Collections.Generic.IEnumerable  
    FunctionSequence(Func f, int start = 0)  
{  
    int i = start;
    ...

Et vous pouvez même changer le résultat de votre vérification, que ce soit son null ou un objet spécifique renvoyé!

public static System.Collections.Generic.IEnumerable  
    FunctionSequence(Func f, int start = 0, T terminator = null)  
{  
    int i = start;
    while(true){
        value = f(i++);
        if(value != terminator) { 
        ...

Je pense que cela est la meilleure solution pour votre cas.

3
ajouté

Je choisirais l'option 3, car je ne vois pas pourquoi on serait disponible pour les types de référence: surtout si vous n'utilisez pas un type de référence, vous n'avez pas besoin de vérifier les valeurs nulles; au cas où vous voudriez une autre validation telle que "est-ce que cette énumération a la valeur appropriée?" vous pouvez simplement utiliser cette vérification de temps en temps et elle ne sera exécutée que si elle convient (ou vous pouvez utiliser ref au lieu de out, même si cela signifie initialiser la variable lors de sa déclaration)

0
ajouté
Comme je l'ai dit dans ma réponse, null, ce n'est qu'un exemple, vous pouvez vérifier tout ce dont vous avez besoin dans TryGetValue. La façon dont je vois les choses serait plus expressive que la solution que vous proposez à mon avis qu'il s'agirait simplement d'un sucre syntaxique: puisqu'elle a toujours besoin de vérifier le retour de la fonction, affectez-la à la variable et transmettez-la à DoSomethingWith.
ajouté l'auteur Pieter, source
La vérification sur null est juste un exemple ici. Ce pourrait également être quelque chose comme > 0 . Un autre problème avec out est que vous devrez écrire un wrapper pour la plupart des énumérables standard. Mais je pourrais peut-être capturer cela dans une méthode d'extension.
ajouté l'auteur Naim Zard, source
Doh, je n'ai pas pensé à TryGetValue comme ça. Et oui, ce serait du sucre syntaxique. Mais cela ne conduirait pas à une variable déclarée dans une étendue supérieure à celle requise, et je n'aurais pas besoin de me répéter :).
ajouté l'auteur Naim Zard, source

Je ne fais pas C# mais surtout Swift. Dans la plupart des cas j'écrirais

for value in  { ... }

qui est le sucre syntaxique pour

while let value = iterator.next() { }

(Let value = expression évalue une expression, vérifie si elle est nulle, si elle est nulle, sinon elle stocke une valeur non optionnelle dans value. Next() dans un itérateur renvoie une valeur optionnelle; nil si l'itérateur est terminé ).

0
ajouté

Que diriez-vous d'une alternative 4:

Envelopper GetValue dans un IEnumerable/IEnumerator. Vous pourrez alors utiliser la syntaxe foreach normale qui permet votre scénario idéal. Si vous avez du contenu dynamique pour lequel vous n'avez pas besoin d'implémenter une classe Collection complète, quelques fonctions autour d'une déclaration de rendement suffiront.

Cela devrait même vous permettre d’utiliser certains tours de magie de Linq avec vos données.

0
ajouté