Hitung item dari IEnumerable <T> tanpa iterasi?

317
private IEnumerable<string> Tables
{
    get
    {
        yield return "Foo";
        yield return "Bar";
    }
}

Katakanlah saya ingin mengulanginya dan menulis sesuatu seperti memproses #n dari #m.

Apakah ada cara saya bisa mengetahui nilai m tanpa iterasi sebelum iterasi utama saya?

Saya harap saya membuat diri saya jelas.

sebagomez
sumber

Jawaban:

338

IEnumerabletidak mendukung ini. Ini dengan desain. IEnumerablemenggunakan evaluasi malas untuk mendapatkan elemen yang Anda minta tepat sebelum Anda membutuhkannya.

Jika Anda ingin mengetahui jumlah item tanpa mengulanginya, Anda dapat menggunakannya ICollection<T>, ia memiliki Countproperti.

Mendelt
sumber
38
Saya lebih suka ICollection daripada IList jika Anda tidak perlu mengakses daftar dengan pengindeks.
Michael Meadows
3
Saya biasanya hanya mengambil List dan IList karena kebiasaan. Tetapi terutama jika Anda ingin menerapkannya sendiri, ICollection lebih mudah dan juga memiliki properti Count. Terima kasih!
Mendelt
21
@ Kimmy Anda mengulangi dan menghitung elemen. Atau Anda memanggil Count () dari namespace Linq yang melakukan ini untuk Anda.
Mendelt
1
Haruskah hanya mengganti IEnumerable dengan IList sudah cukup dalam keadaan normal?
Teekin
1
@ Helgi Karena IEnumerable dievaluasi malas Anda dapat menggunakannya untuk hal-hal yang tidak dapat digunakan IList. Anda dapat membangun fungsi yang mengembalikan IEnumerable yang menyebutkan semua desimal Pi misalnya. Selama Anda tidak pernah mencoba untuk mengetahui hasil lengkapnya, itu hanya akan berhasil. Anda tidak dapat membuat IList yang mengandung Pi. Tapi itu semua cukup akademis. Untuk sebagian besar penggunaan normal, saya sepenuhnya setuju. Jika Anda perlu Hitung, Anda perlu IList. :-)
Mendelt
215

The System.Linq.Enumerable.Countmetode penyuluhan IEnumerable<T>memiliki pelaksanaan berikut:

ICollection<T> c = source as ICollection<TSource>;
if (c != null)
    return c.Count;

int result = 0;
using (IEnumerator<T> enumerator = source.GetEnumerator())
{
    while (enumerator.MoveNext())
        result++;
}
return result;

Jadi ia mencoba untuk menggunakan ICollection<T>, yang memiliki Countproperti, dan menggunakannya jika memungkinkan. Kalau tidak, iterates.

Jadi taruhan terbaik Anda adalah menggunakan Count()metode ekstensi pada IEnumerable<T>objek Anda , karena Anda akan mendapatkan kinerja terbaik dengan cara itu.

Daniel Earwicker
sumber
13
Sangat menarik hal yang coba dilemparkan ke yang ICollection<T>pertama.
Oscar Mederos
1
@OscarMederos Sebagian besar metode ekstensi di Enumerable memiliki optimasi untuk urutan berbagai jenis di mana mereka akan menggunakan cara yang lebih murah jika mereka bisa.
Shibumi
1
Ekstensi tersebut tersedia sejak. Net 3.5 dan didokumentasikan dalam MSDN .
Christian
Saya pikir Anda tidak perlu jenis generik Tpada IEnumerator<T> enumerator = source.GetEnumerator(), hanya Anda dapat melakukan ini: IEnumerator enumerator = source.GetEnumerator()dan harus bekerja!
Jaider
7
@Jaider - sedikit lebih rumit dari itu. IEnumerable<T>mewarisi IDisposableyang memungkinkan usingpernyataan untuk membuangnya secara otomatis. IEnumerabletidak. Jadi, jika Anda menelepon GetEnumeratordengan cara apa pun, Anda harus menyelesaikannya denganvar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker
87

Cukup tambahkan beberapa info tambahan:

The Count()ekstensi tidak selalu iterate. Pertimbangkan Linq ke Sql, tempat penghitungan menuju database, tetapi alih-alih mengembalikan semua baris, ia mengeluarkan Count()perintah Sql dan mengembalikan hasilnya.

Selain itu, kompiler (atau runtime) cukup pintar sehingga akan memanggil Count()metode objek jika memilikinya. Jadi tidak seperti yang dikatakan responden lain, benar-benar bodoh dan selalu berulang untuk menghitung elemen.

