Comment obtenir l'index de l'itération actuelle d'une boucle foreach ?

StackOverflow https://stackoverflow.com/questions/43021

  •  09-06-2019
  •  | 
  •  

Question

Existe-t-il une construction de langage rare que je n'ai pas rencontrée (comme celles que j'ai apprises récemment, certaines sur Stack Overflow) en C# pour obtenir une valeur représentant l'itération actuelle d'une boucle foreach ?

Par exemple, je fais actuellement quelque chose comme ceci selon les circonstances :

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}
Était-ce utile?

La solution

Le foreach sert à parcourir les collections qui implémentent IEnumerable.Il le fait en appelant GetEnumerator sur la collection, qui renverra un Enumerator.

Cet énumérateur a une méthode et une propriété :

  • DéplacerSuivant()
  • Actuel

Current renvoie l'objet sur lequel l'énumérateur se trouve actuellement, MoveNext mises à jour Current à l'objet suivant.

Le concept d'index est étranger au concept d'énumération et ne peut être réalisé.

Pour cette raison, la plupart des collections peuvent être parcourues à l'aide d'un indexeur et de la construction de boucle for.

Je préfère largement utiliser une boucle for dans cette situation plutôt que de suivre l'index avec une variable locale.

Autres conseils

Ian Mercer a publié une solution similaire à celle-ci sur Le blog de Phil Haack:

foreach (var item in Model.Select((value, i) => new { i, value }))
{
    var value = item.value;
    var index = item.i;
}

Cela vous donne l'élément (item.value) et son indice (item.i) en utilisant cette surcharge de Linq Select:

le deuxième paramètre de la fonction [inside Select] représente l'index de l'élément source.

Le new { i, value } crée un nouveau objet anonyme.

Les allocations de tas peuvent être évitées en utilisant ValueTuple si vous utilisez C# 7.0 ou version ultérieure :

foreach (var item in Model.Select((value, i) => ( value, i )))
{
    var value = item.value;
    var index = item.i;
}

Vous pouvez également éliminer le item. en utilisant la déstructuration automatique :

<ol>
foreach ((MyType value, Int32 i) in Model.Select((value, i) => ( value, i )))
{
    <li id="item_@i">@value</li>
}
</ol>

Pourrait faire quelque chose comme ceci :

public static class ForEachExtensions
{
    public static void ForEachWithIndex<T>(this IEnumerable<T> enumerable, Action<T, int> handler)
    {
        int idx = 0;
        foreach (T item in enumerable)
            handler(item, idx++);
    }
}

public class Example
{
    public static void Main()
    {
        string[] values = new[] { "foo", "bar", "baz" };

        values.ForEachWithIndex((item, idx) => Console.WriteLine("{0}: {1}", idx, item));
    }
}

Enfin, C#7 a une syntaxe décente pour obtenir un index à l'intérieur d'un foreach boucle (c.e.tuples) :

foreach (var (item, index) in collection.WithIndex())
{
    Debug.WriteLine($"{index}: {item}");
}

Une petite méthode d'extension serait nécessaire :

public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)       
   => self.Select((item, index) => (item, index)); 

Je ne suis pas d'accord avec les commentaires selon lesquels un for loop est un meilleur choix dans la plupart des cas.

foreach est une construction utile et non remplaçable par un for boucle en toutes circonstances.

Par exemple, si vous avez un Lecteur de données et parcourez tous les enregistrements à l'aide d'un foreach il appelle automatiquement le Disposer et ferme le lecteur (qui peut alors fermer automatiquement la connexion).C'est donc plus sûr car cela évite les fuites de connexion même si vous oubliez de fermer le lecteur.

(Bien sûr, c'est une bonne pratique de toujours fermer les lecteurs, mais le compilateur ne le détectera pas si vous ne le faites pas - vous ne pouvez pas garantir que vous avez fermé tous les lecteurs, mais vous pouvez augmenter la probabilité que vous ne perdiez pas de connexions en obtenant j'ai l'habitude d'utiliser foreach.)

Il peut y avoir d'autres exemples d'appel implicite du Dispose méthode étant utile.

Réponse littérale -- avertissement, les performances peuvent ne pas être aussi bonnes que la simple utilisation d'un int pour suivre l'index.Au moins, c'est mieux que d'utiliser IndexOf.

Il vous suffit d'utiliser la surcharge d'indexation de Select pour envelopper chaque élément de la collection avec un objet anonyme qui connaît l'index.Cela peut être fait contre tout ce qui implémente IEnumerable.

