コレクションがnullの場合、.NET foreachループがNullRefExceptionをスローするのはなぜですか?


231

だから私はこの状況に頻繁に遭遇しDo.Something(...)ます...そこでは次のようにnullコレクションを返します:

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

次に、このコレクションを次のように使用してみます。

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

私はただ知りたいのですが、なぜforeachループがnullコレクションで動作しないのですか?反復が0の場合、nullコレクションで実行されるのは理にかなっているようです。代わりにがスローされますNullReferenceException。なぜこれが起こり得るのか誰でも知っていますか?

APIが何を返すのかが明確でないAPIを使用しているので、これは面倒ですif (someCollection != null)

編集:foreach使用して説明していただきありがとうございますGetEnumerator。取得する列挙子がない場合、foreachは失敗します。言語/ランタイムが列挙子を取得する前にnullチェックを実行できない、または実行できない理由を私は尋ねていると思います。動作はまだ明確に定義されているように思えます。


1
配列をコレクションと呼ぶことに何かがおかしいと感じます。しかし、たぶん私はただの古い学校です。
Robaticus 2010年

はい、同意します...このコードベースの多くのメソッドが配列x_xを返す理由がよくわかりません
Polaris878

4
同じ理由で、C#のすべてのステートメントがnull値を指定されたときに何も実行しないことが明確に定義されていると思います。foreachループやその他のステートメントについてもこれを提案していますか?
Ken

7
@ケン... foreachループだけを考えています。プログラマにとって、コレクションが空または存在しない場合は何も起こらないことは明らかです
Polaris878

回答:


251

簡単に言えば、「コンパイラの設計者がそのように設計したからです」。ただし、現実的には、コレクションオブジェクトはnullであるため、コンパイラーが列挙子にコレクションをループ処理する方法はありません。

本当にこのようなことをする必要がある場合は、null合体演算子を試してください:

int[] array = null;

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

3
私の無知を許してください、しかしこれは効率的ですか?各反復で比較が行われませんか?
user919426 2016年

20
私はそうは思いません。生成されたILを見ると、ループはis null比較の後にあります。
Robaticus

10
神聖なネクロ...効率に影響があるかどうかを判断するためにコンパイラーが何をしているかを確認するために、ILを調べる必要がある場合があります。User919426は、反復ごとにチェックを行うかどうかを尋ねていました。答えは一部の人にとっては明白かもしれませんが、誰にとっても明白ではありません。ILを見るとコンパイラが何をしているのかを知ることができるというヒントを提供することは、人々が将来自分で釣りをするのに役立ちます。
ロバティカス2017年

2
@Robaticus(なぜそれが後であるのか)は、仕様がそう言っているので、ILがその理由に見える 構文糖(別名foreach)の拡張は、「in」の右側の式を評価GetEnumeratorし、結果を要求することです
Rune FS

2
@RuneFS-まさに。仕様を理解したり、ILを調べたりすることは、「理由」を理解する方法の1つです。または、2つの異なるC#アプローチが同じILに煮詰められるかどうかを評価します。それは本質的に、上のシミーへの私のポイントでした。
Robaticus 2017年

148

foreachループが呼び出すGetEnumeratorメソッドを。
コレクションがの場合null、このメソッド呼び出しはNullReferenceException

nullコレクションを返すことは悪い習慣です。代わりに、メソッドは空のコレクションを返す必要があります。


7
私は同意します。空のコレクションは常に返されるべきです...しかし、これらのメソッドを記述しませんでした:)
Polaris878

19
@ Polaris、nullの合体演算子で救助!int[] returnArray = Do.Something() ?? new int[] {};
JSBձոգչ2010年

2
または:... ?? new int[0]
Ken

3
+1 nullの代わりに空のコレクションを返すヒントと同様です。ありがとう。
Galilyou、2011

1
私は悪い習慣に同意しません:⇒を参照してください。関数が失敗した場合、空のコレクションを返す可能性があります—これは、コンストラクターへの呼び出し、メモリ割り当て、およびおそらく実行されるコードの束です。単に«null»を返すこともできます。→返されるコードだけがあり、チェックする非常に短いコードは引数が«null»であるかどうかです。それは単なるパフォーマンスです。
Hi-Angel

47

空のコレクションとコレクションへのnull参照の間には大きな違いがあります。

foreach内部でを使用すると、これはIEnumerableのGetEnumerator()メソッドを呼び出します。参照がnullの場合、この例外が発生します。

ただし、IEnumerableまたはを空にすることは完全に有効ですIEnumerable<T>。この場合、foreachは(コレクションが空であるため)何も「反復」しませんが、これは完全に有効なシナリオであるため、スローしません。


編集:

個人的には、これを回避する必要がある場合は、拡張メソッドをお勧めします。

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

その後、単に呼び出すことができます:

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

3
はい、しかしなぜ列挙子を取得する前にnullのチェックをforeachしないのですか?
Polaris878 2010年

12
@ Polaris878:nullコレクションでの使用を意図したものではないため。これはIMOです。null参照と空のコレクションは別々に処理する必要があるためです。これを回避したい場合は、いくつかの方法があります。他の1つのオプションを表示するように編集します...
Reed Copsey

1
@ Polaris878:私はあなたの質問を言い換えることを提案します:「なぜランタイムは列挙子を取得する前にnullチェックを行う必要があるのですか?」
リードコプシー2010年

私は「なぜそうしないのですか」と尋ねていると思います。笑動作はまだ明確に定義されているようです
Polaris878

2
@ Polaris878:私が思うに、コレクションに対してnullを返すのはエラーだと思います。現在の状態では、ランタイムはこの場合に意味のある例外を提供しますが、この動作が気に入らない場合は簡単に回避できます(つまり、上記)。コンパイラがこれを隠した場合、実行時にエラーチェックが失われますが、「オフにする」方法はありません...
Reed Copsey

