Koleksiyon null olduğunda .NET foreach döngüsü neden NullRefException oluşturur?


231

Bu yüzden sık sık bu duruma girerim ... burada Do.Something(...)null bir koleksiyon döndürür, şöyle:

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

Sonra, bu koleksiyonu böyle kullanmaya çalışıyorum:

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

Sadece merak ediyorum, bir foreach döngüsü null koleksiyonda neden çalışamıyor? 0 yinelemenin boş bir koleksiyonla yürütülmesi mantıklı görünüyor ... bunun yerine a NullReferenceException. Bunun neden olabileceğini bilen var mı?

Tam olarak ne döndüklerini açık olmayan API'lerle çalıştığım için bu can sıkıcı, bu yüzden if (someCollection != null)her yerde bitirdim ...

Düzenleme: Bu foreachkullanımları açıklamak için hepinize teşekkür ederiz GetEnumeratorve almak için bir numaralandırıcı yoksa, foreach başarısız olur. Sanırım, numaralandırıcıyı yakalamadan önce dilin / çalışma zamanının neden boş bir denetim yapamayacağını veya yapmayacağını soruyorum. Bana öyle geliyor ki davranış hala iyi tanımlanmış olacak.


1
Bir diziyi koleksiyon olarak adlandırırken bir şeyler yanlış geliyor. Ama belki de sadece eski okulum.
Robaticus

Evet, katılıyorum ... Neden bu kod tabanındaki birçok yöntemin neden diziler
x_x

4
Aynı akıl yürütme ile bir değer verildiğinde no-ops olmak C # tüm ifadeler için iyi tanımlanmış olacağını varsayalım null. Bunu sadece foreachdöngüler veya diğer ifadeler için mi öneriyorsunuz ?
Ken

7
@ Ken ... Ben sadece foreach döngüleri düşünüyorum, çünkü bana göre programcı için koleksiyon boş veya mevcut değilse hiçbir şey olmayacak gibi görünüyor
Polaris878

Yanıtlar:


251

Kısa cevap: "çünkü derleyici tasarımcıları bunu tasarladı." Gerçekçi olarak, toplama nesneniz boştur, bu nedenle derleyicinin numaralandırıcının koleksiyonda döngü yapmasını sağlamanın bir yolu yoktur.

Gerçekten böyle bir şey yapmanız gerekiyorsa, boş birleştirme operatörünü deneyin:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}

3
Lütfen cehaletimi affedin, ama bu etkili mi? Her yinelemede bir karşılaştırma ile sonuçlanmaz mı?
user919426

20
Ben inanmıyorum. Oluşturulan IL'ye bakıldığında, döngü null karşılaştırmasından sonradır.
Robaticus

10
Holy necro ... Bazen, derleyicinin herhangi bir verimlilik isabeti olup olmadığını anlamak için ne yaptığını görmek için IL'ye bakmanız gerekir. User919426 her yineleme için kontrolü yapıp yapmadığını sormuştu. Her ne kadar cevap bazı insanlar için açık olsa da, herkes için açık değildir ve IL'ye bakmanın derleyicinin ne yaptığını size söyleyeceği ipucu vermek, insanların gelecekte kendileri için balık tutmasına yardımcı olur.
Robaticus

2
@Robaticus (neden daha sonra olsa bile), IL spesifikasyonun söylediği için neden böyle görünüyor. Sözdizimsel şekerin genişlemesi (diğer adıyla foreach) "giriş" in sağ tarafındaki ifadeyi değerlendirmek ve çağrı yapmaktır.GetEnumerator ve sonucu
Rune FS

2
@RuneFS - tam olarak. Spesifikasyonu anlamak veya IL'ye bakmak "nedenini" anlamanın bir yoludur. Ya da iki farklı C # yaklaşımının aynı IL'ye kaynamış olup olmadığını değerlendirmek. Esasen yukarıdaki Shimmy'ye olan amacım buydu.
17'de Robaticus

148

Bir foreachdöngü GetEnumeratoryöntemi çağırır .
Koleksiyon ise null, bu yöntem çağrısı a ile sonuçlanır NullReferenceException.

Bir nullkoleksiyonu iade etmek kötü bir uygulamadır ; yöntemleriniz bunun yerine boş bir koleksiyon döndürmelidir.


7
Katılıyorum, boş koleksiyonlar her zaman iade edilmelidir ... ancak bu yöntemleri
yazmadım

19
@Polaris, kurtarmaya null birleştirme operatörü! int[] returnArray = Do.Something() ?? new int[] {};
JSB ձոգչ

2
Veya: ... ?? new int[0].
Ken

3
+1 Boş koleksiyonları null yerine döndürmenin ucu gibi. Teşekkürler.
Galilyou