System.Collections.IEnumerable collection = Enumerable.Range(100, 10);

foreach (var o in collection.OfType<object>().Select((x, i) => new {x, i}))
{
    Console.WriteLine("{0} {1}", o.i, o.x);
}

En utilisant la réponse de @FlySwat, j'ai trouvé cette solution :

//var list = new List<int> { 1, 2, 3, 4, 5, 6 }; // Your sample collection

var listEnumerator = list.GetEnumerator(); // Get enumerator

for (var i = 0; listEnumerator.MoveNext() == true; i++)
{
  int currentItem = listEnumerator.Current; // Get current item.
  //Console.WriteLine("At index {0}, item is {1}", i, currentItem); // Do as you wish with i and  currentItem
}

Vous obtenez l'enquêteur en utilisant GetEnumerator puis vous bouclez en utilisant un for boucle.Cependant, l'astuce consiste à rendre la condition de la boucle listEnumerator.MoveNext() == true.

Depuis le MoveNext La méthode d'un énumérateur renvoie vrai s'il y a un élément suivant et qu'il est accessible, ce qui fait que la condition de boucle arrête la boucle lorsque nous manquons d'éléments sur lesquels parcourir.

Utilisation de LINQ, C# 7 et System.ValueTuple Package NuGet, vous pouvez faire ceci :

foreach (var (value, index) in collection.Select((v, i)=>(v, i))) {
    Console.WriteLine(value + " is at index " + index);
}

Vous pouvez utiliser le régulier foreach construire et pouvoir accéder directement à la valeur et à l'index, pas en tant que membre d'un objet, et conserve les deux champs uniquement dans la portée de la boucle.Pour ces raisons, je pense que c'est la meilleure solution si vous êtes capable d'utiliser C# 7 et System.ValueTuple.

Vous pouvez envelopper l'énumérateur d'origine avec un autre qui contient les informations d'index.

foreach (var item in ForEachHelper.WithIndex(collection))
{
    Console.Write("Index=" + item.Index);
    Console.Write(";Value= " + item.Value);
    Console.Write(";IsLast=" + item.IsLast);
    Console.WriteLine();
}

Voici le code du ForEachHelper classe.

public static class ForEachHelper
{
    public sealed class Item<T>
    {
        public int Index { get; set; }
        public T Value { get; set; }
        public bool IsLast { get; set; }
    }

    public static IEnumerable<Item<T>> WithIndex<T>(IEnumerable<T> enumerable)
    {
        Item<T> item = null;
        foreach (T value in enumerable)
        {
            Item<T> next = new Item<T>();
            next.Index = 0;
            next.Value = value;
            next.IsLast = false;
            if (item != null)
            {
                next.Index = item.Index + 1;
                yield return item;
            }
            item = next;
        }
        if (item != null)
        {
            item.IsLast = true;
            yield return item;
        }            
    }
}

Il n'y a rien de mal à utiliser une variable compteur.En fait, que vous utilisiez for, foreach while ou do, une variable compteur doit être déclarée et incrémentée quelque part.

Utilisez donc cet idiome si vous n'êtes pas sûr d'avoir une collection correctement indexée :

var i = 0;
foreach (var e in collection) {
   // Do stuff with 'e' and 'i'
   i++;
}

