Jak uzyskać indeks bieżącej iteracji pętli foreach?


938

Czy istnieje jakiś rzadki konstrukt języka, którego nie spotkałem (jak kilka, których ostatnio się nauczyłem, niektóre na stosie przepełnienia) w języku C #, aby uzyskać wartość reprezentującą bieżącą iterację pętli foreach?

Na przykład obecnie robię coś takiego w zależności od okoliczności:

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}

1
pobieranie rzutowania foreach na ogół nie będzie dla mnie bardziej zoptymalizowane niż używanie dostępu do kolekcji na podstawie indeksów, choć w wielu przypadkach będzie ono równe. Celem foreach jest uczynienie twojego kodu czytelnym, ale (zwykle) dodaje warstwę pośrednictwa, która nie jest darmowa.
Brian

8
Powiedziałbym, że głównym celem foreachjest zapewnienie wspólnego mechanizmu iteracji dla wszystkich kolekcji, niezależnie od tego, czy są one indeksowalne ( List), czy nie ( Dictionary).
Brian Gideon

2
Cześć Brian Gideon - zdecydowanie się zgadzam (to było kilka lat temu, a ja byłem wtedy znacznie mniej doświadczony). Jednakże, chociaż Dictionarynie można go indeksować, iteracja Dictionaryprzechodzi przez niego w określonej kolejności (tzn. Moduł wyliczający jest indeksowany przez to, że dostarcza elementy kolejno). W tym sensie moglibyśmy powiedzieć, że nie szukamy indeksu w kolekcji, a raczej indeksu bieżącego wyliczonego elementu w wyliczeniu (tj. Czy znajdujemy się na pierwszym, piątym czy ostatnim wyliczonym elemencie).
Matt Mitchell

4
foreach pozwala również kompilatorowi na pomijanie granic sprawdzania dostępu do każdej tablicy w skompilowanym kodzie. Użycie for z indeksem sprawi, że środowisko wykonawcze sprawdzi, czy dostęp do indeksu jest bezpieczny.
IvoTops

1
Ale to nieprawda. Jeśli nie zmienisz zmiennej iteracyjnej pętli for w pętli, kompilator wie, jakie są jej granice i nie musi ich ponownie sprawdzać. Jest to tak częsty przypadek, że każdy porządny kompilator go zaimplementuje.
Jim Balter

Odpowiedzi:


550

foreachJest Iterowanie nad kolekcje implementujące IEnumerable. Robi to poprzez wywołanie GetEnumeratorkolekcji, która zwróci an Enumerator.

Ten moduł wyliczający ma metodę i właściwość:

  • MoveNext ()
  • obecny

Currentzwraca obiekt, na którym aktualnie znajduje się moduł Enumerator, MoveNextaktualizuje Currentnastępny obiekt.

Pojęcie indeksu jest obce koncepcji wyliczenia i nie można tego zrobić.

Z tego powodu większość kolekcji można przeglądać przy użyciu indeksatora i konstrukcji pętli for.

W tej sytuacji zdecydowanie wolę używać pętli for niż śledzenie indeksu za pomocą zmiennej lokalnej.


165
„Oczywiście koncepcja indeksu jest obca koncepcji wyliczenia i nie można tego zrobić”. - To nonsens, jak jasno wyrażają odpowiedzi Davida B i Bcahilla. Indeks jest wyliczeniem w zakresie i nie ma powodu, dla którego nie można wyliczyć dwóch rzeczy równolegle ... dokładnie to robi forma indeksowania Enumerable.Select.
Jim Balter

11
Podstawowy przykład kodu:for(var i = 0; i < myList.Count; i ++){System.Diagnostics.Debug.WriteLine(i);}
Chad Hedgcock 14.04.15