1
Kötü bir uygulama hakkında katılmıyorum: ⇒ bir işlev başarısız olursa, boş bir koleksiyon döndürebilir - bu yapıcıya bir çağrı, bellek ayırma ve belki de çalıştırılacak bir kod grubudur. Ya sadece "boş" döndürebilirsiniz → açık bir şekilde sadece döndürülecek bir kod vardır ve argümanın "boş" olup olmadığını kontrol etmek için çok kısa bir kod vardır. Bu sadece bir performans.
Hi-Angel

47

Boş bir koleksiyon ile koleksiyona null referans arasında büyük bir fark vardır.

foreachDahili olarak kullandığınızda , bu IEnumerable'ın GetEnumerator () yöntemini çağırır . Referans null olduğunda, bu kural dışı durum ortaya çıkar.

Ancak, boş IEnumerableveya IEnumerable<T>. Bu durumda, foreach hiçbir şey üzerinde "yinelenmez" (koleksiyon boş olduğu için), ama aynı zamanda atmayacaktır, çünkü bu tamamen geçerli bir senaryodur.


Düzenle:

Kişisel olarak, bu soruna geçici bir çözüm bulmanız gerekirse, bir uzantı yöntemi öneririm:

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

Ardından şunları arayabilirsiniz:

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

3
Evet, ancak numaralandırıcıyı almadan önce NEDEN öngörü denetimi yapmıyor?
Polaris878

12
@ Polaris878: Çünkü asla boş bir koleksiyonla kullanılmak üzere tasarlanmamıştı. Bu, IMO, iyi bir şey - çünkü boş bir başvuru ve boş bir koleksiyon ayrı ayrı ele alınmalıdır. Bu soruna geçici bir çözüm bulmak istiyorsanız, başka yollar da var ...
Reed Copsey

1
@ Polaris878: Sorunuzu yeniden düzenlemenizi öneririm: "Numaralandırıcıyı almadan önce çalışma zamanının neden boş bir kontrol yapması GEREKİR?"
Reed Copsey

Sanırım "neden olmasın?" lol davranış hala iyi tanımlanmış gibi görünüyor
Polaris878

2
@ Polaris878: Sanırım, benim düşündüğüm gibi, bir koleksiyon için boş değer döndürmek bir hatadır. Şimdi olduğu gibi, çalışma zamanı size bu durumda anlamlı bir istisna verir, ancak bu davranışı beğenmezseniz (yani: yukarıda) kolayca çalışabilirsiniz. Derleyici bunu sizden gizlediyse, çalışma zamanında hata denetimini kaybedersiniz, ancak "kapatmanın" hiçbir yolu olmaz ...
Reed Copsey

12

Bu uzun geri cevap ama ben sadece boş işaretçi istisna önlemek için aşağıdaki şekilde yapmaya çalıştım ve C # null kontrol operatörü kullanan biri için yararlı olabilir?.

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });

@kjbartel sizi bir yılı aşkın bir süredir (" stackoverflow.com/a/32134295/401246 " adresinde ). ;) Bu en iyi çözümdür, çünkü aşağıdakileri yapmaz: a) nulltüm döngüyü LCD'ye genelleştirmek Enumerable( ??kullanacağı gibi ) genel olarak performans düşüşünü (olmasa bile) içermez , b) her projeye bir Genişletme Yöntemi eklenmesini gerektirir, ve c) null IEnumerablebaşlamak için s'den (Pffft! Puh-LEAZE! SMH.) kaçınılması gerekir .
Tom

10

Bu soruna geçici bir çözüm bulmak için başka bir uzantı yöntemi:

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

Birkaç şekilde tüketin:

(1) aşağıdakileri kabul eden bir yöntemle T:

returnArray.ForEach(Console.WriteLine);

(2) şu ifadeyle:

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

(3) çok satırlı anonim bir yöntemle

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});

3. örnekte kapanış parantezi eksik. Aksi takdirde, ilginç yollarla daha fazla genişletilebilen güzel kod (döngüler, tersine çevirme, sıçramak, vb.). Paylaşım için teşekkürler.
Lara

Böyle harika bir kod için teşekkürler, ama ilk yöntemleri anlamadım, neden dizi elemanlarını yazdırmasına rağmen console.writeline parametresini geçtiniz, ama anlamadı
Ajay Singh

@AjaySingh Console.WriteLinesadece bir argüman (an Action<T>) alan bir yöntem örneğidir . Maddeler 1, 2 ve 3, fonksiyonların .ForEachgenişletme yöntemine geçme örneklerini göstermektedir .
Jay

@ kjbartel'in yanıtı (" stackoverflow.com/a/32134295/401246 " adresinde en iyi çözümdür, çünkü aşağıdakileri yapmaz: a) nulltüm döngüyü LCD'ye genelleştirmek Enumerable( ??kullanacağı gibi ) performans düşüşünü içerir ), b) her Projeye bir Genişletme Yöntemi eklenmesini gerektirir veya c) null IEnumerables (Pffft! Puh-LEAZE! SMH.) ile başlamaktan kaçınmayı gerektirir (cuz, nullN / A anlamına gelir, oysa boş liste anlamlıdır , ancak şu anda, boş , boş ! yani, bir Çalışan, Satış dışı için N / A veya Satış için boş olan Komisyonlara sahip olabilir).
Tom