Dalam banyak kasus di mana programmer hanya memeriksa if( enumerable.Count != 0 )menggunakan Any()metode ekstensi, karena if( enumerable.Any() ) jauh lebih efisien dengan evaluasi malas linq karena dapat mengalami hubungan pendek setelah dapat menentukan ada elemen. Ini juga lebih mudah dibaca

Robert Paulson
sumber
2
Berkenaan dengan koleksi dan array. Jika Anda kebetulan menggunakan koleksi maka gunakan .Countproperti karena selalu tahu ukurannya. Saat meminta collection.Counttidak ada perhitungan tambahan, itu hanya mengembalikan jumlah yang sudah diketahui. Sama Array.lengthseperti yang saya tahu. Namun, .Any()dapatkan enumerator dari sumber menggunakan using (IEnumerator<TSource> enumerator = source.GetEnumerator())dan mengembalikan true jika itu bisa dilakukan enumerator.MoveNext(). Untuk koleksi if(collection.Count > 0):, array: if(array.length > 0)dan lakukan enumerables if(collection.Any()).
Tidak
1
Poin pertama tidak sepenuhnya benar ... LINQ to SQL menggunakan metode ekstensi ini yang tidak sama dengan yang ini . Jika Anda menggunakan yang kedua, penghitungan dilakukan dalam memori, bukan sebagai fungsi SQL
AlexFoxGill
1
@AlexFoxGill benar. Jika Anda secara eksplisit IQueryably<T>memasukkan Anda ke a IEnumerable<T>itu tidak akan mengeluarkan jumlah sql. Ketika saya menulis ini, Linq to Sql masih baru; Saya pikir saya hanya mendorong untuk menggunakan Any()karena untuk enumerables, koleksi dan sql itu jauh lebih efisien dan mudah dibaca (secara umum). Terima kasih telah meningkatkan jawabannya.
Robert Paulson
12

Seorang teman saya memiliki serangkaian posting blog yang memberikan ilustrasi mengapa Anda tidak bisa melakukan ini. Dia menciptakan fungsi yang mengembalikan IEnumerable di mana setiap iterasi mengembalikan bilangan prima berikutnya, sampai ke sana ulong.MaxValue, dan item berikutnya tidak dihitung sampai Anda memintanya. Cepat, pertanyaan pop: berapa banyak barang yang dikembalikan?

Ini postingannya, tapi agak panjang:

  1. Beyond Loops (menyediakan kelas EnumerableUtility awal yang digunakan dalam posting lain)
  2. Aplikasi Iterate (Implementasi awal)
  3. Metode Ekstensi Gila: ToLazyList (Optimalisasi Kinerja)
Joel Coehoorn
sumber
2
Saya benar-benar berharap MS telah menetapkan cara untuk meminta enumerables untuk menggambarkan apa yang mereka dapat tentang diri mereka sendiri (dengan "tidak tahu apa-apa" menjadi jawaban yang valid). Tidak ada enumerable yang memiliki kesulitan menjawab pertanyaan seperti "Apakah Anda tahu diri Anda terbatas", "Apakah Anda tahu diri Anda terbatas dengan kurang dari N elemen", dan "Apakah Anda tahu diri Anda tidak terbatas", karena setiap enumerable dapat secara sah (jika tidak membantu) jawab "tidak" untuk mereka semua. Jika ada cara standar untuk mengajukan pertanyaan seperti itu, akan jauh lebih aman bagi pencacah untuk mengembalikan urutan yang tak berujung ...
supercat
1
... (karena mereka dapat mengatakan bahwa mereka melakukannya), dan agar kode menganggap bahwa enumerator yang tidak mengklaim untuk mengembalikan urutan tanpa akhir cenderung terikat. Perhatikan bahwa memasukkan cara mengajukan pertanyaan seperti itu (untuk meminimalkan pelat baja, mungkin memiliki properti mengembalikan EnumerableFeaturesobjek) tidak akan memerlukan enumerator untuk melakukan sesuatu yang sulit, tetapi mampu mengajukan pertanyaan seperti itu (bersama dengan beberapa yang lain seperti "Bisakah Anda berjanji untuk selalu mengembalikan urutan item yang sama "," Bisakah Anda dengan aman terpapar kode yang seharusnya tidak mengubah koleksi dasar Anda ", dll.) akan sangat berguna.
supercat
Itu akan sangat keren, tapi saya sekarang yakin seberapa baik itu akan menyatu dengan blok iterator. Anda perlu semacam "opsi hasil" khusus atau sesuatu. Atau mungkin menggunakan atribut untuk menghias metode iterator.
Joel Coehoorn
1
Blok Iterator dapat, dengan tidak adanya deklarasi khusus lainnya, cukup melaporkan bahwa mereka tidak tahu apa-apa tentang urutan yang dikembalikan, meskipun jika IEnumerator atau penerus yang didukung MS (yang bisa dikembalikan oleh implementasi GetEnumerator yang mengetahui keberadaannya) adalah untuk mendukung informasi tambahan, C # kemungkinan akan mendapatkan set yield optionspernyataan atau sesuatu yang serupa untuk mendukungnya. Jika dirancang dengan benar, sebuah IEnhancedEnumeratordapat membuat hal-hal seperti LINQ jauh lebih bermanfaat dengan menghilangkan banyak "pertahanan" ToArrayatau ToListpanggilan, terutama ...
supercat
... dalam kasus di mana hal-hal seperti Enumerable.Concatdigunakan untuk menggabungkan koleksi besar yang tahu banyak tentang dirinya dengan yang kecil yang tidak.
supercat
10