22
@JimBalter: To pierwszy link, który przeczytałem. Nie rozumiem, jak to wspiera twoje stanowisko. Nie odpycham też ad-homów i dzielących mnie słów, gdy odpowiadam. Jako przykład przytaczam twoje użycie „bzdur”, „niezwykłego zamieszania”, „całkowicie błędnego i zmieszanego”, „Mam 37 głosów pozytywnych” itp. (Myślę, że twoje ego jest tutaj trochę na linii). Zamiast tego Chciałbym uprzejmie poprosić, aby nie przeglądać innych za pomocą swojego „argumentum ad verecundiam”, a zamiast tego zachęcam do zastanowienia się nad sposobami wspierania ludzi tutaj na StackOverflow. Powiedziałbym, że Jon Skeet jest tutaj obywatelem modelowym.
Precel

11
Pretzel: Twoje użycie Jona Skeeta jako wzorcowego obywatela jest błędne, ponieważ będzie bić (i głosować) kogoś, kto się z nim nie zgadza, jak się przekonałem. Jim Balter: Niektóre z twoich komentarzy były zbyt agresywne i mogłeś przedstawić swoje punkty z mniejszym gniewem i większym wykształceniem.
Suncat2000,

7
@Pretzel Jim ma na myśli to, że dopóki można zmapować elementy na sekwencję liczb całkowitych, można je indeksować. Nie ma znaczenia, że ​​sama klasa nie przechowuje indeksu. Dodatkowo, że połączone lista nie ma rozkaz tylko wzmacnia pozycję Jima. Wszystko, co musisz zrobić, to numerować każdy element w kolejności. W szczególności możesz to osiągnąć, zwiększając liczbę podczas iteracji, lub możesz wygenerować listę liczb całkowitych o tej samej długości, a następnie spakować je (jak w zipfunkcji Pythona ).
jpmc26,

666

Ian Mercer opublikował podobne rozwiązanie na blogu Phila Haacka :

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

W ten sposób otrzymasz item ( item.value) i jego indeks ( item.i), korzystając z tego przeciążenia LINQSelect :

drugi parametr funkcji [wewnątrz Select] reprezentuje indeks elementu źródłowego.

new { i, value }Tworzy nowy obiekt anonimowy .

Przydziałów sterty można uniknąć, ValueTuplejeśli używasz C # 7.0 lub nowszej wersji:

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

Możesz także wyeliminować item., używając automatycznej destrukcji:

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

9
To rozwiązanie jest dobre w przypadku szablonu Razor, w którym porządek szablonu jest nietrywialną kwestią projektową, a Ty chcesz również użyć indeksu każdego wyliczonego elementu. Należy jednak pamiętać, że przydział obiektów z „zawijania” zwiększa koszty (w przestrzeni i czasie) nad (nieuniknionym) zwiększaniem liczby całkowitej.
David Bullock,

6
@mjsr Dokumentacja jest tutaj .
Thorkil Holm-Jacobsen

19
Niestety - to sprytne, ale czy naprawdę jest bardziej czytelne niż tworzenie indeksu poza foreach i zwiększanie go w każdej pętli?
jbyrd

13
W późniejszych wersjach C #, więc również używasz krotek, więc będziesz mieć coś takiego: foreach (var (item, i) w Model.Select ((v, i) => (v, i))) Pozwala na uzyskaj dostęp do elementu i indeksu (i) bezpośrednio wewnątrz pętli for z dekonstrukcją krotki.
Haukman

5
Czy ktoś może mi wyjaśnić, dlaczego jest to dobra odpowiedź (ponad 450 głosów pozytywnych w momencie pisania)? Z tego, co widzę, jest trudniejsze do zrozumienia niż zwykłe zwiększanie licznika, dlatego jest mniej konserwowalne, zużywa więcej pamięci i prawdopodobnie jest wolniejsze. Czy coś brakuje?
Rich N

182

Wreszcie C # 7 ma przyzwoitą składnię do pobierania indeksu wewnątrz foreachpętli (tj. Krotek):

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

Potrzebna byłaby mała metoda rozszerzenia:

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

8
Ta odpowiedź jest niedoceniana, ponieważ krotki są znacznie czystsze
Todd

7
Zmodyfikowano, aby obsługiwał kolekcje zerowe:public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self) => self?.Select((item, index) => (item, index)) ?? new List<(T, int)>();
2Toad

Niezłe. Najbardziej podoba mi się to rozwiązanie.
FranzHuber23

