Bagaimana cara mendapatkan indeks suatu elemen dalam suatu IEnumerable?

144

Saya menulis ini:

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

Tapi saya tidak tahu apakah itu sudah ada, bukan?

Jader Dias
sumber
4
Masalah dengan Maxpendekatan adalah bahwa: itu terus mencari, dan b: ia mengembalikan terakhir indeks ketika ada duplikat (orang biasanya memperkirakan indeks pertama)
Marc Gravell
1
geekswithblogs.net membandingkan 4 solusi dan kinerja mereka. The ToList()/FindIndex()Melakukan trik terbaik
nixda

Jawaban:

51

Inti dari mengeluarkan segala sesuatu sebagai IEnumerable adalah agar Anda dapat dengan malas beralih pada isinya. Dengan demikian, tidak ada benar-benar sebuah konsep indeks. Apa yang Anda lakukan sebenarnya tidak masuk akal untuk IEnumerable. Jika Anda memerlukan sesuatu yang mendukung akses berdasarkan indeks, taruh dalam daftar atau koleksi aktual.

Scott Dorman
sumber
8
Saat ini saya datang di utas ini karena saya menerapkan pembungkus generik IList untuk tipe IEnumerable untuk menggunakan objek IEnumerable saya dengan komponen pihak ketiga yang hanya mendukung sumber data tipe IList. Saya setuju bahwa mencoba untuk mendapatkan indeks elemen dalam objek IEnumerable mungkin dalam banyak kasus tanda sesuatu beign dilakukan salah ada kalanya menemukan indeks seperti itu ketukan sekali lagi mereproduksi koleksi besar dalam memori hanya demi menemukan indeks dari satu elemen ketika Anda sudah memiliki IEnumerable.
jpierson
215
-1 penyebab: Ada alasan sah mengapa Anda ingin mendapatkan indeks dari a IEnumerable<>. Saya tidak membeli keseluruhan dogma "Kamu harus melakukan ini".
John Alexiou
78
Setuju dengan @ ja72; jika Anda tidak harus berurusan dengan indeks IEnumerablemaka Enumerable.ElementAttidak akan ada. IndexOfhanyalah kebalikan - argumen apa pun yang menentangnya harus berlaku sama ElementAt.
Kirk Woll
7
Cleary, C # merindukan konsep IIndexableEnumerable. yang hanya akan menjadi setara dengan konsep "diakses acak", dalam terminologi C ++ STL.
v.oddou
14
ekstensi dengan kelebihan seperti Select ((x, i) => ...) tampaknya menyiratkan bahwa indeks ini seharusnya ada
Michael
126

Saya akan mempertanyakan kebijaksanaan, tetapi mungkin:

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

(gunakan EqualityComparer<T>.Defaultuntuk meniru !=jika diperlukan) - tetapi Anda harus menonton untuk mengembalikan -1 jika tidak ditemukan ... jadi mungkin lakukan saja

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
sumber
8
+1 untuk "mempertanyakan kebijaksanaan". 9 kali dari 10 itu mungkin ide yang buruk di tempat pertama.
Joel Coehoorn
Solusi loop eksplisit juga berjalan 2x lebih cepat (dalam kasus terburuk) daripada solusi Select (). Max () juga.
Steve Guidi
1
Anda bisa saja Menghitung elemen dengan lambda tanpa TakeWhile - ini menyimpan satu loop: source.Count (x => x! = Nilai);
Kamarey
10
@ Kamarey - tidak, itu melakukan sesuatu yang berbeda. TakeWhile berhenti saat mendapat kegagalan; Hitung (predikat) mengembalikan yang cocok. yaitu jika yang pertama adalah miss dan yang lainnya benar, TakeWhile (pred) .Count () akan melaporkan 0; Hitungan (pred) akan melaporkan n-1.
Marc Gravell
1
TakeWhileitu pintar! Ingatlah ini kembali Countjika elemen tidak ada yang merupakan penyimpangan dari perilaku standar.
nawfal
27