IEnumerable tidak dapat dihitung tanpa iterasi.

Dalam keadaan "normal", dimungkinkan untuk kelas yang mengimplementasikan IEnumerable atau IEnumerable <T>, seperti List <T>, untuk mengimplementasikan metode Count dengan mengembalikan properti Daftar <T> .Count. Namun, metode Count sebenarnya bukan metode yang didefinisikan pada antarmuka IEnumerable <T> atau IEnumerable. (Satu-satunya yang, pada kenyataannya, adalah GetEnumerator.) Dan ini berarti bahwa implementasi khusus kelas tidak dapat disediakan untuk itu.

Sebaliknya, Hitung itu adalah metode ekstensi, didefinisikan pada kelas statis Enumerable. Ini berarti dapat dipanggil pada instance kelas turunan <n> IEnumerable, terlepas dari implementasi kelas itu. Tetapi itu juga berarti itu diterapkan di satu tempat, di luar kelas-kelas tersebut. Yang tentu saja berarti bahwa itu harus diimplementasikan dengan cara yang sepenuhnya independen dari internal kelas ini. Satu-satunya cara untuk melakukan penghitungan adalah melalui iterasi.

Chris Ammerman
sumber
itu poin bagus tentang tidak bisa menghitung kecuali jika Anda mengulanginya. Fungsionalitas penghitungan terkait dengan kelas yang mengimplementasikan IEnumerable .... sehingga Anda harus memeriksa jenis IEnumerable apa yang masuk (periksa dengan casting) dan kemudian Anda tahu bahwa Daftar <> dan Kamus <> memiliki cara-cara tertentu untuk menghitung dan kemudian menggunakan itu hanya setelah Anda tahu tipenya. Saya menemukan utas ini sangat berguna secara pribadi jadi terima kasih atas balasan Anda juga Chris.
PositiveGuy
1
Menurut jawaban Daniel, jawaban ini tidak sepenuhnya benar: implementasi TIDAK memeriksa apakah objek mengimplementasikan "ICollection", yang memiliki bidang "Count". Jika demikian, ia menggunakannya. (Apakah itu sepintar itu di '08, saya tidak tahu.)
ToolmakerSteve
9

Atau Anda dapat melakukan hal berikut:

Tables.ToList<string>().Count;
Anatoliy Nikolaev
sumber
8

Tidak, tidak secara umum. Satu poin dalam menggunakan enumerables adalah bahwa set sebenarnya objek dalam enumerasi tidak diketahui (sebelumnya, atau bahkan sama sekali).

JesperE
sumber
Poin penting yang Anda kemukakan adalah bahwa bahkan ketika Anda mendapatkan objek IEnumerable itu, Anda harus melihat apakah Anda dapat membuangnya untuk mengetahui apa jenisnya. Itu poin yang sangat penting bagi mereka yang mencoba menggunakan lebih banyak IEnumerable seperti saya dalam kode saya.
PositiveGuy
8

Anda dapat menggunakan System.Linq.

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
    private IEnumerable<string> Tables
    {
        get {
             yield return "Foo";
             yield return "Bar";
         }
    }

    static void Main()
    {
        var x = new Test();
        Console.WriteLine(x.Tables.Count());
    }
}

Anda akan mendapatkan hasil '2'.

prosseek
sumber
3
Ini tidak berfungsi untuk varian non-generik IEnumerable (tanpa penentu tipe)
Marcel
Implementasi .Count menghitung semua item jika Anda memiliki IEnumerable. (berbeda untuk ICollection). Pertanyaan OP jelas "tanpa iterasi"
JDC
5

