Mengapa .NET foreach loop melempar NullRefException saat koleksi bernilai nol?

231

Jadi saya sering mengalami situasi ini ... di mana Do.Something(...)mengembalikan koleksi nol, seperti:

int[] returnArray = Do.Something(...);

Lalu, saya mencoba menggunakan koleksi ini seperti ini:

foreach (int i in returnArray)
{
    // do some more stuff
}

Saya hanya ingin tahu, mengapa loop foreach tidak dapat beroperasi pada koleksi nol? Tampaknya logis bagi saya bahwa 0 iterasi akan dieksekusi dengan koleksi nol ... alih-alih melempar a NullReferenceException. Adakah yang tahu mengapa ini bisa terjadi?

Ini menjengkelkan karena saya bekerja dengan API yang tidak jelas persis apa yang mereka kembalikan, jadi saya berakhir di if (someCollection != null)mana - mana ...

Sunting: Terima kasih semua karena menjelaskan foreachkegunaan itu GetEnumeratordan jika tidak ada enumerator yang mendapatkan, foreach akan gagal. Saya kira saya bertanya mengapa bahasa / runtime tidak bisa atau tidak akan melakukan pemeriksaan nol sebelum meraih enumerator. Tampak bagi saya bahwa perilaku itu masih akan didefinisikan dengan baik.

Polaris878
sumber
1
Ada yang salah dengan memanggil array sebagai koleksi. Tapi mungkin aku hanya sekolah tua.
Robaticus
Ya, saya setuju ... Saya bahkan tidak yakin mengapa begitu banyak metode dalam array basis kode ini kembali x_x
Polaris878
4
Saya kira dengan alasan yang sama akan didefinisikan dengan baik untuk semua pernyataan dalam C # menjadi no-ops ketika diberi nullnilai. Apakah Anda menyarankan ini hanya untuk foreachperulangan atau pernyataan lain juga?
Ken
7
@ Ken ... Saya sedang memikirkan loop hanya foreach, karena bagi saya tampaknya jelas bagi programmer bahwa tidak ada yang akan terjadi jika koleksi kosong atau tidak ada
Polaris878

Jawaban:

251

Baiklah, jawaban singkatnya adalah "karena memang itulah yang dirancang oleh perancang kompiler." Namun, secara realistis, objek koleksi Anda adalah nol, jadi tidak ada cara bagi kompiler untuk membuat enumerator untuk mengulangi koleksi.

Jika Anda benar-benar perlu melakukan sesuatu seperti ini, coba operator penggabungan nol:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}
Robaticus
sumber
3
Maafkan ketidaktahuan saya, tetapi apakah ini efisien? Apakah itu tidak menghasilkan perbandingan pada setiap iterasi?
user919426
20
Saya tidak percaya begitu. Melihat IL yang dihasilkan, loop adalah setelah perbandingan nol.
Robaticus
10
Holy necro ... Kadang-kadang Anda harus melihat IL untuk melihat apa yang dilakukan kompiler untuk mencari tahu apakah ada efisiensi hit. User919426 telah bertanya apakah ia melakukan pengecekan untuk setiap iterasi. Meskipun jawabannya mungkin jelas bagi sebagian orang, itu tidak jelas bagi semua orang, dan memberikan petunjuk bahwa melihat IL akan memberi tahu Anda apa yang dikerjakan oleh penyusun, membantu orang menangkap ikan untuk diri mereka sendiri di masa depan.
Robaticus
2
@Robaticus (bahkan mengapa nanti) IL terlihat seperti itu karena spesifikasinya mengatakan demikian. Perluasan gula sintaksis (alias foreach) adalah untuk mengevaluasi ekspresi di sisi kanan "dalam" dan memanggil GetEnumeratorhasilnya
Rune FS
2
@RuneFS - tepatnya. Memahami spesifikasi atau melihat IL adalah cara untuk mencari tahu "mengapa." Atau untuk mengevaluasi apakah dua pendekatan C # yang berbeda bermuara pada IL yang sama. Pada dasarnya itu adalah poin saya untuk Shimmy di atas.
Robaticus
148