12

それは長い間答えられていますが、私は次の方法でこれを実行して、nullポインタ例外を回避するだけにしました。

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

@kjbartelはこれに1年以上も負けました(「stackoverflow.com/a/32134295/401246」)。;)これは最善の解決策です。それは次のことを行わないためです:a)(そうでない場合でもnull)ループ全体をLCDに一般化するEnumerable(そうすることを使用して)のパフォーマンス低下を伴う??、b)すべてのプロジェクトに拡張メソッドを追加する必要がある、およびc)null IEnumerableまず、s(Pffft!Puh-LEAZE!SMH。)を回避する必要があります。
トム

10

これを回避する別の拡張メソッド:

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

いくつかの方法で消費します:

(1)を受け入れるメソッドT

returnArray.ForEach(Console.WriteLine);

(2)式:

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

(3)複数行匿名メソッド

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

3番目の例では、閉じかっこがありません。そうでなければ、興味深い方法(ループ、逆転、跳躍など)でさらに拡張できる美しいコード。共有いただきありがとうございます。
ララ

そのような素晴らしいコードをありがとう、しかし、最初の方法を理解できませんでした。なぜ、console.writelineをパラメーターとして渡すのですか?配列要素を出力しますが、理解できませんでした
Ajay Singh

@AjaySingh Console.WriteLineは、1つの引数(an Action<T>)を取るメソッドのほんの一例です。1、2、3は.ForEach拡張メソッドに関数を渡す例です。
ジェイ

@ kjbartelの答え(「でstackoverflow.com/a/32134295/401246それはしていないため」、最適なソリューションです:)(さえないのパフォーマンスの低下を伴うnull)のLCDにループ全体を一般Enumerable使用として(??うを)、b)すべてのプロジェクトに拡張メソッドを追加する必要がある、またはc)null IEnumerables(Pffft!Puh-LEAZE!SMH。)で(cuz nullはN / Aを意味するが、空のリストはそれがアプリケーションであることを意味するが、現在、まあ、空の!、つまり、Empl。は非セールスの場合はN / A、セールスの場合は空のコミッションを持つことができます。
トム

5

あなたを助ける拡張メソッドを書くだけです:

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

nullコレクションは空のコレクションと同じではないためです。空のコレクションは、要素のないコレクションオブジェクトです。nullコレクションは存在しないオブジェクトです。

ここで試してみましょう:任意の種類の2つのコレクションを宣言します。通常は一方を初期化して空にし、もう一方に値を割り当てますnull。次に、両方のコレクションにオブジェクトを追加して、何が起こるかを確認してください。



2

舞台裏でforeachは、これは列挙子を取得するため、これと同等です。

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

2
そう?最初にnullかどうかを確認してループをスキップできないのはなぜですか?別名、拡張メソッドで正確に何が示されていますか?問題は、nullの場合はデフォルトでループをスキップするか、例外をスローするほうが良いですか?スキップした方がいいと思います!ループは何かを意味しているので、ヌルコンテナをスキップではなく、オーバーループすることを意味していると思わIFコンテナが非nullです。
AbstractDissonance

@AbstractDissonance nullメンバーにアクセスするときなど、すべての参照で同じことを主張できます。通常、これはエラーであり、そうでない場合は、たとえば、別のユーザーが回答として提供した拡張メソッドを使用してこれを処理するのに十分簡単です。
Lucero、2016

1
そうは思いません。foreachはコレクションを操作するためのもので、nullオブジェクトを直接参照するのとは異なります。同じことを主張することもできますが、世界中のすべてのコードを分析した場合、コレクションの「null」(つまり、したがって、空と同じように扱われます)。私は誰もがnullコレクションをループすることを望んでいるものとは考えておらず、コレクションがnullの場合はループを無視するだけだと思います。たぶん、むしろ、foreach?(Cのvar x)を使用できます。
AbstractDissonance

私が主にしようとしている点は、正当な理由なしに毎回チェックする必要があるため、コードに少しごみを作成することです。もちろん、拡張機能は機能しますが、言語機能を追加して、これらの問題をさほど問題なく回避することができます。(主に現在の方法では、プログラマーがチェックを入れ忘れて例外が発生する可能性があるため、隠れたバグが生成されると思います...ループの前にどこかでチェックが発生することを期待しているか、事前に初期化されていると考えているためです(これは変更される場合と変更された場合があります。ただし、どちらの原因でも、動作は空の場合と同じです
AbstractDissonance

@AbstractDissonanceまあ、いくつかの適切な静的分析があれば、nullが存在する可能性がある場所とできない場所がわかります。期待しない場所でnullを取得した場合は、問題を黙って無視するのではなく、失敗するほうがよい(高速失敗するという精神で)。したがって、これは正しい動作だと思います。
Lucero、2016

1

例外がスローされる理由の説明は、ここに提供されている回答で非常に明確だと思います。私は通常、これらのコレクションで作業する方法を補足したいだけです。時々、コレクションを複数回使用し、毎回nullかどうかをテストする必要があるためです。それを避けるために、私は以下を行います:

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

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

このようにして、例外を恐れずにコレクションを好きなだけ使用でき、過度の条件ステートメントでコードを汚すことはありません。

nullチェック演算子を使用すること?.も優れたアプローチです。ただし、配列の場合(問題の例のように)、それは前にListに変換する必要があります。

    int[] returnArray = DoSomething();

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

2
ForEachメソッドにアクセスするためだけにリストに変換することは、コードベースで嫌いなことの1つです。
huysentruitw 2017

同意します...できるだけ避けます。:(
Alielson Piffer 2017

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

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.