To najlepsza odpowiedź
w0ns88

2
Przydatne może być wywołanie metody, Enumeratedaby była bardziej rozpoznawalna dla osób przyzwyczajonych do innych języków (i być może zamień też kolejność parametrów krotki). Nie, WithIndexto i tak nie jest oczywiste.
FernAndr

114

Może zrobić coś takiego:

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));
    }
}

12
To nie „naprawdę” rozwiązuje problemu. Pomysł jest dobry, ale nie
omija

To nie działa, jeśli w naszym pętli for znajduje się instrukcja return, jeśli zmienisz w tym celu „ForEachWithIndex”, to nie jest to ogólne, lepiej pisz regularne dla pętli
Shankar Raju

Twoje wywołanie ForEachWithIndex jest równoważne temu przy użyciu Linq Select, który pobiera ciąg znaków i indeks:values.Select((item, idx) => { Console.WriteLine("{0}: {1}", idx, item); return item; }).ToList();
user2023861 28.01.2016

95

Nie zgadzam się z komentarzami, że forpętla jest lepszym wyborem w większości przypadków.

foreachjest użyteczną konstrukcją i nie może być zastąpiona przez forpętlę we wszystkich okolicznościach.

Na przykład, jeśli masz DataReader i przeglądasz wszystkie rekordy za jego pomocą, foreachto automatycznie wywołuje metodę Dispose i zamyka czytnik (który może wtedy automatycznie zamknąć połączenie). Jest to zatem bezpieczniejsze, ponieważ zapobiega wyciekom połączenia, nawet jeśli zapomnisz zamknąć czytnik.

(Pewnie, że dobrą praktyką jest zawsze zamykać czytniki, ale kompilator nie złapie tego, jeśli tego nie zrobisz - nie możesz zagwarantować, że zamknąłeś wszystkie czytniki, ale możesz zwiększyć prawdopodobieństwo, że nie wyciekniesz z połączenia przez uzyskanie w zwyczaju używania foreach.)

Mogą istnieć inne przykłady ukrytego wywołania Disposemetody, które są przydatne.


2
Dzięki za zwrócenie na to uwagi. Raczej subtelny. Możesz uzyskać więcej informacji na pvle.be/2010/05/foreach-statement-calls-dispose-on-ienumerator i msdn.microsoft.com/en-us/library/aa664754(VS.71).aspx .
Mark Meuer

+1. Pisałem bardziej szczegółowo o tym, czym foreachróżni się for(i bliżej while) na Programmers.SE .
Arseni Mourzenko

64

Dosłowna odpowiedź - ostrzeżenie, wydajność może nie być tak dobra, jak zwykłe użycie intdo śledzenia indeksu. Przynajmniej jest to lepsze niż używanie IndexOf.

Wystarczy użyć przeciążenia indeksowania Select, aby owinąć każdy element w kolekcji anonimowym obiektem znającym indeks. Można to zrobić w stosunku do wszystkiego, co implementuje 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);
}

3
Jedynym powodem użycia OfType <T> () zamiast Cast <T> () jest to, że niektóre elementy w wyliczeniu mogą zawieść jawne rzutowanie. W przypadku obiektów nigdy tak nie będzie.
dahlbyk

13
Jasne, z wyjątkiem innego powodu używania OfType zamiast Cast - czyli tego, że nigdy nie używam Cast.
Amy B

36

Korzystając z LINQ, C # 7 i System.ValueTuplepakietu NuGet, możesz to zrobić:

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

Możesz użyć zwykłej foreachkonstrukcji i mieć bezpośredni dostęp do wartości i indeksu, a nie jako element obiektu, i zachowuje oba pola tylko w zakresie pętli. Z tych powodów uważam, że jest to najlepsze rozwiązanie, jeśli możesz używać C # 7 i System.ValueTuple.


Czym różni się to od odpowiedzi user1414213562 ?
Edward Brey,

@EdwardBrey Chyba nie jest. To pytanie ma wiele odpowiedzi, prawdopodobnie po prostu przeoczyłem je lub nie zauważyłem, co robi, ponieważ podzielił część logiki na metodę rozszerzenia.
Pavel