Saya akan menerapkannya seperti ini:

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
sumber
1
Itu sebenarnya sangat lucu, +1! Ini melibatkan objek tambahan, tetapi mereka harus relatif murah (GEN0), jadi bukan masalah besar. The ==mungkin perlu kerja?
Marc Gravell
1
Menambahkan kelebihan IEqualityComparer, dengan gaya LINQ yang sebenarnya. ;)
dahlbyk
1
Saya pikir Anda bermaksud mengatakan ... comparer.Equals (xa, value) =)
Marc
Karena ekspresi Select mengembalikan hasil gabungan, yang kemudian diproses, saya membayangkan secara eksplisit menggunakan tipe nilai KeyValuePair akan memungkinkan Anda untuk menghindari segala jenis alokasi tumpukan, asalkan .NET impl mengalokasikan tipe nilai pada stack dan mesin negara mana pun yang LINQ dapat hasilkan menggunakan bidang untuk hasil Select'd yang tidak dinyatakan sebagai Obyek kosong (sehingga menyebabkan hasil KVP menjadi kotak). Tentu saja, Anda harus mengerjakan ulang kondisi == null yang ditemukan (karena ditemukan sekarang akan menjadi nilai KVP). Mungkin menggunakan DefaultIfEmpty () atau KVP<T, int?>(nullable index)
kornman00
1
Implementasi yang bagus, meskipun satu hal yang saya sarankan tambahkan adalah cek untuk melihat apakah obj mengimplementasikan IList<T>dan jika demikian, tunda metode IndexOf untuk berjaga-jaga jika ada optimasi tipe-spesifik.
Josh
16

Cara saya saat ini melakukan ini sedikit lebih pendek dari yang sudah disarankan dan sejauh yang saya tahu memberikan hasil yang diinginkan:

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

Agak kikuk, tetapi berhasil dan cukup ringkas.

Tandai Watts
sumber
6
Meskipun ini akan berfungsi untuk koleksi kecil, misalkan Anda memiliki sejuta item di "tumpukan jerami". Melakukan ToList () pada itu akan beralih melalui semua satu juta elemen dan menambahkannya ke daftar. Kemudian akan mencari melalui daftar untuk menemukan indeks elemen yang cocok. Ini akan sangat tidak efisien serta kemungkinan untuk melemparkan pengecualian jika daftar menjadi terlalu besar.
esteuart
3
@esteuart Pasti - Anda harus memilih pendekatan yang sesuai dengan kasus penggunaan Anda. Saya ragu ada satu ukuran cocok untuk semua solusi, yang mungkin mengapa tidak ada implementasi di perpustakaan inti.
Mark Watts
8

Cara terbaik untuk menangkap posisi adalah dengan FindIndexFungsi ini hanya tersedia untukList<>

Contoh

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

Jika Anda memiliki enumerator atau array gunakan cara ini

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

atau

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

Saya pikir opsi terbaik adalah menerapkan seperti ini:

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

Itu juga tidak akan membuat objek anonim

Axente Adrian
sumber
5

Agak terlambat dalam permainan, saya tahu ... tapi inilah yang baru-baru ini saya lakukan. Ini sedikit berbeda dari milik Anda, tetapi memungkinkan pemrogram untuk menentukan apa yang harus operasi kesetaraan (predikat). Yang saya temukan sangat berguna ketika berhadapan dengan berbagai jenis, karena saya kemudian memiliki cara umum untuk melakukannya terlepas dari jenis objek dan<T> dibangun di operator kesetaraan.

Ini juga memiliki jejak memori yang sangat sangat kecil, dan sangat, sangat cepat / efisien ... jika Anda peduli tentang itu.

Lebih buruk lagi, Anda hanya akan menambahkan ini ke daftar ekstensi Anda.

Ngomong-ngomong ... ini dia.

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

Semoga ini bisa membantu seseorang.