Sebuah foreachloop memanggil GetEnumeratormetode.
Jika pengumpulannya adalah null, pemanggilan metode ini menghasilkan a NullReferenceException.

Merupakan praktik yang buruk untuk mengembalikan nullkoleksi; metode Anda harus mengembalikan koleksi kosong.

Slaks
sumber
7
Saya setuju, koleksi kosong harus selalu dikembalikan ... namun saya tidak menulis metode ini :)
Polaris878
19
@Polaris, operator penggabungan nol untuk menyelamatkan! int[] returnArray = Do.Something() ?? new int[] {};
JSB ձոգչ
2
Atau: ... ?? new int[0].
Ken
3
+1 Suka ujung mengembalikan koleksi kosong, bukan nol. Terima kasih.
Galilyou
1
Saya tidak setuju tentang praktik buruk: lihat ⇒ jika suatu fungsi gagal dapat mengembalikan koleksi kosong - itu adalah panggilan untuk konstruktor, alokasi memori, dan mungkin banyak kode yang akan dieksekusi. Entah Anda bisa mengembalikan «null» → jelas hanya ada kode untuk kembali dan kode yang sangat singkat untuk diperiksa adalah argumennya adalah «null». Itu hanya kinerja.
Hi-Angel
47

Ada perbedaan besar antara koleksi kosong dan referensi nol untuk koleksi.

Ketika Anda menggunakan foreach, secara internal, ini memanggil metode GetEnumerator () dari IEnumerable . Ketika referensi nol, ini akan memunculkan pengecualian ini.

Namun, itu sangat sah untuk memiliki IEnumerableatau IEnumerable<T>. Dalam hal ini, foreach tidak akan "beralih" pada apa pun (karena koleksinya kosong), tetapi juga tidak akan melempar, karena ini adalah skenario yang benar-benar valid.


Edit:

Secara pribadi, jika Anda perlu mengatasinya, saya akan merekomendasikan metode ekstensi:

public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> original)
{
     return original ?? Enumerable.Empty<T>();
}

Anda kemudian dapat menghubungi:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}
Reed Copsey
sumber
3
Ya, tapi MENGAPA tidak melakukan cek sebelum mendapatkan enumerator?
Polaris878
12
@ Polaris878: Karena itu tidak pernah dimaksudkan untuk digunakan dengan koleksi nol. Ini, IMO, adalah hal yang baik - karena referensi nol dan koleksi kosong harus diperlakukan secara terpisah. Jika Anda ingin mengatasi ini, ada beberapa cara ... Saya akan mengedit untuk menampilkan satu opsi lain ...
Reed Copsey
1
@ Polaris878: Saya akan menyarankan untuk menulis ulang pertanyaan Anda: "Mengapa HARUS runtime melakukan pemeriksaan nol sebelum mendapatkan enumerator?"
Reed Copsey
Saya kira saya bertanya "mengapa tidak?" lol sepertinya perilaku itu masih akan didefinisikan dengan baik
Polaris878
2
@ Polaris878: Saya kira, seperti yang saya pikirkan, mengembalikan nol untuk koleksi adalah kesalahan. Seperti sekarang, runtime memberi Anda pengecualian yang berarti dalam kasus ini, tetapi mudah untuk ditangani (yaitu: di atas) jika Anda tidak menyukai perilaku ini. Jika kompiler menyembunyikan ini dari Anda, Anda akan kehilangan pengecekan kesalahan saat runtime, tetapi tidak akan ada cara untuk "mematikannya" ...
Reed Copsey
12

