Come ottenere l'indice di un elemento in un IEnumerable?


144

Ho scritto questo:

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

Ma non so se esiste già, vero?


4
Il problema con un Maxapproccio è che a: continua a cercare eb: restituisce l' ultimo indice quando ci sono duplicati (la gente di solito si aspetta il primo indice)
Marc Gravell

1
geekswithblogs.net confronta 4 soluzioni e le loro prestazioni. Il ToList()/FindIndex()trucco funziona meglio
nixda

Risposte:


51

Il punto centrale di far emergere le cose come IEnumerable è così che tu possa pigramente iterare sui contenuti. In quanto tale, non è in realtà un concetto di un indice. Quello che stai facendo davvero non ha molto senso per un IEnumerable. Se hai bisogno di qualcosa che supporti l'accesso in base all'indice, inseriscilo in un elenco o una raccolta effettivi.


8
Attualmente mi sono imbattuto in questo thread perché sto implementando un wrapper IList <> generico per il tipo IEnumerable <> per utilizzare i miei oggetti IEnumerable <> con componenti di terze parti che supportano solo origini dati di tipo IList. Sono d'accordo che il tentativo di ottenere un indice di un elemento all'interno di un oggetto IEnumerable è probabilmente nella maggior parte dei casi un segno di qualcosa di sbagliato fatto ci sono volte in cui trovare tale indice una volta batte riprodurre una grande raccolta in memoria solo per il gusto di trovare l'indice di un singolo elemento quando hai già un IEnumerable.
jpierson,

215
-1 causa: vi sono motivi legittimi per cui si desidera ottenere un indice da a IEnumerable<>. Non compro l'intero dogma "lo faresti tu".
John Alexiou,

78
Accetto con @ ja72; se non dovessi avere a che fare con indici IEnumerableallora Enumerable.ElementAtnon esisterebbe. IndexOfè semplicemente l'inverso - qualsiasi argomento contro di esso deve essere applicato allo stesso modo ElementAt.
Kirk Woll,

7
Chiaramente, C # manca il concetto di IIndexableEnumerable. sarebbe solo l'equivalente di un concetto "accessibile in modo casuale", nella terminologia C ++ STL.
v

14
estensioni con sovraccarichi come Select ((x, i) => ...) sembrano implicare l'esistenza di questi indici
Michael

126

Metterei in dubbio la saggezza, ma forse:

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

(usando EqualityComparer<T>.Defaultper emulare !=se necessario) - ma devi guardare per restituire -1 se non trovato ... quindi forse fallo solo a lungo

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 per "mettere in discussione la saggezza". 9 volte su 10 è probabilmente una cattiva idea in primo luogo.
Joel Coehoorn,

La soluzione a ciclo esplicito funziona anche due volte più veloce (nel peggiore dei casi) rispetto alla soluzione Select (). Max ().
Steve Guidi,

1
Puoi semplicemente contare gli elementi di lambda senza TakeWhile: salva un ciclo: source.Count (x => x! = Value);
Kamarey,

10
@Kamarey - no, fa qualcosa di diverso. TakeWhile si interrompe quando si verifica un errore; Count (predicato) restituisce quelli corrispondenti. cioè se il primo è stato un fallimento e tutto il resto era vero, TakeWhile (pred) .Count () segnalerà 0; Count (pred) riporterà n-1.
Marc Gravell

1
TakeWhileè intelligente! Tenete presente però che questo ritorna Countse non esiste un elemento che è una deviazione dal comportamento standard.
nawfal,

27

Lo implementerei così:

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
In realtà è molto carino, +1! Implica oggetti extra, ma dovrebbero essere relativamente economici (GEN0), quindi non è un grosso problema. Il ==potrebbe aver bisogno di lavoro?
Marc Gravell

1
Aggiunto sovraccarico IEqualityComparer, in vero stile LINQ. ;)
dahlbyk,

1
Penso che intendi dire ... comparer.Equals (xa, value) =)
Marc