Sinon, utilisez celui-ci si vous savoir que votre indexable la collection est O(1) pour l’accès à l’index (ce qui sera le cas pour Array et probablement pour List<T> (la documentation ne le dit pas), mais pas nécessairement pour d'autres types (comme LinkedList)):

// Hope the JIT compiler optimises read of the 'Count' property!
for (var i = 0; i < collection.Count; i++) {
   var e = collection[i];
   // Do stuff with 'e' and 'i'
}

Il ne devrait jamais être nécessaire de faire fonctionner « manuellement » le IEnumerator en invoquant MoveNext() et interroger Current - foreach cela vous épargne ce problème particulier...si vous devez sauter des éléments, utilisez simplement un continue dans le corps de la boucle.

Et juste pour être complet, selon ce que vous étiez faire avec votre index (les constructions ci-dessus offrent beaucoup de flexibilité), vous pouvez utiliser Parallel LINQ :

// First, filter 'e' based on 'i',
// then apply an action to remaining 'e'
collection
    .AsParallel()
    .Where((e,i) => /* filter with e,i */)
    .ForAll(e => { /* use e, but don't modify it */ });

// Using 'e' and 'i', produce a new collection,
// where each element incorporates 'i'
collection
    .AsParallel()
    .Select((e, i) => new MyWrapper(e, i));

Nous utilisons AsParallel() ci-dessus, car nous sommes déjà en 2014 et nous voulons faire bon usage de ces multiples cœurs pour accélérer les choses.De plus, pour LINQ « séquentiel », tu n'as qu'un ForEach() méthode d'extension sur List<T> et Array ...et il n'est pas clair que l'utiliser soit mieux que de faire un simple foreach, puisque vous utilisez toujours un seul thread pour une syntaxe plus laide.

Voici une solution que je viens de trouver à ce problème

Code d'origine :

int index=0;
foreach (var item in enumerable)
{
    blah(item, index); // some code that depends on the index
    index++;
}

Code mis à jour

enumerable.ForEach((item, index) => blah(item, index));

Méthode d'extension :

    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumerable, Action<T, int> action)
    {
        var unit = new Unit(); // unit is a new type from the reactive framework (http://msdn.microsoft.com/en-us/devlabs/ee794896.aspx) to represent a void, since in C# you can't return a void
        enumerable.Select((item, i) => 
            {
                action(item, i);
                return unit;
            }).ToList();

        return pSource;
    }
int index;
foreach (Object o in collection)
{
    index = collection.indexOf(o);
}

Cela fonctionnerait pour les collections prenant en charge IList.

C# 7 nous offre enfin une manière élégante de procéder :

