¿Cómo obtener el índice de un elemento en un IEnumerable?

144

Yo escribí esto:

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

Pero no sé si ya existe, ¿verdad?

Jader Dias
fuente
44
El problema con un Maxenfoque es que a: sigue buscando yb: devuelve el último índice cuando hay duplicados (la gente suele esperar el primer índice)
Marc Gravell
1
geekswithblogs.net compara 4 soluciones y su rendimiento. El ToList()/FindIndex()truco funciona mejor
nixda

Respuestas:

51

El objetivo de sacar las cosas como IEnumerable es para que puedas iterar perezosamente sobre los contenidos. Como tal, no existe realmente un concepto de índice. Lo que estás haciendo realmente no tiene mucho sentido para un IEnumerable. Si necesita algo que admita el acceso por índice, colóquelo en una lista o colección real.

Scott Dorman
fuente
8
Actualmente me encontré con este hilo porque estoy implementando un contenedor IList <> genérico para el tipo IEnumerable <> para usar mis objetos IEnumerable <> con componentes de terceros que solo admiten fuentes de datos de tipo IList. Estoy de acuerdo en que tratar de obtener un índice de un elemento dentro de un objeto IEnumerable es probablemente en la mayoría de los casos un signo de que algo está mal hecho, hay veces en que encontrar dicho índice una vez supera la reproducción de una gran colección en memoria solo por encontrar el índice de un solo elemento cuando ya tienes un IEnumerable.
jpierson el
215
-1 causa: existen razones legítimas por las que desea obtener un índice de a IEnumerable<>. No compro todo el dogma "deberías estar haciendo esto".
John Alexiou
78
De acuerdo con @ ja72; si no se tratara con índices, IEnumerableentonces Enumerable.ElementAtno existiría. IndexOfes simplemente lo inverso: cualquier argumento en contra debe aplicarse igualmente a ElementAt.
Kirk Woll
77
Claramente, C # pierde el concepto de IIndexableEnumerable. eso sería el equivalente de un concepto "accesible al azar", en terminología C ++ STL.
v.oddou
14
Las extensiones con sobrecargas como Select ((x, i) => ...) parecen implicar que estos índices deberían existir
Michael
126

Cuestionaría la sabiduría, pero tal vez:

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

(usando EqualityComparer<T>.Defaultpara emular !=si es necesario), pero necesita mirar para devolver -1 si no se encuentra ... así que tal vez solo hágalo a la larga

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;
}
Marc Gravell
fuente
8
+1 por "cuestionar la sabiduría". 9 de cada 10 veces es probablemente una mala idea en primer lugar.
Joel Coehoorn
La solución de bucle explícito también se ejecuta 2 veces más rápido (en el peor de los casos) que la solución Select (). Max () también.
Steve Guidi
1
Puede simplemente contar elementos por lambda sin TakeWhile: guarda un bucle: source.Count (x => x! = Value);
Kamarey el
10
@Kamarey: no, eso hace algo diferente. TakeWhile se detiene cuando falla; Count (predicate) devuelve los que coinciden. es decir, si el primero fue un fallo y todo lo demás fue cierto, TakeWhile (pred) .Count () informará 0; Count (pred) reportará n-1.
Marc Gravell
1
TakeWhile¡es inteligente! Tenga en cuenta que esto regresa Countsi el elemento no existe, lo cual es una desviación del comportamiento estándar.
nawfal
27

Lo implementaría así:

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
fuente
1
Eso es realmente muy lindo, +1! Se trata de objetos adicionales, pero deberían ser relativamente baratos (GEN0), por lo que no es un gran problema. El ==podría necesitar trabajo?
Marc Gravell
1
Se agregó sobrecarga de IEqualityComparer, en un verdadero estilo LINQ. ;)
dahlbyk
1
Creo que quieres decir ... comparer.Equals (xa, value) =)
Marc
Dado que la expresión Select devuelve el resultado combinado, que luego se procesa, me imagino que usar explícitamente el tipo de valor KeyValuePair le permitiría evitar cualquier tipo de asignaciones de almacenamiento dinámico, siempre que .NET implique los tipos de valor en la pila y cualquier máquina de estado que LINQ pueda generar utiliza un campo para el resultado Seleccionado que no se declara como un Objeto simple (lo que hace que el resultado KVP quede encuadrado). Por supuesto, tendría que volver a trabajar la condición nula encontrada == (ya que encontrada ahora sería un valor KVP). Tal vez usando DefaultIfEmpty () o KVP<T, int?>(índice anulable)
kornman00
1
Buena implementación, aunque una cosa que sugeriría agregar es una verificación para ver si obj se implementa IList<T>y, en caso afirmativo, diferir a su método IndexOf en caso de que tenga una optimización de tipo específico.
Josh
16

La forma en que lo estoy haciendo actualmente es un poco más corta que las ya sugeridas y, por lo que puedo decir, da el resultado deseado:

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

Es un poco torpe, pero hace el trabajo y es bastante conciso.

Mark Watts
fuente
66
Aunque esto funcionaría para pequeñas colecciones, supongamos que tiene un millón de artículos en el "pajar". Hacer una ToList () en ese iterará a través de todos los elementos de un millón y los agregará a una lista. Luego buscará en la lista para encontrar el índice del elemento coincidente. Esto sería extremadamente ineficiente, así como la posibilidad de lanzar una excepción si la lista se vuelve demasiado grande.
esteuart
3
@esteuart Definitivamente, debe elegir un enfoque que se adapte a su caso de uso. Dudo que haya una solución única para todos, por lo que posiblemente no haya una implementación en las bibliotecas principales.
Mark Watts
8

