Как получить индекс элемента в IEnumerable?

144

Я написал это:

public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> obj, T value)
    {
        return obj
            .Select((a, i) => (a.Equals(value)) ? i : -1)
            .Max();
    }

    public static int IndexOf<T>(this IEnumerable<T> obj, T value
           , IEqualityComparer<T> comparer)
    {
        return obj
            .Select((a, i) => (comparer.Equals(a, value)) ? i : -1)
            .Max();
    }
}

Но я не знаю, существует ли он уже, не так ли?

Джадер Диас
источник
4
Проблема с Maxподходом состоит в том, что a: он продолжает искать, а b: он возвращает последний индекс, когда есть дубликаты (люди обычно ожидают первый индекс)
Марк Гравелл
1
geekswithblogs.net сравнивает 4 решения и их производительность. В ToList()/FindIndex()трюк работает лучше
nixda

Ответы:

51

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

Скотт Дорман
источник
8
В настоящее время я наткнулся на этот поток, потому что я реализую универсальную оболочку IList <> для типа IEnumerable <>, чтобы использовать мои объекты IEnumerable <> со сторонними компонентами, которые поддерживают только источники данных типа IList. Я согласен с тем, что попытка получить индекс элемента в объекте IEnumerable, вероятно, в большинстве случаев является признаком того, что что-то было сделано неправильно, иногда бывают случаи, когда такой индекс встречается, когда бьет воспроизведение большой коллекции в памяти только ради поиска индекса. одного элемента, когда у вас уже есть IEnumerable.
jpierson
215
-1 причина: есть законные причины, по которым вы хотите получить индекс из IEnumerable<>. Я не покупаю всю догму "ты должен делать это".
Джон Алексиу
78
Согласен с @ ja72; если бы вам не приходилось иметь дело с индексами, IEnumerableто Enumerable.ElementAtне существовало бы. IndexOfявляется просто обратным - любой аргумент против него должен применяться в равной степени к ElementAt.
Кирк Волл
7
Клири, C # пропускает концепцию IIndexableEnumerable. это было бы просто эквивалентом понятия «случайный доступ» в терминологии C ++ STL.
v.oddou
14
расширения с перегрузками вроде Select ((x, i) => ...), по-видимому, подразумевают, что эти индексы должны существовать
Майкл
126

Я бы поставил под сомнение мудрость, но, возможно,

source.TakeWhile(x => x != value).Count();

(используется EqualityComparer<T>.Defaultдля эмуляции, !=если необходимо) - но вам нужно смотреть, чтобы вернуть -1, если не найден ... так что, возможно, просто сделать это долгий путь

public static int IndexOf<T>(this IEnumerable<T> source, T value)
{
    int index = 0;
    var comparer = EqualityComparer<T>.Default; // or pass in as a parameter
    foreach (T item in source)
    {
        if (comparer.Equals(item, value)) return index;
        index++;
    }
    return -1;
}
Марк Гравелл
источник
8
+1 за "сомнение мудрости". 9 раз из 10 это, наверное, плохая идея.
Джоэл Коухорн
Решение с явным циклом также выполняется в 2 раза быстрее (в худшем случае), чем решение Select (). Max ().
Стив Гуиди
1
Вы можете просто считать элементы по лямбде без TakeWhile - это сохраняет один цикл: source.Count (x => x! = Value);
Камарей
10
@ Камарей - нет, это делает что-то другое. TakeWhile останавливается, когда происходит сбой; Count (предикат) возвращает те, которые совпадают. т. е. если первое было пропущено, а все остальное было верно, TakeWhile (pred) .Count () сообщит 0; Граф (пред) сообщит n-1.
Марк Гравелл
1
TakeWhileумный! Имейте в виду, что это возвращает, Countесли элемент не существует, что является отклонением от стандартного поведения.
Nawfal
27

Я бы реализовал это так:

public static class EnumerableExtensions
{
    public static int IndexOf<T>(this IEnumerable<T> obj, T value)
    {
        return obj.IndexOf(value, null);
    }

    public static int IndexOf<T>(this IEnumerable<T> obj, T value, IEqualityComparer<T> comparer)
    {
        comparer = comparer ?? EqualityComparer<T>.Default;
        var found = obj
            .Select((a, i) => new { a, i })
            .FirstOrDefault(x => comparer.Equals(x.a, value));
        return found == null ? -1 : found.i;
    }
}
dahlbyk
источник
1
Это на самом деле очень мило, +1! Это включает в себя дополнительные объекты, но они должны быть относительно дешевыми (GEN0), так что это не большая проблема. ==Может понадобиться работать?
Марк Гравелл
1
Добавлена ​​перегрузка IEqualityComparer в истинном стиле LINQ. ;)
dahlbyk
1
Я думаю, что вы хотите сказать ... Comparer.Equals (XA, значение) =)
Марк
Поскольку выражение Select возвращает комбинированный результат, который затем обрабатывается, я бы предположил, что использование значения типа KeyValuePair в явном виде позволит вам избежать любого вида выделения кучи, если .NET impl выделяет типы значений в стеке и любой конечный автомат, который может генерировать LINQ, использует поле для результата Select'd, который не объявлен как голый объект (таким образом, результат KVP будет упакован). Конечно, вам придется переделать условие found == null (так как found теперь будет значением KVP). Возможно, используя DefaultIfEmpty () или KVP<T, int?>(обнуляемый индекс)
kornman00
1
Хорошая реализация, хотя одну вещь, которую я бы предложил добавить, это проверить, реализует ли obj IList<T>и, если да, отложить его метод IndexOf на тот случай, если он имеет оптимизацию для конкретного типа.
Джош
16

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

 var index = haystack.ToList().IndexOf(needle);

Это немного неуклюже, но это делает работу и довольно кратко.

Марк Уоттс
источник
6
Хотя это подойдет для небольших коллекций, предположим, у вас есть миллион предметов в «стоге сена». Выполнение ToList () для этого будет перебирать все миллион элементов и добавлять их в список. Затем он будет искать в списке, чтобы найти индекс соответствующего элемента. Это было бы крайне неэффективно, а также возможность создания исключения, если список становится слишком большим.
Esteuart
3
@esteuart Определенно - вам нужно выбрать подход, который подходит вашему варианту использования. Я сомневаюсь, что есть одно решение, подходящее для всех, поэтому, возможно, нет реализации в основных библиотеках.
Марк Уоттс
8

Лучший способ поймать позицию - FindIndexэта функция доступна только дляList<>

пример

int id = listMyObject.FindIndex(x => x.Id == 15); 

Если у вас есть перечислитель или массив, используйте этот способ

int id = myEnumerator.ToList().FindIndex(x => x.Id == 15); 

или

 int id = myArray.ToList().FindIndex(x => x.Id == 15); 
daniele3004
источник
7

Я думаю, что лучший вариант - это реализовать так:

public static int IndexOf<T>(this IEnumerable<T> enumerable, T element, IEqualityComparer<T> comparer = null)
{
    int i = 0;
    comparer = comparer ?? EqualityComparer<T>.Default;
    foreach (var currentElement in enumerable)
    {
        if (comparer.Equals(currentElement, element))
        {
            return i;
        }

        i++;
    }

    return -1;
}

Это также не будет создавать анонимный объект

Аксенте Адриан
источник
5

Я немного опоздал в игре, я знаю ... но это то, что я недавно сделал. Он немного отличается от вашего, но позволяет программисту определять, какой должна быть операция равенства (предикат). Который я считаю очень полезным при работе с различными типами, так как тогда у меня есть общий способ сделать это независимо от типа объекта и <T>встроенного оператора равенства.

Он также имеет очень маленький объем памяти и очень, очень быстрый / эффективный ... если вы заботитесь об этом.

