foreach 中的空合并运算符 IList、Array、Enumerable.Empty

Null coalescing operator IList, Array, Enumerable.Empty in foreach

this question 中我发现了以下内容:

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

类型

它给了我:

Operator '??' cannot be applied to operands of type 'System.Collections.IList' and 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> 无效?

我相信它决定了第一个成员的结果类型,在你的例子中是 IList。第一种情况有效,因为数组实现了 IListIEnumerable 不是真的。

这只是我的猜测,因为没有详细信息in the documentation for ?? operator online

UPD. 正如在已接受的问题中指出的那样,C# 规范中有更多关于该主题的详细信息 (ECMA or on GitHub)

当使用这个表达式时:

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> 并且 ?? 运算符可以做它的事情。


对"type of the expression will be the type of a"进行了简化,规范全文如下:


表达式的类型 a ?? b 取决于操作数上可用的隐式转换。按优先顺序,a ?? b的类型是A0AB,其中A是a的类型(前提是a有类型),Bb的类型(前提是b有类型),A0A的底层类型如果 A 是可空类型,否则 A。具体来说,a ?? b是这样处理的:

  • 如果 A 存在并且不是可空类型或引用类型,则会发生编译时错误。
  • 如果b是一个动态表达式,结果类型是动态的。在运行时,首先计算 a。如果a不是nulla被转换为动态类型,这就是结果。否则,b被求值,结果成为结果
  • 否则,如果 A 存在并且是可空类型,并且存在从 bA0 的隐式转换,则结果类型为 A0。在运行时,首先计算 a。如果 a 不是 nulla 将解包为类型 A0,它成为结果。否则,b 被评估并转换为类型 A0,它成为结果。
  • 否则,如果 A 存在并且存在从 bA 的隐式转换,则结果类型为 A。在运行时,首先计算 a。如果 a 不为空,则 a 成为结果。否则,b 被评估并转换为类型 A,它成为结果。
  • 否则,如果 b 具有类型 B 并且存在从 aB 的隐式转换,则结果类型为 B。在运行时,首先计算 a。如果 a 不是 nulla 将解包为类型 A0(如果 A 存在且可为空)并转换为类型 B,它变成了结果。否则,b 被评估并成为结果。
  • 否则,ab不兼容,会出现编译时错误。

您将非泛型 System.Collections.IList 与泛型 System.Collections.Generic.IEnumerable<> 一起用作 ?? 运算符的操作数。由于两个接口都没有继承另一个接口,所以这行不通。

我建议你这样做:

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

相反。这将起作用,因为任何 Array 都是非通用的 IList。 (顺便说一下,一维零索引数组同时也是 也是 泛型 IList<>。)

在这种情况下,?? 选择的 "common" 类型将是非泛型 IList

Array.Empty<T>() 的优点是每次使用相同的类型参数 T.

调用时都可以重复使用相同的实例

一般来说,我会避免使用非泛型 IList。请注意,在您拥有的 foreach 代码中存在从 objectDrawingPoint 的不可见显式转换(也符合我上面的建议)。那是只会在 运行 时间检查的东西。如果 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<>() 方法一样,这将确保我们每次都重复使用相同的空数组。


最后一个建议是通过 Cast<>() 扩展方法强制 IList 通用;那么你可以使用 Enumerable.Empty<>():

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

注意 ?. 的用法以及我们现在可以使用 var 的事实。