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

问题描述 投票:22回答:3

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>不工作?

c# .net collections null-coalescing-operator
3个回答
20
投票

使用此表达式时:

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的类型是A0AB,其中A是a的类型(假设a有类型),Bb的类型(假设b有类型),和A0如果A是可以为空的类型,则为A的基础类型,否则为A。具体来说,a ?? b处理如下:

  • 如果A存在且不是可空类型或引用类型,则会发生编译时错误。
  • 如果b是动态表达式,则结果类型是动态的。在运行时,首先评估a。如果a不是null,则a将转换为动态类型,这就成了结果。否则,评估b,结果成为结果。
  • 否则,如果A存在并且是可空类型并且存在从bA0的隐式转换,则结果类型为A0。在运行时,首先评估a。如果a不是nulla将被打开以打字A0,它就会成为结果。否则,评估b并将其转换为A0类型,它就会成为结果。
  • 否则,如果存在A并且存在从bA的隐式转换,则结果类型为A。在运行时,首先评估a。如果a不为null,则a成为结果。否则,评估b并将其转换为A类型,它就会成为结果。
  • 否则,如果b的类型为B,并且存在从aB的隐式转换,则结果类型为B。在运行时,首先评估a。如果a不是null,则a打开以打包A0(如果A存在且可以为空)并转换为B类型,它就会成为结果。否则,评估b并成为结果。
  • 否则,ab不兼容,并发生编译时错误。

2
投票

我认为它决定了第一个成员的结果类型,在你的情况下是IList。第一种情况有效,因为数组实现了IList。使用IEnumerable并非如此。

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

UPD。正如它在已接受的问题中指出的那样,在C#规范(ECMAon GitHub)中有关于该主题的更多细节


2
投票

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

我建议你这样做:

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

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

在这种情况下,??选择的“普通”类型将是非通用的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的事实。

© www.soinside.com 2019 - 2024. All rights reserved.