web-dev-qa-db-ja.com

foreachのヌル合体演算子IList、Array、Enumerable.Empty

この質問 で次のことがわかりました。

int[] array = null;

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

そして

int[] returnArray = Do.Something() ?? new int[] {};

そして

... ?? new int[0]

NotifyCollectionChangedEventHandlerでは、Enumerable.Emptyを次のように適用したいと思います。

foreach (DrawingPoint drawingPoint in e.OldItems ?? Enumerable.Empty<DrawingPoint>())
    this.RemovePointMarker(drawingPoint);

注:OldItemsは次のタイプですIList

そしてそれは私に与えます:

演算子 '??'タイプ「System.Collections.IList」およびSystem.Collections.Generic.IEnumerable<DrawingPoint>のオペランドには適用できません

しかしながら

foreach (DrawingPoint drawingPoint in e.OldItems ?? new int[0])

そして

foreach (DrawingPoint drawingPoint in e.OldItems ?? new int[] {})

正常に動作します。

何故ですか?
IList ?? T[]が機能するのにIList ?? IEnumerable<T>が機能しないのはなぜですか?

23
IDarkCoder

この式を使用する場合:

a ?? b

次に、bは、aと同じ型であるか、その型に暗黙的にキャスト可能である必要があります。つまり、参照を使用すると、aの型を実装または継承する必要があります。

これらの作業:

SomethingThatIsIListOfT ?? new T[0]
SomethingThatIsIListOfT ?? new T[] { }

T[]isIList<T>であるため、配列型はそのインターフェイスを実装します。

ただし、これは機能しません。

SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

式のタイプはaタイプであり、コンパイラはSomethingThatImplementsIEnumerableOfTIList<T>も実装していることを保証できないことは明らかです。

互換性のあるタイプになるように、2つの面のいずれかをキャストする必要があります。

(IEnumerable<T>)SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

これで、式のタイプはIEnumerable<T>になり、??演算子で処理できます。


「式のタイプはaのタイプになります」は少し簡略化されており、仕様の全文は次のとおりです。


a ?? bのタイプは、オペランドで使用できる暗黙の変換によって異なります。優先順に、a ?? bのタイプはA0A、またはBです。ここで、Aは(aにタイプがある場合)、Bbのタイプ(提供されるbには型があります)、A0Aがnull許容型である場合はAの基本型であり、それ以外の場合はAです。具体的には、a ?? bは次のように処理されます。

  • Aが存在し、null許容型または参照型ではない場合、コンパイル時エラーが発生します。
  • bが動的式の場合、結果のタイプは動的です。実行時に、aが最初に評価されます。 anullでない場合、aは動的型に変換され、これが結果になります。それ以外の場合、bが評価され、結果が結果になります。
  • それ以外の場合、Aが存在し、それがnull許容型であり、bからA0への暗黙の変換が存在する場合、結果の型はA0です。実行時に、aが最初に評価されます。 anullでない場合、aA0型にラップ解除され、結果になります。それ以外の場合、bが評価され、タイプA0に変換され、結果になります。
  • それ以外の場合、Aが存在し、bからAへの暗黙の変換が存在する場合、結果の型はAです。実行時に、aが最初に評価されます。 aがnullでない場合、aが結果になります。それ以外の場合、bが評価され、A型に変換され、結果になります。
  • それ以外の場合、bB型があり、aからBへの暗黙の変換が存在する場合、結果の型はBになります。実行時に、aが最初に評価されます。 anullでない場合、aはタイプA0にラップ解除され(Aが存在し、null可能である場合)、タイプBに変換され、結果になります。それ以外の場合、bが評価され、結果になります。
  • それ以外の場合、abは互換性がなく、コンパイル時エラーが発生します。
20

_System.Collections.IList_演算子のオペランドとして、ジェネリック_System.Collections.Generic.IEnumerable<>_とともにジェネリック_??_を使用しています。どちらのインターフェースも他方を継承しないため、機能しません。

私はあなたがそうすることを勧めます:

_foreach (DrawingPoint drawingPoint in e.OldItems ?? Array.Empty<DrawingPoint>())
  ...
_

代わりに。 ArrayはジェネリックではないIListであるため、これは機能します。 (ちなみに、1次元のゼロインデックスの配列はalsoジェネリック_IList<>_でもあります。)

その場合、_??_によって選択される「一般的な」タイプは非ジェネリックIListになります。

Array.Empty<T>()には、同じ型パラメーターTで呼び出されるたびに同じインスタンスを再利用できるという利点があります。

一般的に、ジェネリックでないIListの使用は避けます。あなたが持っているobjectコード内にDrawingPointからforeachへの目に見えない明示的なキャストが存在することに注意してください(上記の私の提案も)。これは、実行時にのみチェックされるものです。 IListDrawingPoint以外のオブジェクトが含まれている場合、例外が発生します。よりタイプセーフな_IList<>_を使用できる場合は、コードを入力するときにすでにタイプをチェックできます。


すでに_Array.Empty<>_を提案しているckuriによる(スレッド内の別の回答に対する)コメントを見つけました。あなたは関連する.NETバージョン(そこにあるコメントによる)を持っていないので、おそらく次のようなことをするべきです:

_public static class EmptyArray<TElement>
{
  public static readonly TElement[] Value = new TElement[] { };
}
_

あるいは単に:

_public static class EmptyArray<TElement>
{
  public static readonly TElement[] Value = { };
}
_

次に:

_foreach (DrawingPoint drawingPoint in e.OldItems ?? EmptyArray<DrawingPoint>.Value)
  ...
_

Array.Empty<>()メソッドと同様に、毎回同じ空の配列を確実に再利用します。


最後の1つの提案は、IListCast<>()拡張メソッドによってジェネリックにすることを強制することです。次に、Enumerable.Empty<>()を使用できます。

_foreach (var drawingPoint in
  e.OldItems?.Cast<DrawingPoint> ?? Enumerable.Empty<DrawingPoint>()
  )
  ...
_

_?._の使用と、varを使用できることに注意してください。

2

私はそれがあなたのケースではIListである最初のメンバーによる結果のタイプを決定すると信じています。最初のケースは、配列がIListを実装するため機能します。 IEnumerableの場合、それは真実ではありません。

詳細はないので、これは私の推測にすぎません ??演算子のオンラインドキュメント

UPD。受け入れられた質問で指摘されているように、C#仕様( [〜#〜] ecma [〜#〜] または GitHub上

2
Stas Ivanov