1
Jest inaczej, ponieważ .Select jest wbudowany w LINQ. Nie musisz pisać własnej funkcji? Musisz jednak zainstalować VS „System.ValueTuple”.
Anton

33

Korzystając z odpowiedzi @ FlySwat, wymyśliłem to rozwiązanie:

//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
}

Otrzymujesz moduł wyliczający za pomocą, GetEnumeratora następnie zapętlasz za pomocą forpętli. Jednak sztuczka polega na tym, aby ustawić stan pętli listEnumerator.MoveNext() == true.

Ponieważ MoveNextmetoda modułu wyliczającego zwraca wartość true, jeśli istnieje kolejny element i można do niego uzyskać dostęp, co powoduje, że warunek pętli powoduje zatrzymanie pętli, gdy zabraknie elementów do iteracji.


10
Nie ma potrzeby porównywania listEnumerator.MoveNext () == true. To tak, jakby zapytać komputer, czy prawda == prawda? :) Po prostu powiedz, jeśli listEnumerator.MoveNext () {}
Kliknij tutaj,

9
@ Zesty, masz absolutną rację. Czułem, że bardziej czytelne jest dodanie go w tym przypadku, szczególnie dla ludzi, którzy nie są przyzwyczajeni do wpisywania niczego innego niż ja <blahSize jako warunek.
Gezim,

@Gezim Nie mam nic przeciwko predykatom tutaj, ale rozumiem, że nie wygląda to na predykat.
John Dvorak

1
Powinieneś pozbyć się modułu wyliczającego.
Antonín Lejsek

@ AntonínLejsek Moduł wyliczający nie implementuje IDisposable.
Edward Brey,

27

Nie ma nic złego w używaniu zmiennej licznika. W rzeczywistości, niezależnie od tego, czy używasz for, foreach whileczy do, zmienną licznika należy gdzieś zadeklarować i zwiększyć.

Użyj więc tego idiomu, jeśli nie masz pewności, czy masz odpowiednio indeksowaną kolekcję:

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

W przeciwnym razie skorzystaj z tej, jeśli wiesz, że twoja kolekcja indeksowana ma wartość O (1) w celu uzyskania dostępu do indeksu (dla której będzie Arrayi prawdopodobnie List<T>(dokumentacja tego nie mówi), ale niekoniecznie dla innych typów (takich jak 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'
}

Nigdy nie powinno być konieczne „ręczne” obsługiwanie IEnumeratorprzez wywoływanie MoveNext()i przesłuchiwanie Current- foreachto oszczędza ci tego szczególnego kłopotu ... jeśli chcesz pominąć przedmioty, po prostu użyj continuew ciele pętli.

I tylko dla kompletności, w zależności od tego, co robiłeś z indeksem (powyższe konstrukcje oferują dużą elastyczność), możesz użyć 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));

Używamy AsParallel()powyżej, ponieważ to już 2014 rok i chcemy dobrze wykorzystać te wiele rdzeni, aby przyspieszyć. Ponadto w przypadku „sekwencyjnego” LINQ otrzymujesz tylko ForEach()metodę rozszerzenia List<T>iArray … i nie jest jasne, że użycie jej jest lepsze niż wykonanie prostej foreach, ponieważ nadal używasz jednowątkowej składni dla brzydszych.


26

Możesz owinąć oryginalny moduł wyliczający innym, który zawiera informacje o indeksie.

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

Oto kod ForEachHelperklasy.

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;
        }            
    }
}

To faktycznie nie zwróci indeksu elementu. Zamiast tego zwróci indeks wewnątrz wyliczonej listy, która może być tylko podlistą listy, tym samym zapewniając dokładne dane tylko wtedy, gdy podlista i lista są równej wielkości. Zasadniczo za każdym razem, gdy w kolekcji znajdują się obiekty spoza żądanego typu, indeks będzie niepoprawny.
Lucas B

7
@Lucas: Nie, ale zwróci indeks bieżącej iteracji Foreach. To było pytanie.
Brian Gideon,