Melampaui pertanyaan langsung Anda (yang telah dijawab sepenuhnya dalam negatif), jika Anda ingin melaporkan kemajuan sambil memproses enumerable, Anda mungkin ingin melihat posting blog saya Kemajuan Pelaporan Selama Pertanyaan Linq .

Ini memungkinkan Anda melakukan ini:

BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += (sender, e) =>
      {
          // pretend we have a collection of 
          // items to process
          var items = 1.To(1000);
          items
              .WithProgressReporting(progress => worker.ReportProgress(progress))
              .ForEach(item => Thread.Sleep(10)); // simulate some real work
      };
Samuel Jack
sumber
4

Saya menggunakan cara seperti di dalam sebuah metode untuk memeriksa disahkan pada IEnumberablekonten

if( iEnum.Cast<Object>().Count() > 0) 
{

}

Di dalam metode seperti ini:

GetDataTable(IEnumberable iEnum)
{  
    if (iEnum != null && iEnum.Cast<Object>().Count() > 0) //--- proceed further

}
Shahidul Haque
sumber
1
Kenapa melakukan itu? "Hitung" mungkin mahal, jadi dengan diberi IEnumerable, lebih murah untuk menginisialisasi output apa pun yang Anda perlukan untuk default yang sesuai, kemudian mulai iterasi dengan "iEnum". Pilih default sehingga "iEnum" kosong, yang tidak pernah menjalankan loop, berakhir dengan hasil yang valid. Terkadang, ini berarti menambahkan bendera boolean untuk mengetahui apakah loop telah dieksekusi. Memang itu canggung, tetapi mengandalkan "Count" tampaknya tidak bijaksana. Jika perlu bendera ini, kode terlihat seperti: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve
... itu juga mudah untuk menambahkan kode khusus yang harus dilakukan hanya pertama kalinya, atau hanya pada iterasi lain daripada yang pertama kalinya: `... {if {hasContents = true; (hasContents!) ..satu kode waktu ..; } else {..code untuk semua kecuali yang pertama kali ..} ...} "Harus diakui, ini lebih jelas daripada pendekatan sederhana Anda, di mana kode satu kali akan berada di dalam if Anda, sebelum loop, tetapi jika biaya" .Count () "mungkin menjadi masalah, maka ini adalah cara untuk pergi.
ToolmakerSteve
3

Tergantung pada versi .Net dan implementasi objek IEnumerable Anda. Microsoft telah memperbaiki metode IEnumerable.Count untuk memeriksa implementasi, dan menggunakan ICollection.Count atau ICollection <TSource> .Count, lihat detailnya di sini https://connect.microsoft.com/VisualStudio/feedback/details/454130

Dan di bawah ini adalah MSIL dari Ildasm untuk System.Core, di mana System.Linq berada.

.method public hidebysig static int32  Count<TSource>(class 

[mscorlib]System.Collections.Generic.IEnumerable`1<!!TSource> source) cil managed
{
  .custom instance void System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       85 (0x55)
  .maxstack  2
  .locals init (class [mscorlib]System.Collections.Generic.ICollection`1<!!TSource> V_0,
           class [mscorlib]System.Collections.ICollection V_1,
           int32 V_2,
           class [mscorlib]System.Collections.Generic.IEnumerator`1<!!TSource> V_3)
  IL_0000:  ldarg.0
  IL_0001:  brtrue.s   IL_000e
  IL_0003:  ldstr      "source"
  IL_0008:  call       class [mscorlib]System.Exception System.Linq.Error::ArgumentNull(string)
  IL_000d:  throw
  IL_000e:  ldarg.0
  IL_000f:  isinst     class [mscorlib]System.Collections.Generic.ICollection`1<!!TSource>
  IL_0014:  stloc.0
  IL_0015:  ldloc.0
  IL_0016:  brfalse.s  IL_001f
  IL_0018:  ldloc.0
  IL_0019:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.ICollection`1<!!TSource>::get_Count()
  IL_001e:  ret
  IL_001f:  ldarg.0
  IL_0020:  isinst     [mscorlib]System.Collections.ICollection
  IL_0025:  stloc.1
  IL_0026:  ldloc.1
  IL_0027:  brfalse.s  IL_0030
  IL_0029:  ldloc.1
  IL_002a:  callvirt   instance int32 [mscorlib]System.Collections.ICollection::get_Count()
  IL_002f:  ret
  IL_0030:  ldc.i4.0
  IL_0031:  stloc.2
  IL_0032:  ldarg.0
  IL_0033:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<!!TSource>::GetEnumerator()
  IL_0038:  stloc.3
  .try
  {
    IL_0039:  br.s       IL_003f
    IL_003b:  ldloc.2
    IL_003c:  ldc.i4.1
    IL_003d:  add.ovf
    IL_003e:  stloc.2
    IL_003f:  ldloc.3
    IL_0040:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_0045:  brtrue.s   IL_003b
    IL_0047:  leave.s    IL_0053
  }  // end .try
  finally
  {
    IL_0049:  ldloc.3
    IL_004a:  brfalse.s  IL_0052
    IL_004c:  ldloc.3
    IL_004d:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0052:  endfinally
  }  // end handler
  IL_0053:  ldloc.2
  IL_0054:  ret
} // end of method Enumerable::Count
prabug
sumber
2

