Comment obtenir l'index d'un élément dans un IEnumerable?


144

J'ai écrit ceci:

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

Mais je ne sais pas si ça existe déjà, n'est-ce pas?


4
Le problème avec une Maxapproche est que a: elle continue de chercher, et b: elle renvoie le dernier index quand il y a des doublons (les gens s'attendent généralement au premier index)
Marc Gravell

1
geekswithblogs.net compare 4 solutions et leurs performances. Le ToList()/FindIndex()tour fonctionne le mieux
nixda

Réponses:


51

L'intérêt de faire sortir les choses en tant que IEnumerable est que vous puissiez parcourir paresseusement le contenu. En tant que tel, il n'y a pas vraiment de concept d'index. Ce que vous faites n'a vraiment pas beaucoup de sens pour un IEnumerable. Si vous avez besoin de quelque chose qui prend en charge l'accès par index, placez-le dans une liste ou une collection réelle.


8
Actuellement, je suis tombé sur ce fil parce que j'implémente un wrapper IList <> générique pour le type IEnumerable <> afin d'utiliser mes objets IEnumerable <> avec des composants tiers qui ne prennent en charge que les sources de données de type IList. Je suis d'accord qu'essayer d'obtenir un index d'un élément dans un objet IEnumerable est probablement dans la plupart des cas un signe que quelque chose a été mal fait, il y a des moments où trouver un tel index bat une fois la reproduction d'une grande collection en mémoire juste pour le plaisir de trouver l'index d'un seul élément lorsque vous avez déjà un IEnumerable.
jpierson

215
-1 cause: il existe des raisons légitimes pour lesquelles vous souhaitez extraire un index d'un fichier IEnumerable<>. Je n'achète pas tout le dogme "tu ferais ça".
John Alexiou

78
D'accord avec @ ja72; si vous ne deviez pas avoir affaire à des index, IEnumerablealors Enumerable.ElementAtn'existerait pas. IndexOfest simplement l'inverse - tout argument contre cela doit s'appliquer également à ElementAt.
Kirk Woll

7
Clairement, C # manque le concept de IIndexableEnumerable. ce serait juste l'équivalent d'un concept "accessible au hasard", dans la terminologie C ++ STL.
v.oddou

14
les extensions avec des surcharges comme Select ((x, i) => ...) semblent impliquer que ces index devraient exister
Michael

126

Je remettrais en question la sagesse, mais peut-être:

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

(utiliser EqualityComparer<T>.Defaultpour émuler !=si nécessaire) - mais vous devez regarder pour renvoyer -1 si non trouvé ... alors peut-être le faire le long du chemin

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 pour "remettre en question la sagesse". 9 fois sur 10, c'est probablement une mauvaise idée en premier lieu.
Joel Coehoorn

La solution de boucle explicite fonctionne également 2 fois plus vite (dans le pire des cas) que la solution Select (). Max () aussi.
Steve Guidi

1
Vous pouvez simplement compter les éléments par lambda sans TakeWhile - il enregistre une boucle: source.Count (x => x! = Valeur);
Kamarey

10
@Kamarey - non, cela fait quelque chose de différent. TakeWhile s'arrête en cas d'échec; Count (prédicat) renvoie ceux qui correspondent. c'est-à-dire si le premier était un échec et que tout le reste était vrai, TakeWhile (pred) .Count () rapportera 0; Count (pred) rapportera n-1.
Marc Gravell

1
TakeWhileest intelligent! Gardez à l'esprit que cela renvoie Countsi l'élément n'existe pas, ce qui est un écart par rapport au comportement standard.
nawfal

27

Je le mettrais en œuvre comme ceci:

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
C'est en fait très mignon, +1! Cela implique des objets supplémentaires, mais ils devraient être relativement bon marché (GEN0), donc pas un gros problème. Le ==pourrait avoir besoin de travail?
Marc Gravell

1
Ajout de la surcharge IEqualityComparer, dans le vrai style LINQ. ;)
dahlbyk

1
Je pense que vous voulez dire ... comparer.Equals (xa, value) =)
Marc

Étant donné que l'expression Select renvoie le résultat combiné, qui est ensuite traité, j'imagine que l'utilisation explicite du type de valeur KeyValuePair vous permettrait d'éviter toute sorte d'allocations de tas, tant que le .NET implique alloue des types de valeur sur la pile et toute machine d'état que LINQ peut générer utilise un champ pour le résultat Select qui n'est pas déclaré comme un objet nu (provoquant ainsi l'encadrement du résultat KVP). Bien sûr, vous devrez retravailler la condition found == null (puisque found serait désormais une valeur KVP). Peut-être en utilisant DefaultIfEmpty () ou KVP<T, int?>(nullable index)
kornman00

1
Belle implémentation, bien qu'une chose que je suggère d'ajouter est une vérification pour voir si obj implémente IList<T>et si c'est le cas, reportez-vous à sa méthode IndexOf au cas où il aurait une optimisation spécifique au type.
Josh

16

La façon dont je fais actuellement est un peu plus courte que celles déjà suggérées et, pour autant que je sache, donne le résultat souhaité:

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

C'est un peu maladroit, mais cela fait le travail et est assez concis.


6
Bien que cela fonctionne pour les petites collections, supposons que vous ayez un million d'articles dans la "botte de foin". Faire une ToList () sur cela va parcourir tous les un million d'éléments et les ajouter à une liste. Ensuite, il effectuera une recherche dans la liste pour trouver l'index de l'élément correspondant. Ce serait extrêmement inefficace ainsi que la possibilité de lancer une exception si la liste devient trop grande.
esteuart