20

Oto rozwiązanie, które właśnie wymyśliłem dla tego problemu

Oryginalny kod:

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

Zaktualizowany kod

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

Metoda rozszerzenia:

    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;
    }

16

Po prostu dodaj własny indeks. Nie komplikuj.

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

15

Będzie działać tylko dla listy, a nie dla dowolnej IEnumerable, ale w LINQ jest to:

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

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

Console.ReadLine();

@Jathanathan Nie powiedziałem, że to świetna odpowiedź, po prostu powiedziałem, że pokazuje, że można zrobić to, o co prosił :)

@Graphain Nie spodziewałbym się, że będzie szybki - nie jestem do końca pewien, jak to działa, może za każdym razem powtarzać całą listę, aby znaleźć pasujący obiekt, który byłby niezrównanym porównaniem.

To powiedziawszy, List może przechowywać indeks każdego obiektu wraz z liczbą.

Jonathan wydaje się mieć lepszy pomysł, gdyby opracował?

Lepiej byłoby po prostu policzyć, gdzie jesteś na wyciągnięcie ręki, prostsze i bardziej elastyczne.


5
Nie jestem pewien w przypadku ciężkich głosowań. Pewnie wydajność sprawia, że ​​jest to wygórowane, ale odpowiedziałeś na pytanie!
Matt Mitchell,

4
Innym problemem jest to, że działa tylko wtedy, gdy elementy na liście są unikalne.
CodesInChaos

14

C # 7 w końcu daje nam elegancki sposób na zrobienie tego:

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}");
        }
    }
}

Tak, a stwardnienie rozsiane powinno rozszerzyć CLR / BCL, aby tego rodzaju rzeczy były rodzime.
Todd

14

Po co przepowiadać?

Najprostszym sposobem jest użycie do zamiast foreach jeśli używasz listy :

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

Lub jeśli chcesz użyć foreach:

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

Możesz użyć tego do poznania indeksu każdej pętli:

myList.indexOf(m)

8
Rozwiązanie indexOf jest nieprawidłowe dla listy z duplikatami, a także działa bardzo wolno.
tymtam,

2
Problemem, którego należy unikać, jest wielokrotne przeglądanie IEnumerable, np. W celu uzyskania liczby elementów, a następnie każdego elementu. Ma to wpływ na przykład, gdy IEnumerable jest wynikiem zapytania do bazy danych.
David Clarke

2
myList.IndexOf () jest O (n), więc twoja pętla będzie O (n ^ 2).
Patrick Beard

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

Działa to w przypadku obsługi kolekcji IList.


68
Dwa problemy: 1) Jest tak, O(n^2)ponieważ w większości wdrożeń IndexOfjest O(n). 2) Nie powiedzie się, jeśli na liście znajdują się zduplikowane elementy.
CodesInChaos

15
Uwaga: O (n ^ 2) oznacza, że ​​może to być katastrofalnie wolne w przypadku dużej kolekcji.
O'Rooney

Świetny wynalazek do użycia metody IndexOf! Właśnie tego szukałem, aby uzyskać indeks (liczbę) w pętli foreach! Big Thx
Mitja Bonca

19
Boże, mam nadzieję, że tego nie użyłeś! :( Używa tej zmiennej, której nie chciałeś utworzyć - w rzeczywistości utworzy n + 1 ints, ponieważ ta funkcja musi również utworzyć jedną, aby zwrócić również - i ten indeks wyszukiwania jest znacznie, dużo wolniejszy niż jedna operacja inkrementacji liczb całkowitych na każdym kroku. Dlaczego ludzie nie zagłosują na tę odpowiedź?
canahari

13
Nie używaj tej odpowiedzi, znalazłem twardą prawdę wymienioną w jednym z komentarzy. „Nie udaje się, jeśli na liście znajdują się zduplikowane elementy.” !!!
Bruce

9

Tak to robię, co jest miłe ze względu na swoją prostotę / zwięzłość, ale jeśli dużo robisz w ciele pętli obj.Value, szybko się zestarzeje.

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

8

Ta odpowiedź: lobbować zespół językowy C # w celu uzyskania bezpośredniej pomocy językowej.

Wiodąca odpowiedź brzmi:

Oczywiście koncepcja indeksu jest obca koncepcji wyliczenia i nie można tego zrobić.

Chociaż dotyczy to obecnej wersji językowej C # (2020), nie jest to koncepcyjny limit CLR / język, można to zrobić.

Zespół programistów języka Microsoft C # mógłby stworzyć nową funkcję języka C #, dodając obsługę nowego interfejsu 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);
}