Berikut ini adalah diskusi yang bagus tentang evaluasi malas dan eksekusi yang ditangguhkan . Pada dasarnya Anda harus mematerialisasikan daftar untuk mendapatkan nilai itu.

JP Alioto
sumber
2

Hasil dari fungsi IEnumerable.Count () mungkin salah. Ini adalah sampel yang sangat sederhana untuk diuji:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;

namespace Test
{
  class Program
  {
    static void Main(string[] args)
    {
      var test = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 };
      var result = test.Split(7);
      int cnt = 0;

      foreach (IEnumerable<int> chunk in result)
      {
        cnt = chunk.Count();
        Console.WriteLine(cnt);
      }
      cnt = result.Count();
      Console.WriteLine(cnt);
      Console.ReadLine();
    }
  }

  static class LinqExt
  {
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int chunkLength)
    {
      if (chunkLength <= 0)
        throw new ArgumentOutOfRangeException("chunkLength", "chunkLength must be greater than 0");

      IEnumerable<T> result = null;
      using (IEnumerator<T> enumerator = source.GetEnumerator())
      {
        while (enumerator.MoveNext())
        {
          result = GetChunk(enumerator, chunkLength);
          yield return result;
        }
      }
    }

    static IEnumerable<T> GetChunk<T>(IEnumerator<T> source, int chunkLength)
    {
      int x = chunkLength;
      do
        yield return source.Current;
      while (--x > 0 && source.MoveNext());
    }
  }
}

Hasil harus (7,7,3,3) tetapi hasil aktualnya adalah (7,7,3,17)

Roman Golubin
sumber
1

Cara terbaik yang saya temukan adalah menghitung dengan mengubahnya menjadi daftar.

IEnumerable<T> enumList = ReturnFromSomeFunction();

int count = new List<T>(enumList).Count;
Abhas Bhoi
sumber
-1

Saya sarankan memanggil ToList. Ya, Anda melakukan enumerasi lebih awal, tetapi Anda masih memiliki akses ke daftar item Anda.

Jonathan Allen
sumber
-1

Ini mungkin tidak menghasilkan kinerja terbaik, tetapi Anda dapat menggunakan LINQ untuk menghitung elemen dalam IEnumerable:

public int GetEnumerableCount(IEnumerable Enumerable)
{
    return (from object Item in Enumerable
            select Item).Count();
}
Hugo
sumber
Apa bedanya dengan hanya melakukan "` return Enumerable.Count (); "?
ToolmakerSteve
Pertanyaan yang bagus, atau apakah itu sebenarnya jawaban untuk pertanyaan Stackoverflow ini.
Hugo
-1

Saya pikir cara termudah untuk melakukan ini

Enumerable.Count<TSource>(IEnumerable<TSource> source)

Referensi: system.linq.enumerable

Sowvik Roy
sumber
Pertanyaannya adalah "Hitung item dari IEnumerable <T> tanpa iterasi?" Bagaimana jawaban ini?
Enigmativity
-3

Saya menggunakan IEnum<string>.ToArray<string>().Lengthdan berfungsi dengan baik.

Oliver Kötter
sumber
Ini seharusnya bekerja dengan baik. IEnumerator <Object> .ToArray <Object> .Panjang
din
1
Mengapa Anda melakukan ini, alih-alih solusi yang lebih ringkas dan berkinerja lebih cepat yang sudah diberikan dalam jawaban Daniel yang sangat populer yang ditulis tiga tahun lebih awal dari jawaban Anda , " IEnum<string>.Count();"?
ToolmakerSteve
Kamu benar. Entah bagaimana saya mengabaikan jawaban Daniel, mungkin karena dia mengutip dari implementasi dan saya pikir dia menerapkan metode ekstensi dan saya sedang mencari solusi dengan kode lebih sedikit.
Oliver Kötter
Apa cara yang paling diterima untuk menangani jawaban buruk saya? Haruskah saya menghapusnya?
Oliver Kötter
-3

Saya menggunakan kode seperti itu, jika saya memiliki daftar string:

((IList<string>)Table).Count
Saya lapar
sumber