Нулевой коалесцирующий оператор 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
.