关于c#:在foreach中Null合并运算符IList,Array,Enumerable.Empty

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

在这里,我发现下面的问题:

1
2
3
4
5
6
int[] array = null;

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

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

1
... ?? new int[0]

我想在一个NotifyCollectionChangedEventHandler适用《Enumerable.Empty:像婊子

1
2
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

然而,

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

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

工作只是罚款。

这是为什么呢? 为什么IList ?? T[]IList ?? IEnumerable工作但不?


使用此表达式时:

1
a ?? b

那么,b要么是与a相同的类型,要么是隐式地可转换为该类型,通过引用意味着它必须实现或继承a是什么类型。

这些工作:

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

由于T[]IList类型,数组类型实现该接口。

但是,这不起作用:

1
SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

因为表达式的类型将是a类型,并且编译器显然无法保证SomethingThatImplementsIEnumerableOfT也实现IList

您将不得不在两个方面中的一个进行强制转换,这样您就有了兼容的类型:

1
(IEnumerable<T>)SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

现在表达式的类型是IEnumerable??运算符可以完成它的工作。

"表达式类型将是a的类型"有点简化,规范全文如下:

表达式a ?? b的类型取决于哪些隐式转换可用于操作数。根据优先顺序,a ?? b的类型是A0ab,其中a是A型(前提是a有A型),EDOCX1 1〔0〕是EDOCX11〔0〕型(前提是b有A型),EDOCX11〔0〕是EDOCX11〔0〕型(前提是b有A型),EDOCX11〔13〕是EDOCX11〔1〕的基础类型,如果EDOCX11〔1〕有A型,EDOCX11〔1〕的基础类型是EDOCX11〔1〕的基础类型(前提条件条件是EDOCX11〔1〕有1〔0〕有A型),EDOCX11 a是nulllable类型,或者a,否则。具体来说,a ?? b的处理如下:

  • 如果a存在且不是可以为空的类型或引用类型,则会发生编译时错误。
  • 如果b是动态表达式,则结果类型为动态。在运行时,首先对a进行评估。如果a不是null的话,a转换为动态类型,这就是结果。否则,评估b,结果即为结果。
  • 否则,如果a存在并且是可以为空的类型,并且存在从bA0的隐式转换,则结果类型为A0。在运行时,首先对a进行评估。如果a不是null型,则a被解包为A0型,成为结果。否则,对b进行评估,转换为A0型,即为结果。
  • 否则,如果存在a且存在从ba的隐式转换,则结果类型为a。在运行时,首先对a进行评估。如果a不为空,则结果为a。否则,对b进行评估,并转换为a型,即为结果。
  • 否则,如果b具有b类型,并且存在从ab的隐式转换,则结果类型为b。在运行时,首先对a进行评估。如果a不是null,则a展开为A0型(如果a存在且可以为空),并转换为b型,即为结果。否则,将对b进行评估并成为结果。
  • 否则,ab不兼容,出现编译时错误。


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

我建议你这样做:

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

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

在这种情况下,由??选择的"通用"类型将是非通用IList

Array.Empty()的优点是每次使用同一类型参数T调用同一实例时都可以重用该实例。

一般来说,我会避免使用非通用的IList。请注意,在您拥有的foreach代码中,存在一个从objectDrawingPoint的不可见显式强制转换(也有我上面的建议)。这是只在运行时检查的内容。如果IList中含有DrawingPoint以外的其他物体,它会爆炸,但有例外。如果您可以使用更安全的类型IList<>,那么可以在键入代码时检查类型。

我看到了一个由克库里(在线程中的另一个答案)发表的评论,它已经暗示了Array.Empty<>。由于您没有相关的.NET版本(根据此处的注释),可能您应该做如下操作:

1
2
3
4
public static class EmptyArray<TElement>
{
  public static readonly TElement[] Value = new TElement[] { };
}

或者只是:

1
2
3
4
public static class EmptyArray<TElement>
{
  public static readonly TElement[] Value = { };
}

然后:

1
2
foreach (DrawingPoint drawingPoint in e.OldItems ?? EmptyArray<DrawingPoint>.Value)
  ...

就像Array.Empty<>()方法一样,这将确保每次重用相同的空数组。

最后一个建议是强制IList通过Cast<>()扩展方法成为泛型;然后可以使用Enumerable.Empty<>()

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

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


我相信它决定了FIRS成员的结果类型,在您的情况下是IList。第一种情况起作用,因为数组实现了IList。对于IEnumerable,这不是真的。

这只是我的猜测,因为文档中没有详细信息??操作员在线。

UPD。正如它在接受的问题中指出的,在C规范(ECMA或Github)中有很多关于这个主题的详细信息。