Как вы получаете индекс текущей итерации цикла foreach? - PullRequest
789 голосов
/ 04 сентября 2008

Есть ли какая-нибудь редкая языковая конструкция, с которой я не сталкивался (например, немногие, которые я недавно узнал, некоторые о переполнении стека) в C # для получения значения, представляющего текущую итерацию цикла foreach?

Например, в настоящее время я делаю что-то подобное в зависимости от обстоятельств:

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

Ответы [ 34 ]

524 голосов
/ 11 июля 2012

Ян Мерсер опубликовал подобное решение на Блог Фила Хаака :

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

При этом вы получаете элемент (item.value) и его индекс (item.i), используя эту перегрузку Select:

второй параметр функции [inside Select] представляет индекс исходного элемента.

new { i, value } создает новый анонимный объект .

Распределения кучи можно избежать, используя ValueTuple, если вы используете C # 7.0 или новее:

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

Вы также можете устранить item. с помощью автоматической деструктуризации:

<ol>
foreach ((MyType value, Int32 i) in Model.Select((value, i) => ( value, i )))
{
    <li id="item_@i">@value</li>
}
</ol>
493 голосов
/ 04 сентября 2008

foreach предназначен для перебора коллекций, которые реализуют IEnumerable. Это делается путем вызова GetEnumerator для коллекции, которая вернет Enumerator.

Этот перечислитель имеет метод и свойство:

  • MoveNext ()
  • Текущее

Current возвращает объект, на котором в данный момент работает Enumerator, MoveNext обновляет Current до следующего объекта.

Понятие индекса чуждо понятию перечисления и не может быть сделано.

В связи с этим большинство коллекций можно просматривать с помощью индексатора и конструкции цикла for.

Я очень предпочитаю использовать цикл for в этой ситуации по сравнению с отслеживанием индекса с помощью локальной переменной.

106 голосов
/ 04 сентября 2008

Может сделать что-то вроде этого:

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));
    }
}
87 голосов
/ 12 октября 2016

Наконец, C # 7 имеет приличный синтаксис для получения индекса внутри цикла foreach (т. Е. Кортежей):

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

Потребуется небольшой метод расширения:

public static IEnumerable<(T item, int index)> WithIndex<T>(this IEnumerable<T> self)       
   => self.Select((item, index) => (item, index)); 
83 голосов
/ 26 июня 2009

Я не согласен с комментариями о том, что цикл for является лучшим выбором в большинстве случаев.

foreach - полезная конструкция, которая не может быть заменена циклом for при любых обстоятельствах.

Например, если у вас есть DataReader и перебираете все записи, используя foreach, он автоматически вызывает метод Dispose и закрывает читатель (который затем может закрыть соединение автоматически). Поэтому это безопаснее, поскольку предотвращает утечки соединения, даже если вы забыли закрыть ридер.

(Конечно, это хорошая практика - всегда закрывать читатели, но компилятор не поймает его, если вы этого не сделаете - вы не можете гарантировать, что закрыли все читатели, но вы можете сделать это с большей вероятностью, что не пропустите связи, привыкнув использовать foreach.)

Могут быть и другие примеры неявного вызова метода Dispose, полезного.

57 голосов
/ 17 сентября 2008

Буквальный ответ - предупреждение, производительность может быть не такой хорошей, как при использовании int для отслеживания индекса. По крайней мере, это лучше, чем использовать IndexOf.

Вам просто нужно использовать перегрузку индексации Select, чтобы обернуть каждый элемент в коллекции анонимным объектом, который знает индекс. Это может быть сделано против всего, что реализует 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);
}
32 голосов
/ 08 октября 2013

Используя ответ @ FlySwat, я нашел следующее решение:

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

Вы получаете перечислитель, используя GetEnumerator, а затем зацикливаетесь, используя цикл for. Однако хитрость заключается в том, чтобы сделать условие цикла listEnumerator.MoveNext() == true.

Поскольку метод перечислителя MoveNext возвращает значение true, если есть следующий элемент, и к нему можно получить доступ, благодаря чему условие цикла останавливает цикл, когда у нас заканчиваются элементы для повторения.

26 голосов
/ 10 сентября 2017

Используя LINQ, C # 7 и пакет System.ValueTuple NuGet, вы можете сделать это:

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

Вы можете использовать обычную конструкцию foreach и иметь возможность прямого доступа к значению и индексу, а не к элементу объекта, и оба поля сохраняются только в области действия цикла. По этим причинам я считаю, что это лучшее решение, если вы можете использовать C # 7 и System.ValueTuple.

24 голосов
/ 20 июля 2010

Вы можете заключить оригинальный перечислитель в другой, содержащий информацию индекса.

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

Вот код для ForEachHelper класса.

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;
        }            
    }
}
22 голосов
/ 25 февраля 2014

Нет ничего плохого в использовании переменной счетчика. Фактически, используете ли вы for, foreach while или do, переменная счетчика должна где-то быть объявлена ​​и увеличена.

Так что используйте эту идиому, если вы не уверены, что у вас есть соответственно проиндексированная коллекция:

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

Иначе используйте этот вариант, если вы знаете , что ваша индексируемая коллекция имеет значение O (1) для доступа к индексу (что будет для Array и, вероятно, для List<T> ( в документации ничего не сказано), но не обязательно для других типов (например, 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'
}

Никогда не нужно «вручную» управлять IEnumerator, вызывая MoveNext() и опрашивая Current - foreach, что спасает вас от этого конкретного беспокойства ... если вам нужно пропустить элементы, просто используйте continue в теле цикла.

И просто для полноты, в зависимости от того, что вы делаете с вашим индексом (приведенные выше конструкции предлагают большую гибкость), вы можете использовать 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));

Мы используем AsParallel() выше, потому что уже 2014 год, и мы хотим эффективно использовать эти несколько ядер, чтобы ускорить процесс. Кроме того, для «последовательного» LINQ, вы получаете метод расширения ForEach() только для List<T> и Array ... и не ясно, что его использование лучше, чем простое foreach, поскольку вы все еще используете однопоточный для более уродливого синтаксиса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...