5

Size yardımcı olacak bir uzantı yöntemi yazmanız yeterlidir:

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

5

Çünkü null bir koleksiyon, boş bir koleksiyonla aynı şey değildir. Boş koleksiyon öğeleri olmayan bir toplama nesnesidir; null koleksiyonu, var olmayan bir nesnedir.

İşte denenecek bir şey: Herhangi bir türden iki koleksiyon ilan edin. Birini boş olacak şekilde normal olarak başlatın ve diğerine değeri atayın null. Ardından her iki koleksiyona bir nesne eklemeyi deneyin ve ne olduğunu görün.


3

Bunun hatası Do.Something(). Buradaki en iyi uygulama null yerine 0 boyutunda (mümkün) bir dizi döndürmektir.


2

Çünkü perde arkasında buna foreacheşit bir numaralandırıcı kazanır:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}

2
yani? Neden önce boş olup olmadığını kontrol edip döngüyü atlamıyor? AKA, uzatma yöntemlerinde tam olarak ne gösteriliyor? Soru, boşsa döngüyü atlamak veya bir istisna atmak için varsayılan olarak daha iyi mi? Bence atlamak daha iyi! Döngüler , kap null değilse EĞER bir şey yapmayı amaçladığından, boş kapların ilmek yerine değil atlanması gerektiği anlaşılıyor .
AbstractDissonance

@AbstractDissonance Örneğe nullerişirken tüm referanslarla aynı şeyi tartışabilirsiniz . Genellikle bu bir hatadır ve eğer değilse, bunu örneğin başka bir kullanıcının yanıt olarak sağladığı uzantı yöntemiyle işlemek için yeterince basittir.
Lucero

1
Ben öyle düşünmüyorum. Foreach, koleksiyon üzerinde çalışmak içindir ve boş bir nesneye doğrudan başvurmaktan farklıdır. Biri aynı tartışabilir olsa da, eğer dünyadaki tüm kodu analiz ettiyseniz, en çok foreach döngüsünün önünde sadece koleksiyon "null" olduğunda döngüyü atlamak için önlerinde null kontrolleri olurdu dolayısıyla boşla aynı şekilde işlem görür). Kimsenin istediği bir şey olarak null bir koleksiyon üzerinde döngü olarak düşündüğü ve koleksiyon null ise basitçe döngü görmezden olacağını sanmıyorum. Belki daha ziyade, bir önsöz? (C cinsinden x) kullanılabilir.
AbstractDissonance

Esasen yapmaya çalıştığım nokta, kodun bir miktar çöp oluşturması, çünkü her seferinde iyi bir sebep için kontrol etmem gerekiyor. Uzantılar, elbette, bu şeylerden kaçınmak için çok fazla sorun yaşamadan bir dil özelliği eklenebilir. (esas olarak, mevcut yöntem gizli hatalar ürettiğini düşünüyorum çünkü programcı çek ve dolayısıyla bir istisna koymayı unutabilir ... çünkü ya kontrolün döngüden önce başka bir yerde gerçekleşmesini beklediğini veya ( Ama her iki durumda da, davranış
AbstractDissonance

@AbstractDissonance Bazı uygun statik analizlerle nerede null alabileceğinizi ve neredeyeceğinizi biliyorsunuz. Eğer birini beklemediğiniz bir yerde boş kalırsanız, problemleri sessizce görmezden gelmek yerine başarısız olmak daha iyidir IMHO ( hızlı başarısız olma ruhuyla ). Bu yüzden bunun doğru davranış olduğunu hissediyorum.
Lucero

1

İstisnaların neden atıldığının açıklaması burada verilen cevaplarla çok açık. Sadece bu koleksiyonlarla çalışma şeklimizi tamamlamak istiyorum. Çünkü, bazen, koleksiyonu bir kereden fazla kullanıyorum ve her seferinde boş olup olmadığını test etmem gerekiyor. Bundan kaçınmak için aşağıdakileri yaparım:

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

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

Bu şekilde, koleksiyonu istisnasız korkmadan istediğimiz kadar kullanabiliriz ve kodu aşırı koşullu ifadelerle kirletmeyiz.

Boş kontrol operatörünü ?.kullanmak da harika bir yaklaşımdır. Ancak, diziler söz konusu olduğunda (sorudaki örnek gibi), daha önce List'e dönüştürülmelidir:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });

2
Sadece bir ForEachyönteme erişmek için bir listeye dönüştürmek , bir kod tabanında nefret ettiğim şeylerden biridir.
huysentruitw

Katılıyorum ... Bundan mümkün olduğunca kaçıyorum. :(
Alielson Piffer

-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.