La mejor manera de tomar la posición es FindIndex Esta función está disponible solo paraList<>

Ejemplo

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

Si tiene enumerador o matriz, use de esta manera

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

o

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

Creo que la mejor opción es implementar así:

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

Tampoco creará el objeto anónimo

Axente Adrian
fuente
5

Un poco tarde en el juego, lo sé ... pero esto es lo que hice recientemente. Es ligeramente diferente al suyo, pero permite que el programador dicte lo que la operación de igualdad debe ser (predicado). Lo cual encuentro muy útil cuando trato con diferentes tipos, ya que tengo una forma genérica de hacerlo independientemente del tipo de objeto y del <T>operador de igualdad incorporado.

También tiene una huella de memoria muy pequeña, y es muy, muy rápido / eficiente ... si te importa eso.

En el peor de los casos, solo agregará esto a su lista de extensiones.

De todos modos ... aquí está.

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

Esperemos que esto ayude a alguien.

MaxOvrdrv
fuente
1
Tal vez me estoy perdiendo algo, pero ¿por qué GetEnumeratory en MoveNextlugar de solo un foreach?
Josh Gallagher
1
¿Respuesta corta? Eficiencia. Respuesta larga: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv
2
Al observar el IL, parece que la diferencia de rendimiento es que un foreachllamará Disposeal enumerador si se implementa IDisposable. (Consulte stackoverflow.com/questions/4982396/… ) Como el código en esta respuesta no sabe si el resultado de la llamada GetEnumeratores o no desechable, debería hacer lo mismo. En ese momento todavía no estoy claro si hay un beneficio de rendimiento, ¡aunque hubo una IL adicional cuyo propósito no se me ocurrió!
Josh Gallagher
@JoshGallagher Investigué un poco hace un tiempo sobre los beneficios de rendimiento entre foreach y para (i), y el principal beneficio de usar para (i) fue que ByRefs el objeto en su lugar en lugar de recrearlo / pasarlo atrás ByVal. Supongo que lo mismo se aplica a MoveNext versus foreach, pero no estoy seguro de eso. Tal vez ambos usan ByVal ...
MaxOvrdrv
2
Al leer este blog ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ) puede ser que el "bucle iterador" al que se refiere sea un foreachbucle, en cuyo caso para el caso particular de Tal ser un tipo de valor, podría estar guardando una operación de caja / caja utilizando el bucle while. Sin embargo, esto no es confirmado por el IL que obtuve de una versión de su respuesta foreach. Sin embargo, sigo pensando que la eliminación condicional del iterador es importante. ¿Podría modificar la respuesta para incluir eso?
Josh Gallagher
5

Unos años más tarde, pero esto usa Linq, devuelve -1 si no se encuentra, no crea objetos adicionales y debería cortocircuitarse cuando se encuentra [en lugar de iterar sobre todo 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);
}

Donde 'FirstOr' es:

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
fuente
Otra forma de hacerlo puede ser: public static int IndexOf <T> (esta lista IEnumerable <T>, elemento T) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( elemento, x)? x + 1: -1) .PrimerOrDefault (x => x> 0); volver (e == 0)? -1: e - 1); }
Anu Thomas Chandy
"no crea objetos adicionales". De hecho, Linq creará objetos en segundo plano, por lo que esto no es completamente correcto. Ambos source.Wherey source.DefaultIfEmpty, por ejemplo, crearán un IEnumerablecada uno.
Martin Odhelius
1

Encontré esto hoy en busca de respuestas y pensé en agregar mi versión a la lista (sin juego de palabras). Utiliza el operador condicional nulo de 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;

He hecho algunas 'carreras de los viejos caballos' (pruebas) y para grandes colecciones (~ 100,000), el peor de los casos en que el artículo que deseas es al final, esto es 2 veces más rápido que hacerlo ToList().FindIndex(). Si el artículo que desea está en el medio, es ~ 4 veces más rápido.

Para colecciones más pequeñas (~ 10,000) parece ser solo marginalmente más rápido

Aquí está cómo lo probé https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e

Dave
fuente
1

Usando la respuesta de @Marc Gravell, encontré una manera de usar el siguiente método:

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

para obtener -1 cuando no se puede encontrar el artículo:

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

Supongo que de esta manera podría ser tanto el más rápido como el más simple. Sin embargo, aún no he probado las actuaciones.

Davide Cannizzo
fuente
0

Una alternativa para encontrar el índice después del hecho es envolver el Enumerable, algo similar a usar el método 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();
    }
}

Lo que da un caso de uso de:

var items = new[] {1, 2, 3};
var indexedItems = items.ToIndexed();
foreach (var item in indexedItems)
{
    Console.WriteLine("items[{0}] = {1}", item.Index, item.Value);
}
Joshka
fuente
Gran línea de base. También es útil agregar miembros IsEven, IsOdd, IsFirst e IsLast.
JJS
0

Esto puede ser realmente genial con una extensión (que funciona como un proxy), por ejemplo:

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

Que asignará automáticamente índices a la colección accesible a través de este Index propiedad.

Interfaz:

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

Extensión personalizada (probablemente más útil para trabajar con EF y 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();
Iom S.
fuente