Poiché l'espressione Select restituisce il risultato combinato, che viene quindi elaborato, immagino che l'utilizzo esplicito del tipo di valore KeyValuePair ti consenta di evitare qualsiasi tipo di allocazione di heap, purché l'impianto .NET alloca tipi di valore nello stack e qualsiasi macchina a stati che LINQ può generare utilizza un campo per il risultato Select'd che non è dichiarato come oggetto nudo (causando così l'inscatolamento del risultato KVP). Ovviamente, dovresti rielaborare la condizione found == null (poiché found ora sarebbe un valore KVP). Forse usando DefaultIfEmpty () o KVP<T, int?>(indice nullable)
kornman00

1
Bella implementazione, anche se una cosa che suggerirei di aggiungere è un controllo per vedere se implementa obj IList<T>e, in tal caso, rimandare al suo metodo IndexOf nel caso in cui abbia un'ottimizzazione specifica per tipo.
Josh,

16

Il modo in cui lo sto facendo attualmente è un po 'più breve di quelli già suggeriti e, per quanto ne so, dà il risultato desiderato:

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

È un po 'goffo, ma fa il lavoro ed è abbastanza conciso.


6
Anche se questo funzionerebbe per le piccole collezioni, supponi di avere un milione di articoli nel "pagliaio". Fare una ToList () su che ripeterà tutti i milioni di elementi e li aggiungerà a un elenco. Quindi cercherà nell'elenco per trovare l'indice dell'elemento corrispondente. Ciò sarebbe estremamente inefficiente e la possibilità di generare un'eccezione se l'elenco diventa troppo grande.
esteuart,

3
@esteuart Sicuramente, devi scegliere un approccio adatto al tuo caso d'uso. Dubito che esista una soluzione unica per tutte le soluzioni, motivo per cui non esiste un'implementazione nelle librerie principali.
Mark Watts,

8

Il modo migliore per catturare la posizione è tramite FindIndexQuesta funzione è disponibile solo perList<>

Esempio

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

Se si dispone dell'enumeratore o dell'array, utilizzare in questo modo

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

o

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

7

Penso che l'opzione migliore sia implementare in questo modo:

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

Inoltre non creerà l'oggetto anonimo


5

Un po 'in ritardo nel gioco, lo so ... ma questo è quello che ho fatto di recente. È leggermente diverso dal tuo, ma consente al programmatore di dettare ciò che l'operazione di uguaglianza deve essere (predicato). Che trovo molto utile quando ho a che fare con diversi tipi, dal momento che ho un modo generico di farlo indipendentemente dal tipo di oggetto e<T> dall'operatore di uguaglianza incorporato.

Ha anche un ingombro di memoria molto piccolo ed è molto, molto veloce / efficiente ... se ti interessa.

Nel peggiore dei casi, lo aggiungerai semplicemente al tuo elenco di estensioni.

Comunque ... eccolo qui.

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

Spero che questo aiuti qualcuno.


1
Forse mi manca qualcosa, ma perché GetEnumeratore MoveNextpiuttosto che solo un foreach?
Josh Gallagher, l'

1
Risposta breve? Efficienza. Risposta lunga: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv

2
Guardando l'IL sembra che la differenza di prestazioni sia che a foreachchiamerà Disposel'enumeratore se implementa IDisposable. (Vedi stackoverflow.com/questions/4982396/… ) Dato che il codice in questa risposta non sa se il risultato della chiamata GetEnumeratorè o non è usa e getta dovrebbe fare lo stesso. A quel punto non sono ancora chiaro che ci sia un vantaggio perfetto, sebbene ci fosse qualche IL in più il cui scopo non è saltato fuori da me!
Josh Gallagher,

@JoshGallagher Qualche tempo fa ho fatto un po 'di ricerche sui benefici perf tra foreach e for (i), e il vantaggio principale dell'uso di for (i) è stato che ByRefs l'oggetto sul posto anziché ricrearlo / passarlo indietro ByVal. Suppongo che lo stesso valga per MoveNext contro foreach, ma non ne sono sicuro. Forse usano entrambi ByVal ...
MaxOvrdrv

2
Leggendo questo blog ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ) può darsi che il "loop iteratore" a cui si sta riferendo sia un foreachloop, nel qual caso per il caso particolare di Tessendo un tipo di valore, potrebbe salvare un'operazione box / unbox usando il ciclo while. Tuttavia, questo non è confermato dall'IL che ho ricevuto da una versione della tua risposta foreach. Penso comunque che lo smaltimento condizionato dell'iteratore sia importante. Potresti modificare la risposta per includerla?
Josh Gallagher,

5

Qualche anno dopo, ma utilizza Linq, restituisce -1 se non viene trovato, non crea oggetti extra e dovrebbe cortocircuitare quando viene trovato [invece di iterare sull'intero 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);
}

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

Un altro modo di farlo può essere: public static int IndexOf <T> (questo elenco IEnumerable <T>, elemento T) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( elemento, x)? x + 1: -1) .PrimaOrDefault (x => x> 0); ritorno (e == 0)? -1: e - 1); }
Anu Thomas Chandy,

"non crea oggetti extra". Linq creerà infatti oggetti in background, quindi questo non è completamente corretto. Entrambi source.Wheree source.DefaultIfEmptyper esempio creeranno uno IEnumerableciascuno.
Martin Odhelius,

1

Mi sono imbattuto in questo oggi in una ricerca di risposte e ho pensato di aggiungere la mia versione all'elenco (nessun gioco di parole previsto). Utilizza l'operatore condizionale null di 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;

Ho fatto un po 'di corse dei vecchi cavalli (test) e per grandi collezioni (~ 100.000), il peggior scenario in cui l'articolo che vuoi è alla fine, questo è 2 volte più veloce di quello che fai ToList().FindIndex(). Se l'oggetto che vuoi è nel mezzo è ~ 4x più veloce.

Per le raccolte più piccole (~ 10.000) sembra essere solo leggermente più veloce

Ecco come l'ho provato https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e


1

Usando la risposta di @Marc Gravell, ho trovato il modo di usare il seguente metodo:

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

per ottenere -1 quando non è possibile trovare l'elemento:

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

Immagino che in questo modo potrebbe essere sia il più veloce che il più semplice. Tuttavia, non ho ancora testato le prestazioni.


0

Un'alternativa alla ricerca dell'indice dopo il fatto è quella di racchiudere l'Enumerable, in qualche modo simile all'utilizzo del metodo 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();
    }
}

Che dà un caso d'uso di:

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

ottima base. È utile aggiungere anche i membri IsEven, IsOdd, IsFirst e IsLast.
JJS,

0

Questo può diventare davvero interessante con un'estensione (che funziona come proxy), ad esempio:

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

Che assegnerà automaticamente gli indici alla raccolta accessibile tramite questo Index proprietà.

Interfaccia:

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

Estensione personalizzata (probabilmente più utile per lavorare con EF e 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();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.