Ini sedang dijawab lama tetapi saya telah mencoba melakukan ini dengan cara berikut untuk hanya menghindari pengecualian null pointer dan mungkin berguna bagi seseorang yang menggunakan operator cek C # null?

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });
Devesh
sumber
@ kjbartel mengalahkan Anda dalam hal ini lebih dari setahun (di " stackoverflow.com/a/32134295/401246 "). ;) Ini adalah solusi terbaik, karena tidak: a) melibatkan penurunan kinerja (bahkan ketika tidak null) menggeneralisasi seluruh loop ke LCD Enumerable(seperti menggunakan ??), b) memerlukan penambahan Metode Perpanjangan untuk setiap Proyek, dan c) memerlukan penghindaran null IEnumerable(Pffft! Puh-LEAZE! SMH.) untuk memulainya.
Tom
10

Metode ekstensi lain untuk mengatasi ini:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
    if(items == null) return;
    foreach (var item in items) action(item);
}

Konsumsi dalam beberapa cara:

(1) dengan metode yang menerima T:

returnArray.ForEach(Console.WriteLine);

(2) dengan ekspresi:

returnArray.ForEach(i => UpdateStatus(string.Format("{0}% complete", i)));

(3) dengan metode anonim multiline

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});
Jay
sumber
Baru saja melewatkan tanda kurung penutup pada contoh ke-3. Kalau tidak, kode indah yang dapat diperluas lebih lanjut dengan cara yang menarik (untuk loop, membalik, melompat, dll). Terima kasih telah berbagi.
Lara
Terima kasih untuk kode yang luar biasa, Tapi saya tidak mengerti metode pertama, mengapa Anda lulus console.writeline sebagai parameter, meskipun itu mencetak elemen array.tapi tidak mengerti
Ajay Singh
@AjaySingh Console.WriteLinehanyalah sebuah contoh metode yang mengambil satu argumen (an Action<T>). Item 1, 2 dan 3 menunjukkan contoh fungsi yang lewat ke .ForEachmetode ekstensi.
Jay
@ kjbartel jawaban (di " stackoverflow.com/a/32134295/401246 " adalah solusi terbaik, karena itu tidak: a) melibatkan penurunan kinerja (bahkan ketika tidak null) menggeneralisasi seluruh loop ke LCD Enumerable(seperti menggunakan ??akan ), b) memerlukan penambahan Metode Ekstensi untuk setiap Proyek, atau c) mengharuskan untuk menghindari null IEnumerables (Pffft! Puh-LEAZE! SMH.) untuk memulai dengan (cuz, nullberarti N / A, sedangkan daftar kosong berarti, aplikasi itu. tetapi saat ini, yah, kosong !, yaitu seorang Empl. dapat memiliki Komisi yang N / A untuk non-Penjualan atau kosong untuk Penjualan).
Tom
5

Cukup tulis metode ekstensi untuk membantu Anda:

public static class Extensions
{
   public static void ForEachWithNull<T>(this IEnumerable<T> source, Action<T> action)
   {
      if(source == null)
      {
         return;
      }

      foreach(var item in source)
      {
         action(item);
      }
   }
}
BFree
sumber
5

Karena koleksi nol tidak sama dengan koleksi kosong. Koleksi kosong adalah objek koleksi tanpa elemen; koleksi nol adalah objek yang tidak ada.

Ini ada sesuatu yang bisa dicoba: Nyatakan dua koleksi apa pun. Inisialisasi satu secara normal sehingga kosong, dan tetapkan nilainya yang lain null. Kemudian coba tambahkan objek ke kedua koleksi dan lihat apa yang terjadi.

TUSUKAN
sumber
3

Itu adalah kesalahan Do.Something(). Praktik terbaik di sini adalah mengembalikan array ukuran 0 (itu mungkin) daripada nol.

Henk Holterman
sumber
2