static class Extensions
{
    public static IEnumerable<(int, T)> Enumerate<T>(
        this IEnumerable<T> input,
        int start = 0
    )
    {
        int i = start;
        foreach (var t in input)
        {
            yield return (i++, t);
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var s = new string[]
        {
            "Alpha",
            "Bravo",
            "Charlie",
            "Delta"
        };

        foreach (var (i, t) in s.Enumerate())
        {
            Console.WriteLine($"{i}: {t}");
        }
    }
}

Cela ne fonctionnera que pour une liste et non pour n'importe quel IEnumerable, mais dans LINQ, il y a ceci :

IList<Object> collection = new List<Object> { 
    new Object(), 
    new Object(), 
    new Object(), 
    };

foreach (Object o in collection)
{
    Console.WriteLine(collection.IndexOf(o));
}

Console.ReadLine();

@Jonathan, je n'ai pas dit que c'était une excellente réponse, j'ai juste dit que cela montrait simplement qu'il était possible de faire ce qu'il avait demandé :)

@Grapain Je ne m'attendrais pas à ce que ce soit rapide - je ne suis pas entièrement sûr de comment cela fonctionne, cela pourrait réitérer la liste entière à chaque fois pour trouver un objet correspondant, ce qui serait une sacrée comparaison.

Cela dit, List peut conserver un index de chaque objet avec le décompte.

Jonathan semble avoir une meilleure idée, s'il voulait bien développer ?

Il serait préférable de simplement compter où vous en êtes dans le foreach, plus simple et plus adaptable.

Ajoutez simplement votre propre index.Rester simple.

int i = 0;
foreach (var item in Collection)
{
    item.index = i;
    ++i;
}

C'est comme ça que je fais, ce qui est sympa pour sa simplicité/brièveté, mais si vous faites beaucoup de choses dans le corps de la boucle obj.Value, ça va vieillir assez vite.

foreach(var obj in collection.Select((item, index) => new { Index = index, Value = item }) {
    string foo = string.Format("Something[{0}] = {1}", obj.Index, obj.Value);
    ...
}

Pourquoi foreach ?!

La manière la plus simple consiste à utiliser pour au lieu de chaque fois si vous utilisez la liste .

for(int i = 0 ; i < myList.Count ; i++)
{
    // Do Something...
}

OU si vous voulez utiliser foreach :

foreach (string m in myList)
{
     // Do something...       
}

vous pouvez l'utiliser pour connaître l'index de chaque boucle :

myList.indexOf(m)

Mieux vaut utiliser un mot-clé continue construction sûre comme celle-ci

int i=-1;
foreach (Object o in collection)
{
    ++i;
    //...
    continue; //<--- safe to call, index will be increased
    //...
}

Si la collection est une liste, vous pouvez utiliser List.IndexOf, comme dans :

foreach (Object o in collection)
{
    // ...
    @collection.IndexOf(o)
}

La réponse principale indique :

"De toute évidence, le concept d'index est étranger au concept d'énumération et ne peut être réalisé."

Bien que cela soit vrai pour la version C# actuelle, il ne s’agit pas d’une limite conceptuelle.

La création d'une nouvelle fonctionnalité du langage C# par MS pourrait résoudre ce problème, ainsi que la prise en charge d'une nouvelle interface IIndexedEnumerable.

foreach (var item in collection with var index)
{
    Console.WriteLine("Iteration {0} has value {1}", index, item);
}

//or, building on @user1414213562's answer
foreach (var (item, index) in collection)
{
    Console.WriteLine("Iteration {0} has value {1}", index, item);
}

Si foreach reçoit un IEnumerable et ne peut pas résoudre un IIndexedEnumerable, mais qu'il est demandé avec var index, alors le compilateur C# peut envelopper la source avec un objet IndexedEnumerable, qui ajoute le code pour suivre l'index.

interface IIndexedEnumerable<T> : IEnumerable<T>
{
    //Not index, because sometimes source IEnumerables are transient
    public long IterationNumber { get; }
}

Pourquoi:

  • Foreach est plus joli et, dans les applications métier, il s'agit rarement d'un goulot d'étranglement en termes de performances.
  • Foreach peut être plus efficace sur la mémoire.Avoir un pipeline de fonctions au lieu de convertir en nouvelles collections à chaque étape.Peu importe s'il utilise quelques cycles CPU supplémentaires, s'il y a moins de défauts de cache CPU et moins de GC.Collects
  • Exiger du codeur qu'il ajoute un code de suivi d'index gâche la beauté
  • C'est assez facile à mettre en œuvre (merci MS) et est rétrocompatible

Bien que la plupart des gens ici ne soient pas MS, c'est une réponse correcte, et vous pouvez faire pression sur MS pour qu'elle ajoute une telle fonctionnalité.Vous pourriez déjà créer votre propre itérateur avec un fonction d'extension et utilisation de tuples, mais MS pourrait saupoudrer le sucre syntaxique pour éviter la fonction d'extension

Ma solution à ce problème est une méthode d'extension WithIndex(),

http://code.google.com/p/ub-dotnet-utilities/source/browse/trunk/Src/Utilities/Extensions/EnumerableExtensions.cs

Utilisez-le comme

var list = new List<int> { 1, 2, 3, 4, 5, 6 };    

var odd = list.WithIndex().Where(i => (i.Item & 1) == 1);
CollectionAssert.AreEqual(new[] { 0, 2, 4 }, odd.Select(i => i.Index));
CollectionAssert.AreEqual(new[] { 1, 3, 5 }, odd.Select(i => i.Item));

Par intérêt, Phil Haack vient d'écrire un exemple de ceci dans le contexte d'un délégué Razor Templated (http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)

En effet, il écrit une méthode d'extension qui encapsule l'itération dans une classe "IteratedItem" (voir ci-dessous) permettant l'accès à l'index ainsi qu'à l'élément pendant l'itération.

public class IndexedItem<TModel> {
  public IndexedItem(int index, TModel item) {
    Index = index;
    Item = item;
  }

  public int Index { get; private set; }
  public TModel Item { get; private set; }
}

Cependant, même si cela conviendrait dans un environnement non-Razor si vous effectuez une seule opération (c'est-à-direcelui qui pourrait être fourni sous forme lambda), cela ne constituera pas un remplacement solide de la syntaxe for/foreach dans des contextes non-Razor.

Je ne pense pas que cela devrait être très efficace, mais ça marche :

@foreach (var banner in Model.MainBanners) {
    @Model.MainBanners.IndexOf(banner)
}

J'ai construit ça dans LINQPad:

var listOfNames = new List<string>(){"John","Steve","Anna","Chris"};

var listCount = listOfNames.Count;

var NamesWithCommas = string.Empty;

foreach (var element in listOfNames)
{
    NamesWithCommas += element;
    if(listOfNames.IndexOf(element) != listCount -1)
    {
        NamesWithCommas += ", ";
    }
}

NamesWithCommas.Dump();  //LINQPad method to write to console.

Vous pouvez aussi simplement utiliser string.join:

var joinResult = string.Join(",", listOfNames);

Vous pouvez écrire votre boucle comme ceci :

var s = "ABCDEFG";
foreach (var item in s.GetEnumeratorWithIndex())
{
    System.Console.WriteLine("Character: {0}, Position: {1}", item.Value, item.Index);
}

Après avoir ajouté la structure et la méthode d'extension suivantes.

La méthode struct et extension encapsule la fonctionnalité Enumerable.Select.

public struct ValueWithIndex<T>
{
    public readonly T Value;
    public readonly int Index;

    public ValueWithIndex(T value, int index)
    {
        this.Value = value;
        this.Index = index;
    }

    public static ValueWithIndex<T> Create(T value, int index)
    {
        return new ValueWithIndex<T>(value, index);
    }
}

public static class ExtensionMethods
{
    public static IEnumerable<ValueWithIndex<T>> GetEnumeratorWithIndex<T>(this IEnumerable<T> enumerable)
    {
        return enumerable.Select(ValueWithIndex<T>.Create);
    }
}

Je ne pense pas qu'il existe un moyen d'obtenir la valeur de l'itération actuelle d'une boucle foreach.Se compter soi-même semble être le meilleur moyen.

Puis-je vous demander pourquoi voudriez-vous savoir ?

Il semble que vous feriez très probablement l’une des trois choses suivantes :

1) Récupérer l'objet de la collection, mais dans ce cas vous l'avez déjà.

2) Compter les objets pour un post-traitement ultérieur... les collections ont une propriété Count que vous pouvez utiliser.

3) Définir une propriété sur l'objet en fonction de son ordre dans la boucle... bien que vous puissiez facilement la définir lorsque vous avez ajouté l'objet à la collection.

