Wie erhalte ich den Index eines Elements in einer IEnumerable?


144

Ich habe das geschrieben:

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

Aber ich weiß nicht, ob es das schon gibt, oder?


4
Das Problem bei einem MaxAnsatz ist, dass a: es weiter sucht und b: es den letzten Index zurückgibt, wenn es Duplikate gibt (Leute erwarten normalerweise den ersten Index)
Marc Gravell

1
geekswithblogs.net vergleicht 4 Lösungen und ihre Leistung. Der ToList()/FindIndex()Trick
funktioniert

Antworten:


51

Der springende Punkt beim Herausgeben von Dingen als IEnumerable ist, dass Sie den Inhalt träge durchlaufen können. Als solches ist es nicht wirklich ein Konzept eines Index. Was Sie tun, macht für eine IEnumerable wirklich keinen Sinn. Wenn Sie etwas benötigen, das den Zugriff per Index unterstützt, fügen Sie es in eine aktuelle Liste oder Sammlung ein.


8
Derzeit bin ich auf diesen Thread gestoßen, weil ich einen generischen IList <> -Wrapper für den Typ IEnumerable <> implementiere, um meine IEnumerable <> -Objekte mit Komponenten von Drittanbietern zu verwenden, die nur Datenquellen vom Typ IList unterstützen. Ich bin damit einverstanden, dass der Versuch, einen Index eines Elements innerhalb eines IEnumerable-Objekts abzurufen, in den meisten Fällen wahrscheinlich ein Zeichen dafür ist, dass etwas falsch gemacht wurde. Es gibt Zeiten, in denen das Auffinden eines solchen Index einmal die Reproduktion einer großen Sammlung im Speicher übertrifft, nur um den Index zu finden eines einzelnen Elements, wenn Sie bereits eine IEnumerable haben.
Jpierson

214
-1 Ursache: Es gibt legitime Gründe, warum Sie einen Index aus einem erhalten möchten IEnumerable<>. Ich kaufe nicht das ganze Dogma "Du solltest das tun".
John Alexiou

78
Stimme zu @ ja72; wenn Sie nicht mit mit Indizes zu tun haben sollten , IEnumerabledann Enumerable.ElementAtwürde nicht existieren. IndexOfist einfach das Gegenteil - jedes Argument dagegen muss gleichermaßen gelten ElementAt.
Kirk Woll

7
Klar, C # vermisst das Konzept von IIndexableEnumerable. Das wäre nur das Äquivalent eines "zufällig zugänglichen" Konzepts in der C ++ STL-Terminologie.
v.oddou

14
Erweiterungen mit Überladungen wie Select ((x, i) => ...) scheinen zu implizieren, dass diese Indizes existieren sollten
Michael

126

Ich würde die Weisheit in Frage stellen, aber vielleicht:

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

(Verwenden EqualityComparer<T>.Default, um !=bei Bedarf zu emulieren ) - aber Sie müssen darauf achten, -1 zurückzugeben, wenn es nicht gefunden wird ... also machen Sie es vielleicht einfach den langen Weg

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 für "die Weisheit hinterfragen". 9 mal von 10 ist es wahrscheinlich in erster Linie eine schlechte Idee.
Joel Coehoorn

Die explizite Schleifenlösung läuft auch 2x schneller (im schlimmsten Fall) als die Select (). Max () -Lösung.
Steve Guidi

1
Sie können Elemente einfach ohne TakeWhile nach Lambda zählen - es wird eine Schleife gespeichert: source.Count (x => x! = Value);
Kamarey

10
@Kamarey - nein, das macht etwas anderes. TakeWhile stoppt, wenn ein Fehler auftritt . Count (Prädikat) gibt diejenigen zurück, die übereinstimmen. dh wenn der erste ein Fehler war und alles andere wahr war, meldet TakeWhile (pred) .Count () 0; Count (pred) meldet n-1.
Marc Gravell

1
TakeWhileist schlau! Beachten Sie jedoch, dass dies zurückgegeben wird, Countwenn kein Element vorhanden ist, was eine Abweichung vom Standardverhalten darstellt.
Nawfal

27

Ich würde es so implementieren:

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
Das ist eigentlich sehr süß, +1! Es handelt sich um zusätzliche Objekte, die jedoch relativ billig sein sollten (GEN0), also kein großes Problem. Das ==könnte Arbeit brauchen?
Marc Gravell