Karena di belakang layar yang foreachmemperoleh enumerator, setara dengan ini:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}
Lucero
sumber
2
begitu? Mengapa tidak bisa hanya memeriksa apakah itu null terlebih dahulu dan lewati loop? AKA, tepatnya apa yang ditampilkan dalam metode ekstensi? Pertanyaannya adalah, apakah lebih baik default untuk melewatkan loop jika nol atau untuk melemparkan pengecualian? Saya pikir lebih baik untuk melewati! Tampaknya wadah nol dimaksudkan untuk dilompati bukan diulang karena loop dimaksudkan untuk melakukan sesuatu JIKA wadah tersebut bukan nol.
AbstractDissonance
@AbstractDissonance Anda bisa berdebat sama dengan semua nullreferensi, misalnya ketika mengakses anggota. Biasanya ini adalah kesalahan, dan jika tidak maka cukup sederhana untuk menangani ini misalnya dengan metode ekstensi yang diberikan pengguna lain sebagai jawaban.
Lucero
1
Saya kira tidak. Foreach dimaksudkan untuk beroperasi di atas koleksi dan berbeda dari referensi objek nol secara langsung. Sementara orang bisa berpendapat yang sama, saya yakin jika Anda menganalisis semua kode di dunia, Anda akan memiliki sebagian besar loop foreach memiliki cek nol dari beberapa jenis di depan mereka hanya untuk memotong loop ketika koleksi "null" (yang merupakan karenanya diperlakukan sama dengan kosong). Saya tidak berpikir siapa pun menganggap perulangan koleksi nol sebagai sesuatu yang mereka inginkan dan lebih baik mengabaikan loop jika koleksi tersebut nol. Mungkin, lebih tepatnya, foreach? (Var x dalam C) bisa digunakan.
AbstractDissonance
Maksud saya terutama berusaha membuat adalah bahwa ia menciptakan sedikit sampah dalam kode karena kita harus memeriksa setiap waktu tanpa alasan yang baik. Ekstensi, tentu saja, berfungsi tetapi fitur bahasa dapat ditambahkan untuk menghindari hal-hal ini tanpa banyak masalah. (terutama saya pikir metode saat ini menghasilkan bug tersembunyi karena programmer mungkin lupa untuk meletakkan cek dan karenanya pengecualian ... karena ia mengharapkan cek terjadi di tempat lain sebelum loop atau berpikir bahwa itu sudah diinisialisasi (yang mungkin atau mungkin telah berubah) Tapi dalam kedua penyebab, perilaku akan sama seperti jika kosong..
AbstractDissonance
@AbstractDissonance Nah, dengan beberapa analisis statis yang tepat Anda tahu di mana Anda bisa memiliki nol dan di mana tidak. Jika Anda mendapatkan nol di mana Anda tidak mengharapkan yang lebih baik gagal daripada diam-diam mengabaikan masalah IMHO (dengan semangat gagal cepat ). Karena itu saya merasa bahwa ini adalah perilaku yang benar.
Lucero
1

Saya pikir penjelasan mengapa pengecualian dilemparkan sangat jelas dengan jawaban yang diberikan di sini. Saya hanya ingin melengkapi dengan cara saya biasanya bekerja dengan koleksi ini. Karena, beberapa kali, saya menggunakan koleksi lebih dari sekali dan harus menguji apakah nol setiap kali. Untuk menghindarinya, saya melakukan hal berikut:

    var returnArray = DoSomething() ?? Enumerable.Empty<int>();

    foreach (int i in returnArray)
    {
        // do some more stuff
    }

Dengan cara ini kita dapat menggunakan koleksi sebanyak yang kita inginkan tanpa takut pengecualian dan kita tidak mencemari kode dengan pernyataan kondisional berlebihan.

Menggunakan operator cek nol ?.juga merupakan pendekatan yang bagus. Tetapi, dalam hal array (seperti contoh dalam pertanyaan), harus diubah menjadi Daftar sebelumnya:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });
Alielson Piffer
sumber
2
Mengkonversi ke daftar hanya untuk memiliki akses ke ForEachmetode adalah salah satu hal yang saya benci dalam basis kode.
huysentruitw
Saya setuju ... Saya menghindari itu sebanyak mungkin. :(
Alielson Piffer
-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Naveen Baabu K
sumber