À moins que votre collection ne puisse renvoyer l'index de l'objet via une méthode, le seul moyen est d'utiliser un compteur comme dans votre exemple.

Cependant, lorsque vous travaillez avec des index, la seule réponse raisonnable au problème consiste à utiliser une boucle for.Tout le reste introduit de la complexité dans le code, sans parler de la complexité temporelle et spatiale.

Que diriez-vous quelque chose comme ça?Notez que myDelimitedString peut être nul si myEnumerable est vide.

IEnumerator enumerator = myEnumerable.GetEnumerator();
string myDelimitedString;
string current = null;

if( enumerator.MoveNext() )
    current = (string)enumerator.Current;

while( null != current)
{
    current = (string)enumerator.Current; }

    myDelimitedString += current;

    if( enumerator.MoveNext() )
        myDelimitedString += DELIMITER;
    else
        break;
}

Je viens d'avoir ce problème, mais réfléchir au problème dans mon cas a donné la meilleure solution, sans rapport avec la solution attendue.

Cela pourrait être un cas assez courant, en gros, je lis à partir d'une liste source et je crée des objets basés sur eux dans une liste de destination. Cependant, je dois d'abord vérifier si les éléments source sont valides et je souhaite renvoyer la ligne de n'importe quel élément. erreur.À première vue, je souhaite obtenir l'index dans l'énumérateur de l'objet au niveau de la propriété Current. Cependant, lorsque je copie ces éléments, je connais de toute façon implicitement l'index actuel à partir de la destination actuelle.Évidemment, cela dépend de votre objet de destination, mais pour moi, c'était une liste, et il implémentera très probablement ICollection.

c'est à dire.

var destinationList = new List<someObject>();
foreach (var item in itemList)
{
  var stringArray = item.Split(new char[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries);

  if (stringArray.Length != 2)
  {
    //use the destinationList Count property to give us the index into the stringArray list
    throw new Exception("Item at row " + (destinationList.Count + 1) + " has a problem.");
  }
  else
  {
    destinationList.Add(new someObject() { Prop1 = stringArray[0], Prop2 = stringArray[1]});
  }
}

Pas toujours applicable, mais assez souvent pour mériter d'être mentionné, je pense.

Quoi qu'il en soit, le fait est que parfois il existe déjà une solution non évidente dans la logique que vous avez...

Je n'étais pas sûr de ce que vous essayiez de faire avec les informations d'index en fonction de la question.Cependant, en C#, vous pouvez généralement adapter la méthode IEnumerable.Select pour obtenir l'index de ce que vous voulez.Par exemple, je pourrais utiliser quelque chose comme ceci pour savoir si une valeur est paire ou impaire.

string[] names = { "one", "two", "three" };
var oddOrEvenByName = names
    .Select((name, index) => new KeyValuePair<string, int>(name, index % 2))
    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

Cela vous donnerait un dictionnaire par nom indiquant si l'élément était impair (1) ou pair (0) dans la liste.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top