3
@esteuart Certainement - vous devez choisir une approche qui convient à votre cas d'utilisation. Je doute qu'il existe une solution unique, ce qui explique peut-être pourquoi il n'y a pas d'implémentation dans les bibliothèques de base.
Mark Watts

8

La meilleure façon de saisir la position est par FindIndexCette fonction n'est disponible que pourList<>

Exemple

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

Si vous avez un énumérateur ou un tableau, utilisez cette méthode

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

ou

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

7

Je pense que la meilleure option est de mettre en œuvre comme ceci:

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

Il ne créera pas non plus l'objet anonyme


5

Un peu tard dans le jeu, je sais ... mais c'est ce que j'ai fait récemment. Il est légèrement différent du vôtre, mais permet au programmeur de dicter ce que l'opération d'égalité doit être (prédicat). Ce que je trouve très utile pour traiter différents types, car j'ai alors une manière générique de le faire quel que soit le type d'objet et <T>l'opérateur d'égalité intégré.

Il a également une très très petite empreinte mémoire, et est très, très rapide / efficace ... si vous vous en souciez.

Au pire, vous l'ajouterez simplement à votre liste d'extensions.

Quoi qu'il en soit ... le voici.

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

Espérons que cela aide quelqu'un.


1
Peut-être que je manque quelque chose, mais pourquoi le GetEnumeratoret MoveNextplutôt que juste un foreach?
Josh Gallagher

1
Réponse courte? Efficacité. Réponse longue: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv

2
En regardant l'IL, il semble que la différence de performances est que a foreachappellera Disposel'énumérateur s'il implémente IDisposable. (Voir stackoverflow.com/questions/4982396/… ) Comme le code de cette réponse ne sait pas si le résultat de l'appel GetEnumeratorest ou n'est pas jetable, il devrait faire de même. À ce stade, je ne suis toujours pas sûr qu'il y ait un avantage en termes de performances, bien qu'il y ait eu une IL supplémentaire dont le but ne m'a pas sauté dessus!
Josh Gallagher

@JoshGallagher J'ai fait un peu de recherche il y a quelque temps concernant les avantages de la performance entre foreach et for (i), et le principal avantage de l'utilisation de for (i) était qu'il ByRefs l'objet en place plutôt que de le recréer / le transmettre retour ByVal. Je suppose que la même chose s'applique à MoveNext par rapport à foreach, mais je ne suis pas sûr de celui-là. Peut-être qu'ils utilisent tous les deux ByVal ...
MaxOvrdrv

2
En lisant ce blog ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/… ) il se peut que la "boucle itératrice" à laquelle il se réfère soit une foreachboucle, auquel cas pour le cas particulier de Tétant un type valeur, il peut enregistrer une opération box / unbox en utilisant la boucle while. Cependant, cela n'est pas confirmé par l'IL que j'ai obtenue à partir d'une version de votre réponse avec foreach. Je pense cependant que l'élimination conditionnelle de l'itérateur est importante. Pourriez-vous modifier la réponse pour inclure cela?
Josh Gallagher

5

Quelques années plus tard, mais cela utilise Linq, retourne -1 si non trouvé, ne crée pas d'objets supplémentaires et devrait court-circuiter une fois trouvé [par opposition à une itération sur tout le 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);
}

Où 'FirstOr' est:

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

Une autre façon de le faire peut être: public static int IndexOf <T> (this IEnumerable <T> list, T item) {int e = list.Select ((x, index) => EqualityComparer <T> .Default.Equals ( élément, x)? x + 1: -1) .FirstOrDefault (x => x> 0); retour (e == 0)? -1: e - 1); }
Anu Thomas Chandy

"ne crée pas d'objets supplémentaires". Linq créera en fait des objets en arrière-plan, ce n'est donc pas tout à fait correct. Both source.Whereand source.DefaultIfEmptycréera par exemple un IEnumerableeach.
Martin Odhelius

1

Je suis tombé sur cela aujourd'hui dans une recherche de réponses et j'ai pensé ajouter ma version à la liste (sans jeu de mots). Il utilise l'opérateur conditionnel nul 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;

J'ai fait des «courses des vieux chevaux» (tests) et pour les grandes collections (~ 100 000), dans le pire des cas, l'élément que vous voulez est à la fin, c'est 2x plus rapide que de le faire ToList().FindIndex(). Si l'élément que vous voulez est au milieu, il est ~ 4x plus rapide.

Pour les collections plus petites (~ 10 000), il semble être légèrement plus rapide

Voici comment je l'ai testé https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e


1

En utilisant la réponse de @Marc Gravell, j'ai trouvé un moyen d'utiliser la méthode suivante:

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

afin d'obtenir -1 lorsque l'élément est introuvable:

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

Je suppose que cette méthode pourrait être à la fois la plus rapide et la plus simple. Cependant, je n'ai pas encore testé les performances.


0

Une alternative à la recherche de l'index après coup consiste à encapsuler le Enumerable, un peu similaire à l'utilisation de la méthode 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();
    }
}

Ce qui donne un cas d'utilisation 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);
}

excellente base de référence. Il est également utile d'ajouter des membres IsEven, IsOdd, IsFirst et IsLast.
JJS

0

Cela peut devenir vraiment cool avec une extension (fonctionnant comme un proxy), par exemple:

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

Qui attribuera automatiquement des index à la collection accessible via ce Index propriété.

Interface:

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

Extension personnalisée (probablement la plus utile pour travailler avec EF et 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();
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.