1
IEqualityComparer-Überladung im echten LINQ-Stil hinzugefügt. ;)
Dahlbyk

1
Ich denke du willst sagen ... compareer.Equals (xa, value) =)
Marc

Da der Select-Ausdruck das kombinierte Ergebnis zurückgibt, das dann verarbeitet wird, kann ich mir vorstellen, dass Sie durch explizite Verwendung des KeyValuePair-Werttyps jegliche Art von Heap-Zuweisungen vermeiden können, solange das .NET-Impl Wertetypen auf dem Stack und zuordnet Jede Zustandsmaschine, die LINQ generieren kann, verwendet ein Feld für das Select'd-Ergebnis, das nicht als nacktes Objekt deklariert ist (wodurch das KVP-Ergebnis eingerahmt wird). Natürlich müssten Sie die Bedingung found == null überarbeiten (da found jetzt ein KVP-Wert wäre). Vielleicht mit DefaultIfEmpty () oder KVP<T, int?>(nullable index)
kornman00

1
Gute Implementierung, obwohl ich vorschlagen würde, eine Überprüfung durchzuführen IList<T>, um festzustellen, ob obj implementiert ist, und wenn ja, die IndexOf-Methode zu verwenden, nur für den Fall, dass eine typspezifische Optimierung vorliegt.
Josh

16

Die Art und Weise, wie ich das derzeit mache, ist etwas kürzer als die bereits vorgeschlagenen und ergibt, soweit ich das beurteilen kann, das gewünschte Ergebnis:

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

Es ist ein bisschen klobig, aber es macht den Job und ist ziemlich prägnant.


6
Obwohl dies für kleine Sammlungen funktionieren würde, nehmen wir an, Sie haben eine Million Gegenstände im "Heuhaufen". Wenn Sie eine ToList () ausführen, werden alle eine Million Elemente durchlaufen und einer Liste hinzugefügt. Anschließend wird die Liste durchsucht, um den Index des passenden Elements zu finden. Dies wäre äußerst ineffizient und bietet die Möglichkeit, eine Ausnahme auszulösen, wenn die Liste zu groß wird.
Esteuart

3
@esteuart Auf jeden Fall - Sie müssen einen Ansatz wählen, der zu Ihrem Anwendungsfall passt. Ich bezweifle, dass es eine Einheitslösung gibt, weshalb es möglicherweise keine Implementierung in den Kernbibliotheken gibt.
Mark Watts

8

Der beste Weg, um die Position zu erfassen, ist: FindIndexDiese Funktion ist nur für verfügbarList<>

Beispiel

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

Wenn Sie einen Enumerator oder ein Array haben, verwenden Sie diese Methode

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

oder

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

7

Ich denke, die beste Option ist, Folgendes zu implementieren:

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

Das anonyme Objekt wird ebenfalls nicht erstellt


5

Ein bisschen spät im Spiel, ich weiß ... aber das habe ich kürzlich getan. Es unterscheidet sich geringfügig von Ihrem, ermöglicht es dem Programmierer jedoch, zu bestimmen, wie die Gleichstellungsoperation aussehen soll (Prädikat). Was ich sehr nützlich finde, wenn ich mich mit verschiedenen Typen befasse, da ich dann eine generische Methode habe, dies unabhängig vom Objekttyp und dem <T>eingebauten Gleichheitsoperator zu tun .

Es hat auch einen sehr sehr kleinen Speicherbedarf und ist sehr, sehr schnell / effizient ... wenn Sie sich dafür interessieren.

Schlimmer noch, Sie fügen dies einfach Ihrer Liste der Erweiterungen hinzu.

Wie auch immer ... hier ist es.

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

Hoffentlich hilft das jemandem.


1
Vielleicht fehlt mir etwas, aber warum das GetEnumeratorund MoveNextnicht nur ein foreach?
Josh Gallagher

1
Kurze Antwort? Effizienz. Lange Antwort: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv

2
Mit Blick auf die IL scheint es , dass der Leistungsunterschied ist , dass ein foreachAnruf wird Disposeauf dem enumerator wenn es implementiert IDisposable. (Siehe stackoverflow.com/questions/4982396/… ) Da der Code in dieser Antwort nicht weiß, ob das Ergebnis des Aufrufs GetEnumeratorverfügbar ist oder nicht, sollte er dasselbe tun. Zu diesem Zeitpunkt bin ich mir noch nicht sicher, ob es einen Perf-Vorteil gibt, obwohl es eine zusätzliche IL gab, deren Zweck mich nicht angesprochen hat!
Josh Gallagher