MaxOvrdrv
sumber
1
Mungkin saya melewatkan sesuatu, tetapi mengapa GetEnumeratordan MoveNextbukan hanya foreach?
Josh Gallagher
1
Jawaban singkat? Efisiensi. Jawaban panjang: msdn.microsoft.com/en-us/library/9yb8xew9.aspx
MaxOvrdrv
2
Melihat IL, tampak bahwa perbedaan kinerja adalah bahwa foreachakan memanggil Disposeenumerator jika mengimplementasikan IDisposable. (Lihat stackoverflow.com/questions/4982396/… ) Karena kode dalam jawaban ini tidak tahu apakah hasil dari panggilan GetEnumeratoradalah atau tidak, harus dilakukan hal yang sama. Pada saat itu saya masih belum jelas bahwa ada manfaat perf, meskipun ada beberapa IL tambahan yang tujuannya tidak melompat ke arahku!
Josh Gallagher
@ JoshGallagher Saya melakukan sedikit riset beberapa waktu lalu mengenai manfaat perf antara foreach dan for (i), dan manfaat utama menggunakan for (i) adalah bahwa ByRefs objek di tempat daripada menciptakan kembali / melewatkannya kembali ByVal. Saya akan menganggap hal yang sama berlaku untuk MoveNext versus foreach, tapi saya tidak yakin tentang itu. Mungkin mereka berdua menggunakan ByVal ...
MaxOvrdrv
2
Membaca blog ini ( blogs.msdn.com/b/ericlippert/archive/2010/09/30/... ) mungkin "loop iterator" yang ia maksudkan adalah foreachloop, dalam hal ini untuk kasus tertentu Tmenjadi tipe nilai mungkin menyimpan kotak / operasi unbox dengan menggunakan loop sementara. Namun, ini tidak didukung oleh IL yang saya dapatkan dari versi jawaban Anda foreach. Saya masih berpikir pembuangan bersyarat iterator penting, meskipun. Bisakah Anda memodifikasi jawaban untuk memasukkan itu?
Josh Gallagher
5

Beberapa tahun kemudian, tetapi ini menggunakan Linq, mengembalikan -1 jika tidak ditemukan, tidak membuat objek tambahan, dan harus korsleting ketika ditemukan [sebagai lawan iterasi seluruh 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);
}

Di mana 'FirstOr' adalah:

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
sumber
Cara lain untuk melakukannya adalah: public static int IndexOf <T> (daftar <T> IEnumerable ini, item T) {int e = list. Pilih ((x, indeks) => EqualityComparer <T> .Default.Equals ( item, x)? x + 1: -1) .FirstOrDefault (x => x> 0); kembali (e == 0)? -1: e - 1); }
Anu Thomas Chandy
+ msgstr "tidak membuat objek tambahan". Linq sebenarnya akan membuat objek di latar belakang jadi ini tidak sepenuhnya benar. Keduanya source.Wheredan source.DefaultIfEmptymisalnya akan membuat IEnumerablemasing - masing.
Martin Odhelius
1

Tersandung di sini hari ini dalam mencari jawaban dan saya pikir saya akan menambahkan versi saya ke daftar (No pun intended). Ini menggunakan operator bersyarat nol dari 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;

Saya telah melakukan beberapa 'balap kuda tua' (pengujian) dan untuk koleksi besar (~ 100.000), skenario terburuk item yang Anda inginkan adalah pada akhirnya, ini 2x lebih cepat daripada melakukannya ToList().FindIndex(). Jika Item yang Anda inginkan ada di tengah ~ 4x lebih cepat.

Untuk koleksi yang lebih kecil (~ 10.000) sepertinya hanya sedikit lebih cepat

Inilah cara saya mengujinya https://gist.github.com/insulind/16310945247fcf13ba186a45734f254e

Dave
sumber
1

Menggunakan jawaban @Marc Gravell, saya menemukan cara untuk menggunakan metode berikut:

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

untuk mendapatkan -1 saat item tidak dapat ditemukan:

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

Saya kira cara ini bisa menjadi yang tercepat dan lebih sederhana. Namun, saya belum menguji kinerja.

Davide Cannizzo
sumber
0

Alternatif untuk menemukan indeks setelah faktanya adalah dengan membungkus Enumerable, agak mirip dengan menggunakan metode 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();
    }
}

Yang memberikan kasus penggunaan:

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
sumber
dasar yang bagus. Sangat membantu untuk menambah anggota IsEven, IsOdd, IsFirst dan IsLast juga.
JJS
0

Ini bisa menjadi sangat keren dengan ekstensi (berfungsi sebagai proxy), misalnya:

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

Yang secara otomatis akan menetapkan indeks untuk koleksi yang dapat diakses melalui Indexproperti ini .

Antarmuka:

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

Ekstensi khusus (mungkin paling berguna untuk bekerja dengan EF dan 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.
sumber