В худшем случае вы просто добавите это в свой список расширений.

Во всяком случае ... вот оно.

 public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
 {
     int retval = -1;
     var enumerator = source.GetEnumerator();

     while (enumerator.MoveNext())
     {
         retval += 1;
         if (predicate(enumerator.Current))
         {
             IDisposable disposable = enumerator as System.IDisposable;
             if (disposable != null) disposable.Dispose();
             return retval;
         }
     }
     IDisposable disposable = enumerator as System.IDisposable;
     if (disposable != null) disposable.Dispose();
     return -1;
 }

Надеюсь, это кому-нибудь поможет.

MaxOvrdrv
источник
1
Может быть , я что - то не хватает, но почему GetEnumeratorи , MoveNextа не просто foreach?
Джош Галлахер
1
Короткий ответ? Эффективность. Длинный ответ: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv
2
Глядя на IL, кажется, что разница в производительности заключается в том, что a foreachвызовет Disposeперечислитель, если он реализует IDisposable. (См. Stackoverflow.com/questions/4982396/… ) Поскольку код в этом ответе не знает, является ли результат вызова GetEnumeratorодноразовым или нет, он должен сделать то же самое. В тот момент мне все еще неясно, есть ли в этом перфекционная выгода, хотя был какой-то дополнительный ИЛ, цель которого не прыгнула на меня!
Джош Галлахер
@JoshGallagher Некоторое время назад я провел небольшое исследование относительно преимуществ perf между foreach и for (i), и основное преимущество использования for (i) заключалось в том, что он ByRefs объект на месте, а не воссоздает его / передает его назад ByVal. Я бы предположил, что то же самое относится к MoveNext по сравнению с foreach, но я не уверен в этом. Возможно они оба используют ByVal ...
MaxOvrdrv
2
Читая этот блог ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ), может случиться так, что «цикл итератора», на который он ссылается, является foreachциклом, и в этом случае для конкретного случая Tбудучи типом значения, он может сохранять операции box / unbox с помощью цикла while. Однако это не подтверждается IL, который я получил по версии вашего ответа foreach. Я все же считаю, что условное удаление итератора важно. Не могли бы вы изменить ответ, чтобы включить это?
Джош Галлахер
5

Несколько лет спустя, но он использует Linq, возвращает -1, если не найден, не создает дополнительных объектов и должен закорачиваться при обнаружении [в отличие от итерации по всему IEnumerable]:

public static int IndexOf<T>(this IEnumerable<T> list, T item)
{
    return list.Select((x, index) => EqualityComparer<T>.Default.Equals(item, x)
                                     ? index
                                     : -1)
               .FirstOr(x => x != -1, -1);
}

Где «FirstOr» это:

public static T FirstOr<T>(this IEnumerable<T> source, T alternate)
{
    return source.DefaultIfEmpty(alternate)
                 .First();
}

public static T FirstOr<T>(this IEnumerable<T> source, Func<T, bool> predicate, T alternate)
{
    return source.Where(predicate)
                 .FirstOr(alternate);
}
Greg
источник
Другой способ сделать это может быть: public static int IndexOf <T> (этот список IEnumerable <T>, элемент T) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( item, x)? x + 1: -1) .irstOrDefault (x => x> 0); возврат (е == 0)? -1: е - 1); }
Ану Томас Чанди
msgstr "не создает лишних объектов". Linq фактически создает объекты в фоновом режиме, поэтому это не совсем правильно. И то source.Whereи другое source.DefaultIfEmpty, например, создаст IEnumerableдруг.
Мартин Одхелиус
1

Наткнулся сегодня на поиск ответов, и я подумал, что добавлю свою версию в список (без каламбура). Он содержит нулевой условный оператор c # 6.0

IEnumerable<Item> collection = GetTheCollection();

var index = collection
.Select((item,idx) => new { Item = item, Index = idx })
//or .FirstOrDefault(_ =>  _.Item.Prop == something)
.FirstOrDefault(_ => _.Item == itemToFind)?.Index ?? -1;

