空合并运算符IList,Array,Enumerable.Empty in foreach

时间:2018-09-18 07:12:29

标签: c# .net collections null-coalescing-operator

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

它给了我

  

操作员“ ??”不能应用于类型为'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>无效?

3 个答案:

答案 0 :(得分:20)

使用此表达式时:

a ?? b

然后,b必须与a相同,或者必须隐式转换为该类型,这意味着引用必须实现或继承{{1} }是。

这些作品:

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 ?? bA0A,其中B是a的类型(前提是{{1 }}具有类型),Aa的类型(假设B具有类型),而bb的基础类型如果A0是可为空的类型,否则为A。具体来说,A的处理如下:

  • 如果A存在并且不是可为空的类型或引用类型,则将发生编译时错误。
  • 如果a ?? b是动态表达式,则结果类型是动态的。在运行时,首先评估A。如果b不是a,则a会转换为动态类型,这将成为结果。否则,将评估null,结果将成为结果。
  • 否则,如果a存在并且是可为空的类型,并且存在从bA的隐式转换,则结果类型为b。在运行时,首先评估A0。如果A0不是a,则将a展开以键入null,并成为结果。否则,将对a进行求值并将其转换为类型A0,它将成为结果。
  • 否则,如果存在b并且存在从A0A的隐式转换,则结果类型为b。在运行时,首先评估A。如果A不为空,则a成为结果。否则,将对a进行求值并将其转换为类型a,它将成为结果。
  • 否则,如果b具有类型A,并且存在从bB的隐式转换,则结果类型为a。在运行时,首先评估B。如果B不是a,则将a展开为类型null(如果a存在并且可以为空)并转换为类型A0,它成为结果。否则,将评估A并成为结果。
  • 否则,Bb不兼容,并且发生编译时错误。

答案 1 :(得分:2)

我相信它决定了第一位成员(在您的情况下为 IList )的结果类型。第一种情况有效,因为数组实现了 IList 。使用 IEnumerable 并非如此。

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

UPD。在已接受的问题中指出,C#规范(ECMAon GitHub)中有关该主题的详细信息很多

答案 2 :(得分:2)

您正在将非通用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。请注意,在您拥有的object代码中,存在从DrawingPointforeach的无形显式转换(也与上面我的建议一样)。只有在运行时才能检查。如果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的事实。