Gdyby foreach () jest używany i with var indexjest obecny, kompilator oczekuje, że kolekcja elementów zadeklaruje IIndexedEnumerableinterfejs. Jeśli interfejs jest nieobecny, kompilator może zapełnić polifile źródło z obiektem IndexedEnumerable, który dodaje kod do śledzenia indeksu.

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

Później można zaktualizować CLR, aby zawierał wewnętrzne śledzenie indeksu, które jest używane tylko wtedy, gdy withokreślono słowo kluczowe, a źródło nie implementuje bezpośrednioIIndexedEnumerable

Dlaczego:

  • Foreach wygląda ładniej, aw aplikacjach biznesowych pętle foreach rzadko stanowią wąskie gardło wydajności
  • Foreach może być bardziej wydajny w zakresie pamięci. Posiadanie szeregu funkcji zamiast konwersji do nowych kolekcji na każdym kroku. Kogo to obchodzi, jeśli zużyje kilka dodatkowych cykli procesora, gdy jest mniej błędów pamięci podręcznej procesora i mniej śmieci.
  • Wymaganie od kodera dodania kodu śledzenia indeksu psuje piękno
  • Jest dość łatwy do wdrożenia (proszę Microsoft) i jest kompatybilny wstecz

Chociaż większość ludzi tutaj nie jest pracownikami Microsoft, jest to poprawna odpowiedź, możesz lobbować Microsoft, aby dodać taką funkcję. Możesz już zbudować swój iterator z funkcją rozszerzenia i używać krotek , ale Microsoft może posypać cukrem składniowym, aby uniknąć funkcji rozszerzenia


Czekaj, więc czy ta funkcja języka już istnieje, czy jest proponowana na przyszłość?
Pavel

1
@Pavel Zaktualizowałem odpowiedź, aby była jasna. Ta odpowiedź została udzielona, ​​aby odeprzeć wiodącą odpowiedź, która brzmi: „Oczywiście koncepcja indeksu jest obca koncepcji wyliczenia i nie można tego zrobić”.
Todd

6

Jeśli kolekcja jest listą, możesz użyć List.IndexOf, jak w:

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

13
A teraz algorytmem jest O (n ^ 2) (jeśli nie gorsze). Myślałem bardzo ostrożnie przed użyciem tego. Jest to także duplikat odpowiedzi
@crucible

1
@BradleyDotNET ma dokładnie rację, nie używaj tej wersji.
Oskar

1
Uważaj na to! Jeśli masz zduplikowany element na liście, uzyska on pozycję pierwszego!
Sonhja,

5

Lepiej używać continuetakiej bezpiecznej konstrukcji słów kluczowych

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

5

Możesz napisać swoją pętlę w następujący sposób:

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

Po dodaniu następującej struktury i metody rozszerzenia.

Metoda struct i extension zawiera funkcjonalność 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);
    }
}

3

Moim rozwiązaniem tego problemu jest metoda rozszerzenia WithIndex() ,

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

Użyj tego jak

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));

Użyłbym structdla pary (indeks, element).
CodesInChaos

3

Dla zainteresowania Phil Haack właśnie napisał przykład tego w kontekście delegata szablonów Razor ( http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx )

W efekcie pisze metodę rozszerzenia, która otacza iterację klasą „IteratedItem” (patrz poniżej), umożliwiając dostęp do indeksu oraz elementu podczas iteracji.

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; }
}

Jednak chociaż byłoby to w porządku w środowisku innym niż Razor, jeśli wykonujesz jedną operację (tj. Taką, która może być podana jako lambda), nie będzie to solidne zastąpienie składni for / foreach w kontekstach innych niż Razor .


