web-dev-qa-db-fra.com

Réflexions sur foreach avec Enumerable.Range vs traditional for loop

En C # 3.0, j'aime ce style:

// Write the numbers 1 thru 7
foreach (int index in Enumerable.Range( 1, 7 ))
{
    Console.WriteLine(index);
}

sur la boucle for traditionnelle:

// Write the numbers 1 thru 7
for (int index = 1; index <= 7; index++)
{
    Console.WriteLine( index );
}

En supposant que "n" est petit, les performances ne sont pas un problème, est-ce que quelqu'un s'oppose au nouveau style par rapport au style traditionnel?

60
Marcel Lamothe

Je trouve le format "minimum-maximum" de ce dernier beaucoup plus clair que le style "minimum-count" de Range à cet effet. De plus, je ne pense pas que ce soit vraiment une bonne pratique de faire un changement comme celui-ci par rapport à la norme qui n'est pas plus rapide, ni plus court, ni plus familier, ni évidemment plus clair.

Cela dit, je ne suis pas contre l'idée en général. Si vous me proposiez une syntaxe qui ressemblait à foreach (int x from 1 to 8), je serais probablement d'accord que ce serait une amélioration par rapport à une boucle for. Pourtant, Enumerable.Range est assez maladroit.

49
mqp

C'est juste pour s'amuser. (J'utiliserais moi-même le format de boucle standard "for (int i = 1; i <= 10; i++)".)

foreach (int i in 1.To(10))
{
    Console.WriteLine(i);    // 1,2,3,4,5,6,7,8,9,10
}

// ...

public static IEnumerable<int> To(this int from, int to)
{
    if (from < to)
    {
        while (from <= to)
        {
            yield return from++;
        }
    }
    else
    {
        while (from >= to)
        {
            yield return from--;
        }
    }
}

Vous pouvez également ajouter une méthode d'extension Step:

foreach (int i in 5.To(-9).Step(2))
{
    Console.WriteLine(i);    // 5,3,1,-1,-3,-5,-7,-9
}

// ...

public static IEnumerable<T> Step<T>(this IEnumerable<T> source, int step)
{
    if (step == 0)
    {
        throw new ArgumentOutOfRangeException("step", "Param cannot be zero.");
    }

    return source.Where((x, i) => (i % step) == 0);
}
38
LukeH

En C # 6.0 avec l'utilisation de

using static System.Linq.Enumerable;

vous pouvez le simplifier

foreach (var index in Range(1, 7))
{
    Console.WriteLine(index);
}
12
Mike Tsayper

Cela semble être une approche assez longue d'un problème déjà résolu. Il y a toute une machine d'état derrière le Enumerable.Range ce n'est pas vraiment nécessaire.

Le format traditionnel est fondamental pour le développement et familier à tous. Je ne vois vraiment aucun avantage à votre nouveau style.

9
spender

Vous pouvez réellement le faire en C # (en fournissant To et Do comme méthodes d'extension sur int et IEnumerable<T> respectivement):

1.To(7).Do(Console.WriteLine);

Smalltalk forever!

9
THX-1138

J'aime un peu l'idée. Cela ressemble beaucoup à Python. Voici ma version en quelques lignes:

static class Extensions
{
    public static IEnumerable<int> To(this int from, int to, int step = 1) {
        if (step == 0)
            throw new ArgumentOutOfRangeException("step", "step cannot be zero");
        // stop if next `step` reaches or oversteps `to`, in either +/- direction
        while (!(step > 0 ^ from < to) && from != to) {
            yield return from;
            from += step;
        }
    }
}

Cela fonctionne comme Python:

  • 0.To(4)[ 0, 1, 2, 3 ]
  • 4.To(0)[ 4, 3, 2, 1 ]
  • 4.To(4)[ ]
  • 7.To(-3, -3)[ 7, 4, 1, -2 ]
6
Kache

Je pense que le foreach + Enumerable.Range est moins sujet aux erreurs (vous avez moins de contrôle et moins de façons de le faire mal, comme diminuer l'index à l'intérieur du corps pour que la boucle ne se termine jamais, etc.)

Le problème de lisibilité concerne la sémantique de la fonction Range, qui peut changer d'une langue à une autre (par exemple, si un seul paramètre est donné, il commencera à 0 ou 1, ou la fin est-elle incluse ou exclue ou le deuxième paramètre est-il un compte plutôt qu'une fin valeur).

Concernant les performances, je pense que le compilateur devrait être suffisamment intelligent pour optimiser les deux boucles afin qu'elles s'exécutent à une vitesse similaire, même avec de grandes plages (je suppose que Range ne crée pas de collection, mais bien sûr un itérateur).

6
fortran

Je pense que Range est utile pour travailler avec une certaine gamme en ligne:

var squares = Enumerable.Range(1, 7).Select(i => i * i);

Vous pouvez chacun terminer. Nécessite une conversion en liste mais garde les choses compactes quand c'est ce que vous voulez.

Enumerable.Range(1, 7).ToList().ForEach(i => Console.WriteLine(i));

Mais à part pour quelque chose comme ça, j'utiliserais la boucle for traditionnelle.

5
mcNux

J'aimerais avoir la syntaxe de quelques autres langages comme Python, Haskell, etc.

