Нулевой коалесцирующий оператор IList, Array, Enumerable.Empty в foreach

В этом вопросе я нашел следующее:

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?? T[] работает, но IList?? IEnumerable<T> IList?? IEnumerable<T> нет?

Ответ 1

При использовании этого выражения:

a ?? b

Тогда b либо должен быть того же типа, что и a, либо он должен быть неявным образом применим к этому типу, который со ссылками означает, что он должен реализовывать или наследовать от любого типа a.

Эти работы:

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

потому что T[] является IList<T>, тип массива реализует этот интерфейс.

Однако это не сработает:

SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

потому что тип выражения будет типом a, и компилятор, очевидно, не может гарантировать, что SomethingThatImplementsIEnumerableOfT также реализует IList<T>.

Вам нужно будет отбросить одну из двух сторон, чтобы у вас были совместимые типы:

(IEnumerable<T>)SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

Теперь тип выражения IEnumerable<T> и ?? оператор может сделать свое дело.


"Тип выражения будет типом a " немного упрощен, полный текст из спецификации выглядит следующим образом:


Тип выражения a?? b a?? b зависит от того, какие неявные преобразования доступны в операндах. В порядке предпочтения, тип a?? b a?? b - A0, A или B, где A - тип a (при условии, что a имеет тип), B - тип b (при условии, что b имеет тип), а A0 является базовым типом A если A является нулевым типом, иначе A В частности, a?? b a?? b обрабатывается следующим образом:

  • Если существует A и не является типом NULL или ссылочным типом, возникает ошибка времени компиляции.
  • Если b является динамическим выражением, тип результата является динамическим. Во время выполнения сначала оценивается значение a. Если a не является null, a преобразуется в динамический тип, и это становится результатом. В противном случае b оценивается, и результат становится результатом.
  • В противном случае, если A существует и является нулевым типом, и существует неявное преобразование от b до A0, то результатом является A0. Во время выполнения сначала оценивается значение a. Если a не является null, a разворачивается для ввода A0, и он становится результатом. В противном случае b оценивается и преобразуется в тип A0, и он становится результатом.
  • В противном случае, если существует A существует неявное преобразование от b до A, то результатом является A Во время выполнения сначала оценивается значение a. Если a не является нулевым, a становится результатом. В противном случае b оценивается и преобразуется в тип A, и это становится результатом.
  • В противном случае, если b имеет тип B и существует неявное преобразование от a до B, результатом является B Во время выполнения сначала оценивается значение a. Если a не является null, a разворачивается для ввода A0 (если существует A и имеет значение NULL) и преобразуется в тип B, и он становится результатом. В противном случае b оценивается и становится результатом.
  • В противном случае a и b несовместимы, и возникает ошибка времени компиляции.

Ответ 2

Я считаю, что он определяет тип результата со стороны первого члена, который в вашем случае является IList. Первый случай работает, потому что массив реализует IList. С IEnumerable это не так.

Это просто мои предположения, поскольку в документации нет никаких подробностей ? оператором онлайн.

UPD. Как указывалось в принятом вопросе, в С# Specification (ECMA или GitHub) есть более подробная информация по этой теме,

Ответ 3

Вы используете необщего System.Collections.IList вместе с родовым System.Collections.Generic.IEnumerable<>, так как операнды ?? оператор. Поскольку ни один интерфейс не наследует другого, это не сработает.

Я предлагаю вам:

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

вместо. Это будет работать, потому что любой Array является неосновным IList. (IList<> прочим, одномерные нуль-индексированные массивы также являются общим IList<>.)

"Общий" тип выбраны ?? в этом случае будет non-generic IList.

Array.Empty<T>() имеет преимущество повторного использования одного и того же экземпляра каждый раз, когда он вызывается с тем же типом параметра T

В общем, я бы избегал использовать не-общий IList. Обратите внимание, что существует невидимое явное приведение от object к DrawingPoint в код foreach который у вас есть (также с моим предложением выше). Это то, что будет проверяться только во время выполнения. Если IList содержит другие объекты, кроме DrawingPoint, он взрывается с исключением. Если вы можете использовать более безопасный тип IList<>, то типы могут быть проверены уже при вводе кода.


Я вижу комментарий ckuri (к другому ответу в потоке), который уже предложил Array.Empty<>. Поскольку у вас нет соответствующей версии.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<>(), это гарантирует, что мы будем повторно использовать один и тот же пустой массив каждый раз.


Один из последних предложений заключается в том, что IList является общим с помощью метода расширения Cast<>(); то вы можете использовать Enumerable.Empty<>():

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

Обратите внимание на использование ?. и тот факт, что мы теперь можем использовать var.