Как вы получаете индекс текущей итерации цикла foreach?

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

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

int i = 0;
foreach (Object o in collection)
{
    // ...
    i++;
}
Это было полезно?

Решение

Тот Самый foreach предназначен для перебора коллекций, реализующих IEnumerable.Он делает это, вызывая GetEnumerator о коллекции, которая вернет Enumerator.

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

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

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

Концепция индекса чужда концепции перечисления и не может быть реализована.

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

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

Другие советы

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

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

Это даст вам нужный товар (item.value) и его индекс (item.i) с помощью эта перегрузка Linq's 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>

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

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

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

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

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

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

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

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

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

Буквальный ответ - предупреждение, производительность может быть не такой хорошей, как при простом использовании 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);
}

Используя ответ @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, если есть следующий элемент и к нему можно получить доступ, таким образом, условие цикла останавливает цикл, когда у нас заканчиваются элементы для повторения.

Используя 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.

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

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

Нет ничего плохого в использовании переменной counter.На самом деле, используете ли вы 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, поскольку вы все еще используете однопоточный для более уродливого синтаксиса.

Вот решение, которое я только что придумал для этой проблемы

Исходный код:

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

Обновленный код

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

Способ расширения:

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

Это сработало бы для коллекций, поддерживающих IList.

C # 7, наконец, дает нам элегантный способ сделать это:

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

Это будет работать только для списка, а не для любого IEnumerable, но в LINQ есть это:

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 Я не говорил, что это отличный ответ, я просто сказал, что это просто показывает, что можно сделать то, о чем он просил :)

@Graphain Я бы не ожидал, что это будет быстро - я не совсем уверен, как это работает, это могло бы повторяться по всему списку каждый раз, чтобы найти соответствующий объект, что потребовало бы адского количества сравнений.

Тем не менее, List может содержать индекс каждого объекта вместе с количеством.

У Джонатана, кажется, есть идея получше, не мог бы он поподробнее рассказать?

Было бы лучше просто вести учет того, чего вы достигли в foreach, хотя это проще и более адаптируемо.

Просто добавьте свой собственный индекс.Пусть все будет просто.

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

Вот как я это делаю, что приятно своей простотой / краткостью, но если вы много делаете в теле цикла obj.Value, это будет устаревать довольно быстро.

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

Почему foreach ?!

Самый простой способ - это использование для вместо foreach если вы используете список .

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

ИЛИ, если вы хотите, используйте foreach :

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

вы можете использовать это для отображения индекса каждого цикла :

myList.indexOf(m)

Лучше использовать ключевое слово continue безопасная конструкция, подобная этой

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

Если коллекция представляет собой список, вы можете использовать List .indexOf , как в:

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

Ведущий ответ гласит:

"Очевидно, что концепция индекса чужда концепции перечисления и не может быть реализована".

Хотя это верно для текущей версии C #, это не является концептуальным ограничением.

Создание MS новой функции языка C # могло бы решить эту проблему, наряду с поддержкой нового интерфейса 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);
}

Если foreach передается IEnumerable и не может разрешить IIndexedEnumerable, но запрашивается с индексом var, то компилятор C # может обернуть исходный код объектом IndexedEnumerable, который добавляет в код для отслеживания индекса.

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

Почему:

  • Foreach выглядит приятнее, и в бизнес-приложениях редко является узким местом производительности
  • Foreach может быть более эффективным с точки зрения использования памяти.Наличие конвейера функций вместо преобразования в новые коллекции на каждом шаге.Какая разница, использует ли он еще несколько циклов процессора, если меньше сбоев кэша процессора и меньше GC.Собирает
  • Требование к программисту добавить код отслеживания индекса портит красоту
  • Это довольно легко реализовать (спасибо MS) и имеет обратную совместимость

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

Моим решением этой проблемы является метод расширения WithIndex(),

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

Используйте это как

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

Для интереса Фил Хаак только что написал пример этого в контексте делегата с шаблоном Razor (http://haacked.com/archive/2011/04/14/a-better-razor-foreach-loop.aspx)

Фактически он пишет метод расширения, который обертывает итерацию в классе "IteratedItem" (см. Ниже), предоставляя доступ как к индексу, так и к элементу во время итерации.

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

Однако, хотя это было бы нормально в среде, отличной от Razor, если вы выполняете одну операцию (т. Е.тот, который мог бы быть предоставлен как лямбда) это не будет надежной заменой синтаксиса for /foreach в контекстах, отличных от Razor.

Я не думаю, что это должно быть достаточно эффективно, но это работает:

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

Я встроил это в 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.

Вы также могли бы просто использовать string.join:

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

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

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

После добавления следующей структуры и метода расширения.

Метод struct и extension инкапсулирует перечисляемое.Выберите функциональность.

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

Я не верю, что есть способ получить значение текущей итерации цикла foreach.Считать себя, по-видимому, лучший способ.

Могу я спросить, почему вы хотели бы это знать?

Похоже, что вы, скорее всего, занимались бы одной из трех вещей:

1) Получение объекта из коллекции, но в данном случае он у вас уже есть.

2) Подсчет объектов для последующей последующей обработки...коллекции имеют свойство Count, которое вы могли бы использовать.

3) Установка свойства для объекта на основе его порядка в цикле ... хотя вы могли бы легко установить это, когда добавляли объект в коллекцию.

Если ваша коллекция не может вернуть индекс объекта с помощью какого-либо метода, единственный способ - использовать счетчик, как в вашем примере.

Однако при работе с индексами единственным разумным решением проблемы является использование цикла for.Все остальное усложняет код, не говоря уже о сложности во времени и пространстве.

Как насчет чего-нибудь подобного?Обратите внимание, что myDelimitedString может быть нулевым, если myEnumerable пуст.

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

У меня только что была эта проблема, но обдумывание проблемы в моем случае дало наилучшее решение, не связанное с ожидаемым решением.

Это может быть довольно распространенный случай, в принципе, я читаю из одного исходного списка и создаю объекты на их основе в списке назначения, однако сначала я должен проверить, действительны ли исходные элементы, и хочу вернуть строку с любой ошибкой.На первый взгляд, я хочу получить индекс в перечислителе объекта по текущему свойству, однако, поскольку я копирую эти элементы, я неявно знаю текущий индекс в любом случае из текущего пункта назначения.Очевидно, что это зависит от вашего целевого объекта, но для меня это был список, и, скорее всего, в нем будет реализована ICollection.

т. е.

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

Не всегда применимо, но, я думаю, достаточно часто, чтобы о нем стоило упомянуть.

В любом случае, дело в том, что иногда в вашей логике уже есть неочевидное решение...

Я не был уверен, что вы пытаетесь сделать с индексной информацией, основанной на этом вопросе.Однако в C # вы обычно можете адаптировать IEnumerable .Выберите метод, чтобы получить индекс из того, что вы хотите.Например, я мог бы использовать что-то вроде этого для определения того, является ли значение четным или нечетным.

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

Это дало бы вам словарь по имени, показывающий, был ли элемент нечетным (1) или четным (0) в списке.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top