@JoshGallagher Ich habe vor einiger Zeit ein wenig über die Perf-Vorteile zwischen foreach und for (i) recherchiert. Der Hauptvorteil der Verwendung von for (i) bestand darin, dass das Objekt an Ort und Stelle ByRefs erstellt wurde, anstatt es neu zu erstellen / weiterzugeben zurück ByVal. Ich würde annehmen, dass das Gleiche für MoveNext im Vergleich zu foreach gilt, aber ich bin mir nicht sicher. Vielleicht benutzen beide ByVal ...
MaxOvrdrv

2
Beim Lesen dieses Blogs ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ) kann es sein, dass die "Iteratorschleife", auf die er sich bezieht, eine foreachSchleife ist, in welchem ​​Fall für den speziellen Fall von TAls Wertetyp wird möglicherweise eine Box- / Unbox-Operation mithilfe der while-Schleife gespeichert. Dies wird jedoch nicht durch die IL bestätigt, die ich aus einer Version Ihrer Antwort mit erhalten habe foreach. Ich denke jedoch immer noch, dass die bedingte Entsorgung des Iterators wichtig ist. Könnten Sie die Antwort so ändern, dass sie diese enthält?
Josh Gallagher

5

Ein paar Jahre später, aber dies verwendet Linq, gibt -1 zurück, wenn es nicht gefunden wird, erstellt keine zusätzlichen Objekte und sollte kurzschließen, wenn es gefunden wird [im Gegensatz zum Iterieren über die gesamte 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);
}

Wo 'FirstOr' ist:

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

Eine andere Möglichkeit ist: public static int IndexOf <T> (diese IEnumerable <T> -Liste, T-Element) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( item, x)? x + 1: -1) .FirstOrDefault (x => x> 0); return (e == 0)? -1: e - 1); }
Anu Thomas Chandy

"erstellt keine zusätzlichen Objekte". Linq erstellt tatsächlich Objekte im Hintergrund, sodass dies nicht vollständig korrekt ist. Beides source.Whereund source.DefaultIfEmptywird zum Beispiel jeweils ein erstellen IEnumerable.
Martin Odhelius

1

Ich bin heute auf der Suche nach Antworten darüber gestolpert und dachte, ich würde meine Version zur Liste hinzufügen (kein Wortspiel beabsichtigt). Es verwendet den bedingten Nulloperator von 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;

Ich habe einige "Rennen der alten Pferde" (Tests) durchgeführt und für große Sammlungen (~ 100.000) ist das schlimmste Szenario, das der gewünschte Gegenstand am Ende hat, 2x schneller als dies der Fall ist ToList().FindIndex(). Wenn sich der gewünschte Gegenstand in der Mitte befindet, ist er ~ 4x schneller.

Für kleinere Sammlungen (~ 10.000) scheint es nur unwesentlich schneller zu sein

Hier ist, wie ich es getestet habe https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e


1

Mit der Antwort von @Marc Gravell habe ich einen Weg gefunden, die folgende Methode zu verwenden:

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

um -1 zu erhalten, wenn der Gegenstand nicht gefunden werden kann:

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

Ich denke, dieser Weg könnte sowohl der schnellste als auch der einfachere sein. Allerdings habe ich die Leistungen noch nicht getestet.


0

Eine Alternative zum nachträglichen Suchen des Index besteht darin, die Aufzählung zu umbrechen, ähnlich wie bei der Verwendung der Linq GroupBy () -Methode.

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

Welches gibt einen Anwendungsfall von:

var items = new[] {1, 2, 3};
var indexedItems = items.ToIndexed();
foreach (var item in indexedItems)
{
    Console.WriteLine("items[{0}] = {1}", item.Index, item.Value);
}

tolle Basislinie. Es ist hilfreich, auch die Mitglieder IsEven, IsOdd, IsFirst und IsLast hinzuzufügen.
JJS

0

Dies kann mit einer Erweiterung (die als Proxy fungiert) sehr cool werden, zum Beispiel:

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

Dadurch werden der Sammlung, auf die über diese IndexEigenschaft zugegriffen werden kann, automatisch Indizes zugewiesen .

Schnittstelle:

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

Benutzerdefinierte Erweiterung (wahrscheinlich am nützlichsten für die Arbeit mit EF und 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();
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.