3

Nie sądzę, że powinno to być dość wydajne, ale działa:

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

3

Zbudowałem to w 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.

Możesz także użyć string.join:

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

Możesz także użyć ciągu string.join w c # Na przykład: var joinResult = string.Join (",", listOfNames); '
Warren LaFrance

1
Czy ktoś może mi wyjaśnić, co to jest O (n * n)?
Axel

@Axel to w zasadzie oznacza, że ​​operacje wymagane do obliczenia wzrostu wyniku są kwadratowe, tzn. Jeśli istnieją nelementy, wówczas operacje są n * nlub są nkontynuowane. en.wikipedia.org/wiki/…
Richard Hansell

2

Nie wierzę, że istnieje sposób na uzyskanie wartości bieżącej iteracji pętli foreach. Liczenie siebie wydaje się najlepszym sposobem.

Czy mogę zapytać, dlaczego chcesz wiedzieć?

Wygląda na to, że najprawdopodobniej zrobiłbyś jedną z trzech rzeczy:

1) Pobieranie obiektu z kolekcji, ale w tym przypadku już go masz.

2) Liczenie obiektów do późniejszego przetwarzania końcowego ... kolekcje mają właściwość Count, z której można skorzystać.

3) Ustawienie właściwości obiektu na podstawie jego kolejności w pętli ... chociaż możesz łatwo ustawić tę właściwość, gdy dodasz obiekt do kolekcji.


4) Przypadek, który trafiłem kilka razy, jest czymś innym, co należy zrobić przy pierwszym lub ostatnim przejściu - powiedz listę obiektów, które zamierzasz wydrukować i potrzebujesz przecinków między elementami, ale nie po ostatnim.
Loren Pechtel,

2

O ile twoja kolekcja nie może zwrócić indeksu obiektu za pomocą jakiejś metody, jedynym sposobem jest użycie licznika jak w twoim przykładzie.

Jednak podczas pracy z indeksami jedyną rozsądną odpowiedzią na problem jest użycie pętli for. Wszystko inne wprowadza złożoność kodu, nie mówiąc już o złożoności czasu i przestrzeni.


2

Co powiesz na coś takiego? Zauważ, że myDelimitedString może mieć wartość NULL, jeśli myEnumerable jest pusty.

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;
}

Wiele problemów tutaj. A) dodatkowy aparat ortodontyczny. B) concat łańcucha + = na iterację pętli.
enorl76,

2

Właśnie miałem ten problem, ale zastanowienie się nad tym problemem w moim przypadku dało najlepsze rozwiązanie, niezwiązane z oczekiwanym rozwiązaniem.

To może być dość powszechny przypadek, w zasadzie czytam z jednej listy źródłowej i tworzę obiekty na ich podstawie na liście docelowej, jednak muszę najpierw sprawdzić, czy elementy źródłowe są prawidłowe i chcę zwrócić wiersz dowolnego błąd. Na pierwszy rzut oka chcę wprowadzić indeks do modułu wyliczającego obiekt we właściwości Current, jednak gdy kopiuję te elementy, i tak domyślnie znam bieżący indeks z bieżącego miejsca docelowego. Oczywiście zależy to od docelowego obiektu, ale dla mnie była to lista i najprawdopodobniej zaimplementuje ICollection.

to znaczy

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]});
  }
}

Myślę, że nie zawsze ma zastosowanie, ale wystarczająco często, aby warto było o tym wspomnieć.

W każdym razie chodzi o to, że czasami w logice masz już nieoczywiste rozwiązanie ...


2

Nie byłem pewien, co próbujesz zrobić z informacjami indeksu opartymi na pytaniu. Jednak w języku C # zwykle można dostosować metodę IEnumerable.Select, aby uzyskać indeks z dowolnego elementu. Na przykład mogę użyć czegoś takiego do ustalenia, czy wartość jest nieparzysta, czy parzysta.

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);

Dałoby to słownik według nazwy, czy element był nieparzysty (1), a nawet (0) na liście.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.