Як отримати індекс елемента в 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підходу полягає в тому, що: він продовжує шукати, і b: він повертає останній індекс, коли є дублікати (люди зазвичай очікують першого індексу)
Marc Gravell

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
Cleary, C # пропускає поняття IIndexableEnumerable. це було б просто еквівалентом "випадково доступної" концепції в термінології С ++ STL.
v.oddou

14
розширення з перевантаженнями типу Select ((x, i) => ...), мабуть, означають, що ці індекси повинні існувати
Michael

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 це, мабуть, погана ідея в першу чергу.
Joel Coehoorn

Рішення явного циклу також працює в 2 рази швидше (в гіршому випадку), ніж рішення Select (). Max ().
Стів Ґуіді

1
Ви можете просто рахувати елементи за лямбда без TakeWhile - це зберігає один цикл: source.Count (x => x! = Значення);
Камарей

10
@Kamarey - ні, це робить щось інше. TakeWhile зупиняється, коли отримує збій; Count (предикат) повертає ті, що відповідають. тобто, якщо перший був промахом, а все інше було правдою, TakeWhile (pred) .Count () повідомить про 0; Граф (pred) повідомить про 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;
    }
}

1
Це насправді дуже мило, +1! Він передбачає додаткові об'єкти, але вони повинні бути відносно дешевими (GEN0), тому не є великою проблемою. ==Може знадобитися працювати?
Marc Gravell

1
Додано перевантаження IEqualityComparer, у справжньому стилі LINQ. ;)
дальбик

1
Я думаю, ти маєш на увазі сказати ... порівняння.Еквал. (Xa, значення) =)
Марк

Оскільки вираз Select повертає комбінований результат, який потім обробляється, я б уявив, що явно використання типу значення KeyValuePair дозволить вам уникнути будь-яких виділень купи, доки .NET impl виділяє типи значень на стек і будь-яка машина машини, яку може генерувати LINQ, використовує поле для результату Select'd, який не оголошується голим Об'єктом (таким чином, результат KVP стає полем). Звичайно, вам доведеться переробити знайдений == null умова (оскільки знайдене тепер буде значенням KVP). Можливо, використовуючи DefaultIfEmpty () або KVP<T, int?>(індекс, що
зводить нанівець

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

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

Сподіваємось, це комусь допомагає.


1
Може бути , я що - то НЕ вистачає, але чому GetEnumeratorі , MoveNextа не просто foreach?
Джош Галлахер

1
Коротка відповідь? Ефективність. Довга відповідь: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv

2
Якщо дивитися на ІЛ, виявляється, що різниця в продуктивності полягає в тому, що аліментатор foreachбуде викликати Disposeперелік, якщо він реалізується IDisposable. (Див. Stackoverflow.com/questions/4982396/… ) Оскільки код у цій відповіді не знає, чи є результат виклику GetEnumeratorодноразовим чи не одноразовим, він повинен робити те саме. На той момент мені все ще незрозуміло, що є користь для парфуму, хоча був якийсь додатковий ІЛ, мета якого не вискочила на мене!
Джош Галлахер

@JoshGallagher Я трохи досліджував питання щодо переваг парфу між foreach та for (i), і головна перевага використання для (i) полягала в тому, що він ByRefs об'єктом на місці, а не заново створювати його / передавати його назад ByVal. Я б припускав, що те саме стосується MoveNext та foreach, але я не впевнений у цьому. Можливо, вони обидва використовують ByVal ...
MaxOvrdrv

2
Читаючи цей блог ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ), можливо, «цикл ітератора», на який він посилається, є foreachциклом, у цьому випадку для конкретного випадку Tбудучи типом значення, це може бути збереження операції box / unbox, використовуючи цикл while. Однак це не підтверджується ІЛ, який я отримав з версії вашої відповіді 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);
}

Іншим способом зробити це може бути: загальнодоступний статичний int IndexOf <T> (цей список IENumerable <T>, T-елемент) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( елемент, x)? x + 1: -1). FirstOrDefault (x => x> 0); повернення (e == 0)? -1: е - 1); }
Ану Томас Чанді

"не створює зайвих об'єктів". Фактично Linq створюватиме об’єкти на задньому плані, тому це не зовсім коректно. Як source.Whereі source.DefaultIfEmptyбуде, наприклад , створити IEnumerableкожен.
Мартін Оделій

1

Сьогодні натрапив на це в пошуках відповідей, і я подумав, що додаю свою версію до списку (каламбур не призначений). Це utlises нульовий умовний оператор 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();
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.