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。第一种情况有效,因为数组实现了 IList。 IEnumerable 不是真的。
这只是我的猜测,因为没有详细信息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
的类型是A0
、A
或B
,其中A
是a的类型(前提是a
有类型),B
是b
的类型(前提是b
有类型),A0
是A
的底层类型如果 A
是可空类型,否则 A
。具体来说,a ?? b
是这样处理的:
- 如果
A
存在并且不是可空类型或引用类型,则会发生编译时错误。
- 如果
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
存在且可为空)并转换为类型 B
,它变成了结果。否则,b
被评估并成为结果。
- 否则,
a
和b
不兼容,会出现编译时错误。
您将非泛型 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
代码中存在从 object
到 DrawingPoint
的不可见显式转换(也符合我上面的建议)。那是只会在 运行 时间检查的东西。如果 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
的事实。
在 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。第一种情况有效,因为数组实现了 IList。 IEnumerable 不是真的。
这只是我的猜测,因为没有详细信息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
的类型是A0
、A
或B
,其中A
是a的类型(前提是a
有类型),B
是b
的类型(前提是b
有类型),A0
是A
的底层类型如果 A
是可空类型,否则 A
。具体来说,a ?? b
是这样处理的:
- 如果
A
存在并且不是可空类型或引用类型,则会发生编译时错误。 - 如果
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
存在且可为空)并转换为类型B
,它变成了结果。否则,b
被评估并成为结果。 - 否则,
a
和b
不兼容,会出现编译时错误。
您将非泛型 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
代码中存在从 object
到 DrawingPoint
的不可见显式转换(也符合我上面的建议)。那是只会在 运行 时间检查的东西。如果 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
的事实。