Я провел несколько «гонок на старых лошадях» (тестирование) и для больших коллекций (~ 100 000), в худшем случае, в конце концов, предмет, который вы хотите получить, это в 2 раза быстрее, чем вы ToList().FindIndex(). Если нужный предмет находится посередине, он примерно в 4 раза быстрее.

Для небольших коллекций (~ 10000) это кажется лишь незначительно быстрее

Вот как я это проверял https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e

Дейв
источник
1

Используя ответ @Marc Gravell, я нашел способ использовать следующий метод:

source.TakeWhile(x => x != value).Count();

чтобы получить -1, когда предмет не может быть найден:

internal static class Utils
{

    public static int IndexOf<T>(this IEnumerable<T> enumerable, T item) => enumerable.IndexOf(item, EqualityComparer<T>.Default);

    public static int IndexOf<T>(this IEnumerable<T> enumerable, T item, EqualityComparer<T> comparer)
    {
        int index = enumerable.TakeWhile(x => comparer.Equals(x, item)).Count();
        return index == enumerable.Count() ? -1 : index;
    }
}

Я думаю, что этот путь может быть как самым быстрым, так и простым. Тем не менее, я еще не тестировал выступления.

Давиде Канниццо
источник
0

Альтернативой нахождению индекса после факта является обертка Enumerable, чем-то похожая на использование метода Linq GroupBy ().

public static class IndexedEnumerable
{
    public static IndexedEnumerable<T> ToIndexed<T>(this IEnumerable<T> items)
    {
        return IndexedEnumerable<T>.Create(items);
    }
}

public class IndexedEnumerable<T> : IEnumerable<IndexedEnumerable<T>.IndexedItem>
{
    private readonly IEnumerable<IndexedItem> _items;

    public IndexedEnumerable(IEnumerable<IndexedItem> items)
    {
        _items = items;
    }

    public class IndexedItem
    {
        public IndexedItem(int index, T value)
        {
            Index = index;
            Value = value;
        }

        public T Value { get; private set; }
        public int Index { get; private set; }
    }

    public static IndexedEnumerable<T> Create(IEnumerable<T> items)
    {
        return new IndexedEnumerable<T>(items.Select((item, index) => new IndexedItem(index, item)));
    }

    public IEnumerator<IndexedItem> GetEnumerator()
    {
        return _items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Что дает вариант использования:

var items = new[] {1, 2, 3};
var indexedItems = items.ToIndexed();
foreach (var item in indexedItems)
{
    Console.WriteLine("items[{0}] = {1}", item.Index, item.Value);
}
Йошка
источник
отличная базовая линия. Также полезно добавить членов IsEven, IsOdd, IsFirst и IsLast.
JJS
0

Это может быть действительно круто с расширением (функционирующим как прокси), например:

collection.SelectWithIndex(); 
// vs. 
collection.Select((item, index) => item);

Который автоматически назначит индексы коллекции, доступной через это Indexсвойство.

Интерфейс:

public interface IIndexable
{
    int Index { get; set; }
}

Пользовательское расширение (вероятно, наиболее полезное для работы с EF и DbContext):

public static class EnumerableXtensions
{
    public static IEnumerable<TModel> SelectWithIndex<TModel>(
        this IEnumerable<TModel> collection) where TModel : class, IIndexable
    {
        return collection.Select((item, index) =>
        {
            item.Index = index;
            return item;
        });
    }
}

public class SomeModelDTO : IIndexable
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public int Index { get; set; }
}

// In a method
var items = from a in db.SomeTable
            where a.Id == someValue
            select new SomeModelDTO
            {
                Id = a.Id,
                Name = a.Name,
                Price = a.Price
            };

return items.SelectWithIndex()
            .OrderBy(m => m.Name)
            .Skip(pageStart)
            .Take(pageSize)
            .ToList();
Йом С.
источник