Como obter o índice de um elemento em um IEnumerable?

144

Eu escrevi isto:

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

Mas não sei se já existe, existe?

Jader Dias
fonte
4
O problema com a Maxabordagem é que um: ele continua olhando, e b: ele retorna o último índice quando houver duplicatas (as pessoas geralmente esperam que o primeiro índice)
Marc Gravell
1
geekswithblogs.net compara 4 soluções e seu desempenho. O ToList()/FindIndex()truque tem melhor desempenho
nixda

Respostas:

51

O objetivo de divulgar como IEnumerable é que você pode repetir preguiçosamente o conteúdo. Como tal, não existe realmente um conceito de índice. O que você está fazendo realmente não faz muito sentido para um IEnumerable. Se você precisar de algo que suporte o acesso por índice, coloque-o em uma lista ou coleção real.

Scott Dorman
fonte
8
Atualmente eu vim através desse segmento porque estou implementando um wrapper IList <> genérico para o tipo IEnumerable <> para usar meus objetos IEnumerable <> com componentes de terceiros que suportam apenas fontes de dados do tipo IList. Concordo que tentar obter um índice de um elemento dentro de um objeto IEnumerable é provavelmente na maioria dos casos um sinal de que algo está errado, há momentos em que encontrar esse índice uma vez que é melhor do que reproduzir uma grande coleção na memória apenas para encontrar o índice de um único elemento quando você já possui um IEnumerable.
jpierson
215
-1 causa: existem razões legítimas pelas quais você deseja obter um índice de a IEnumerable<>. Eu não compro todo o dogma "você deveria estar fazendo isso".
precisa
78
Concorde com @ ja72; se você não estivesse lidando com índices IEnumerable, Enumerable.ElementAtnão existiria. IndexOfé simplesmente o inverso - qualquer argumento contra ele deve ser aplicado igualmente a ElementAt.
Kirk Woll #
7
Cleary, C # perde o conceito de IIndexableEnumerable. isso seria apenas o equivalente a um conceito "aleatoriamente acessível", na terminologia C ++ STL.
v.oddou
14
extensões com sobrecargas como Select ((x, i) => ...) parecem implicar que esses índices devam existir #
Michael Michael
126

Eu questionaria a sabedoria, mas talvez:

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

(usando EqualityComparer<T>.Defaultpara emular, !=se necessário) - mas você precisa prestar atenção para retornar -1 se não for encontrado ... então, talvez faça o caminho mais longo

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
fonte
8
+1 para "questionar a sabedoria". 9 em 10 vezes, é provavelmente uma má ideia em primeiro lugar.
Joel Coehoorn
A solução de loop explícito também executa 2x mais rápido (no pior caso) do que a solução Select (). Max () também.
23411 Steve Guidi
1
Você pode apenas contar elementos por lambda sem TakeWhile - ele salva um loop: source.Count (x => x! = Value);
22909 Kamarey
10
@ Kamarey - não, isso faz algo diferente. TakeWhile para quando ocorre uma falha; Count (predicado) retorna os que correspondem. ou seja, se o primeiro foi um erro e tudo o mais foi verdadeiro, TakeWhile (pred) .Count () relatará 0; Contagem (pred) reportará n-1.
Marc Gravell
1
TakeWhileé esperto! Tenha em mente que isso retorna Countse o elemento não existir, que é um desvio do comportamento padrão.
Nawfal
27

Eu implementaria assim:

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
fonte
1
Isso é realmente muito fofo, +1! Envolve objetos extras, mas eles devem ser relativamente baratos (GEN0), portanto, não é um problema enorme. O ==pode precisar de trabalho?
Marc Gravell
1
Adicionada sobrecarga IEqualityComparer, no verdadeiro estilo LINQ. ;)
dahlbyk 17/08/09
1
Eu acho que você quer dizer ... comparer.Equals (xa, value) =) #
Marc
Como a expressão Select está retornando o resultado combinado, que é processado, eu imagino que o uso explícito do tipo de valor KeyValuePair permita evitar qualquer tipo de alocação de heap, desde que o .NET implemente aloque tipos de valor na pilha e qualquer máquina de estado que o LINQ possa gerar usa um campo para o resultado Selecionado que não é declarado como um Objeto vazio (fazendo com que o resultado KVP seja encaixotado). Obviamente, você teria que refazer a condição null == encontrada (uma vez que a opção encontrada agora seria um valor KVP). Talvez usando DefaultIfEmpty () ou KVP<T, int?>(índice nulo)
kornman00
1
Boa implementação, embora uma coisa que eu sugiro adicionar seja uma verificação para ver se obj é implementado IList<T>e, se for o caso, adie para o método IndexOf apenas no caso de uma otimização específica do tipo.
Josh
16

A maneira como estou fazendo isso atualmente é um pouco menor do que os já sugeridos e, até onde sei, dá o resultado desejado:

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

É um pouco desajeitado, mas faz o trabalho e é bastante conciso.