// Write the numbers 1 thru 7
foreach (int index in [1..7])
{
    Console.WriteLine(index);
}

Heureusement, nous avons maintenant F # :)

Quant à C #, je vais devoir m'en tenir au Enumerable.Range méthode.

5
Thomas Danecker

@Luke: J'ai réimplémenté votre méthode d'extension To() et utilisé la méthode Enumerable.Range() pour le faire. De cette façon, il sort un peu plus court et utilise autant d'infrastructure que nous a donné .NET:

public static IEnumerable<int> To(this int from, int to)
{ 
    return from < to 
            ? Enumerable.Range(from, to - from + 1) 
            : Enumerable.Range(to, from - to + 1).Reverse();
}
5
Thorsten Lorenz

Je suis sûr que tout le monde a ses préférences personnelles (beaucoup préféreraient la dernière juste parce qu'elle est familière à presque tous les langages de programmation), mais je suis comme vous et je commence à aimer de plus en plus foreach, surtout maintenant que vous pouvez définir une plage .

3
TheTXI

À mon avis, la méthode Enumerable.Range() est plus déclarative. Nouveau et inconnu des gens? Certainement. Mais je pense que cette approche déclarative offre les mêmes avantages que la plupart des autres fonctionnalités de langage liées à LINQ.

2
MEMark

J'imagine qu'il pourrait y avoir des scénarios où Enumerable.Range(index, count) est plus clair lorsqu'il s'agit d'expressions pour les paramètres, surtout si certaines des valeurs de cette expression sont modifiées dans la boucle. Dans le cas de for, l'expression serait évaluée en fonction de l'état après l'itération en cours, tandis que Enumerable.Range() est évaluée à l'avance.

En dehors de cela, je conviens que s'en tenir à for serait normalement mieux (plus familier/lisible pour plus de gens ... lisible est une valeur très importante dans le code qui doit être maintenue).

2
jerryjvl

Comment utiliser une nouvelle syntaxe aujourd'hui

En raison de cette question, j'ai essayé certaines choses pour trouver une syntaxe Nice sans attendre le support d'un langage de première classe. Voici ce que j'ai:

using static Enumerizer;

// prints: 0 1 2 3 4 5 6 7 8 9
foreach (int i in 0 <= i < 10)
    Console.Write(i + " ");

Pas la différence entre <= et <.

J'ai également créé un dépôt de preuve de concept sur GitHub avec encore plus de fonctionnalités (itération inversée, taille d'étape personnalisée).

Une implémentation minimale et très limitée de la boucle ci-dessus ressemblerait à ceci:

public readonly struct Enumerizer
{
    public static readonly Enumerizer i = default;

    public Enumerizer(int start) =>
        Start = start;

    public readonly int Start;

    public static Enumerizer operator <(int start, Enumerizer _) =>
        new Enumerizer(start);

    public static Enumerizer operator >(int _, Enumerizer __) =>
        throw new NotImplementedException();

    public static IEnumerable<int> operator <=(Enumerizer start, int end)
    {
        for (int i = start.Start; i < end; i++)
            yield return i;
    }

    public static IEnumerable<int> operator >=(Enumerizer _, int __) =>
        throw new NotImplementedException();
}
1
Bruno Zell

Je suis d'accord que dans de nombreux (ou même la plupart des cas) foreach est beaucoup plus lisible qu'une boucle for- standard lors d'une simple itération sur une collection. Cependant, votre choix d'utiliser Enumerable.Range(index, count) n'est pas un bon exemple de la valeur de foreach pour.

Pour une plage simple commençant par 1, Enumerable.Range(index, count) semble tout à fait lisible. Cependant, si la plage commence par un index différent, elle devient moins lisible car vous devez effectuer correctement index + count - 1 pour déterminer quel sera le dernier élément. Par exemple…

// Write the numbers 2 thru 8
foreach (var index in Enumerable.Range( 2, 7 ))
{
    Console.WriteLine(index);
}

Dans ce cas, je préfère de loin le deuxième exemple.

// Write the numbers 2 thru 8
for (int index = 2; index <= 8; index++)
{
    Console.WriteLine(index);
}
1
Dustin Campbell

À strictement parler, vous abusez de l'énumération.

L'énumérateur fournit les moyens d'accéder à tous les objets dans un conteneur un par un, mais il ne garantit pas la commande.

Il est correct d'utiliser l'énumération pour trouver le plus grand nombre dans un tableau. Si vous l'utilisez pour trouver, disons, le premier élément non nul, vous vous fiez aux détails d'implémentation que vous ne devriez pas connaître. Dans votre exemple, la commande vous semble importante.

Edit : Je me trompe. Comme l'a souligné Luke (voir les commentaires), il est sûr de s'appuyer sur l'ordre lors de l'énumération d'un tableau en C #. C'est différent de, par exemple, utiliser "for in" pour énumérer un tableau en Javascript.

1
buti-oxa

J'aime le foreach + Enumerable.Range approche et l'utilise parfois.

// does anyone object to the new style over the traditional style?
foreach (var index in Enumerable.Range(1, 7))

Je m'oppose à l'abus de var dans votre proposition. J'apprécie var, mais, bon sang, écris simplement int dans ce cas! ;-)

0
xyz