Mark Watts
fonte
6
Embora isso funcione para coleções pequenas, suponha que você tenha um milhão de itens no "palheiro". Fazer um ToList () nele percorrerá todos os um milhão de elementos e os adicionará a uma lista. Em seguida, ele pesquisará na lista para encontrar o índice do elemento correspondente. Isso seria extremamente ineficiente, além da possibilidade de gerar uma exceção se a lista ficar muito grande.
esteuart
3
@esteuart Definitivamente - você precisa escolher uma abordagem adequada ao seu caso de uso. Duvido que exista uma solução única para todos, e é possivelmente por isso que não há uma implementação nas bibliotecas principais.
Mark Watts
8

A melhor maneira de capturar a posição é por FindIndexEsta função está disponível apenas paraList<>

Exemplo

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

Se você tiver um enumerador ou matriz, use desta maneira

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

ou

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

Eu acho que a melhor opção é implementar assim:

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

Também não criará o objeto anônimo

Axente Adrian
fonte
5

Um pouco tarde no jogo, eu sei ... mas foi isso que fiz recentemente. É um pouco diferente do seu, mas permite que o programador determine o que a operação de igualdade precisa ser (predicado). O que acho muito útil ao lidar com tipos diferentes, pois tenho uma maneira genérica de fazê-lo, independentemente do tipo de objeto e<T> operador de igualdade incorporado.

Ele também tem uma pegada de memória muito pequena e é muito, muito rápido / eficiente ... se você se importa com isso.

Na pior das hipóteses, basta adicionar isso à sua lista de extensões.

Enfim ... aqui 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;
 }

Espero que isso ajude alguém.

MaxOvrdrv
fonte
1
Talvez eu esteja perdendo alguma coisa, mas por que o GetEnumeratore MoveNextao invés de apenas um foreach?
Josh Gallagher
1
Resposta curta? Eficiência. Resposta longa: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv
2
Olhando para o IL, parece que a diferença de desempenho é que um foreachchamará Disposeo enumerador se ele implementar IDisposable. (Consulte stackoverflow.com/questions/4982396/… ) Como o código nesta resposta não sabe se o resultado da chamada GetEnumeratoré ou não descartável, ele deve fazer o mesmo. Naquele momento, ainda não estou claro se há um benefício de desempenho, embora houvesse alguma IL extra cujo objetivo não me ocorreu!
Josh Gallagher
@JoshGallagher Pesquisei há algum tempo sobre os benefícios de desempenho entre foreach e for (i), e o principal benefício de usar para (i) era que ByRefs o objeto no local em vez de recriá-lo / passá-lo voltar ByVal. Eu diria que o mesmo se aplica ao MoveNext versus foreach, mas não tenho certeza sobre isso. Talvez eles tanto para uso ByVal ...
MaxOvrdrv
2
Lendo este blog ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ), pode ser que o "loop iterador" ao qual ele está se referindo seja um foreachloop; nesse caso, no caso particular de Tsendo um tipo de valor, pode estar salvando uma operação de caixa / unbox usando o loop while. No entanto, isso não é confirmado pelo IL que recebi de uma versão da sua resposta foreach. Ainda acho que a disposição condicional do iterador é importante. Você poderia modificar a resposta para incluir isso?
Josh Gallagher
5

Alguns anos depois, mas isso usa o Linq, retorna -1 se não for encontrado, não cria objetos extras e deve causar um curto-circuito quando encontrado [em oposição à iteração em todo o 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);
}

Onde '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
fonte
Outra maneira de fazer isso pode ser: public static int IndexOf <T> (esta lista IEnumerable <T>, item T) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( item, x)? x + 1: -1) .FirstOrDefault (x => x> 0); retornar (e == 0)? -1: e - 1); }
Anu Thomas Chandy
"não cria objetos extras". De fato, o Linq criará objetos em segundo plano, portanto isso não está totalmente correto. Ambos source.Wheree source.DefaultIfEmptycriarão, por exemplo, um IEnumerablecada.
Martin Odhelius 22/08/19
1

Tropecei hoje hoje em busca de respostas e pensei em adicionar minha versão à lista (sem trocadilhos). Ele utiliza o operador condicional nulo do 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;

Eu fiz algumas corridas com cavalos velhos (testes) e para grandes coleções (~ 100.000), o pior cenário que o item desejado é no final, é duas vezes mais rápido do que fazer ToList().FindIndex(). Se o item que você quer está no meio, é ~ 4x mais rápido.

Para coleções menores (~ 10.000), parece ser apenas um pouco mais rápido

Aqui está como eu testei https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e

Dave
fonte
1

Usando a resposta de @Marc Gravell, encontrei uma maneira de usar o seguinte método:

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

para obter -1 quando o item não puder ser encontrado:

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

Eu acho que dessa maneira poderia ser o mais rápido e o mais simples. No entanto, ainda não testei performances.

Davide Cannizzo
fonte
0

Uma alternativa para localizar o índice após o fato é agrupar o Enumerable, um pouco semelhante ao uso do 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();
    }
}

O que fornece um 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
fonte
excelente linha de base. É útil adicionar membros IsEven, IsOdd, IsFirst e IsLast também.
JJS
0

Isso pode ser muito legal com uma extensão (funcionando como proxy), por exemplo:

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

O que atribuirá automaticamente índices à coleção acessível por este Index propriedade.

Interface:

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

Extensão personalizada (provavelmente mais útil para trabalhar com 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();
Yom S.
fonte