24

In this question I found the following:

int[] array = null;

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

and

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

and

... ?? new int[0]

In a NotifyCollectionChangedEventHandler I wanted to apply the Enumerable.Empty like so:

foreach (DrawingPoint drawingPoint in e.OldItems ?? Enumerable.Empty<DrawingPoint>())
    this.RemovePointMarker(drawingPoint);

Note: OldItems is of the type IList

And it gives me:

Operator '??' cannot be applied to operands of type 'System.Collections.IList' and System.Collections.Generic.IEnumerable<DrawingPoint>

However

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

and

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

works just fine.

Why is that?
Why does IList ?? T[] work but IList ?? IEnumerable<T> doesn't?

IDarkCoder
  • 709
  • 7
  • 18
  • 5
    `T[]` **is** an `IList`, but there is no guarantee that your object that implements `IEnumerable` implements `IList`. You will have to cast your `IList` object to `IEnumerable` to use it with `?? Enumerable.Empty`. – Lasse V. Karlsen Sep 18 '18 at 07:20

3 Answers3

22

When using this expression:

a ?? b

Then b either must be the same type as a, or it must be implicitly castable to that type, which with references means that it has to implement or inherit from whatever type a is.

These work:

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

because T[] is an IList<T>, the array type implements that interface.

However, this won't work:

SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

because the type of the expression will be the a type, and the compiler is obviously unable to guarantee that SomethingThatImplementsIEnumerableOfT also implements IList<T>.

You're going to have to cast one of the two sides so that you have compatible types:

(IEnumerable<T>)SomethingThatIsIListOfT ?? SomethingThatImplementsIEnumerableOfT

Now the type of the expression is IEnumerable<T> and the ?? operator can do its thing.


The "type of the expression will be the type of a" is a bit simplified, the full text from the specification is as follows:


The type of the expression a ?? b depends on which implicit conversions are available on the operands. In order of preference, the type of a ?? b is A0, A, or B, where A is the type of a (provided that a has a type), B is the type of b (provided that b has a type), and A0 is the underlying type of A if A is a nullable type, or A otherwise. Specifically, a ?? b is processed as follows:

  • If A exists and is not a nullable type or a reference type, a compile-time error occurs.
  • If b is a dynamic expression, the result type is dynamic. At runtime, a is first evaluated. If a is not null, a is converted to a dynamic type, and this becomes the result. Otherwise, b is evaluated, and the outcome becomes the result.
  • Otherwise, if A exists and is a nullable type and an implicit conversion exists from b to A0, the result type is A0. At runtime, a is first evaluated. If a is not null, a is unwrapped to type A0, and it becomes the result. Otherwise, b is evaluated and converted to type A0, and it becomes the result.
  • Otherwise, if A exists and an implicit conversion exists from b to A, the result type is A. At runtime, a is first evaluated. If a is not null, a becomes the result. Otherwise, b is evaluated and converted to type A, and it becomes the result.
  • Otherwise, if b has a type B and an implicit conversion exists from a to B, the result type is B. At runtime, a is first evaluated. If a is not null, a is unwrapped to type A0 (if A exists and is nullable) and converted to type B, and it becomes the result. Otherwise, b is evaluated and becomes the result.
  • Otherwise, a and b are incompatible, and a compile-time error occurs.
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • 1
    Thank you. In the [.NET Documentation on the Array Class](https://learn.microsoft.com/en-us/dotnet/api/system.array?view=netframework-4.7.2) you can see that it implements `System.Collections.IList`. Is there in equivalent for `Enumerable.Empty` for `IList`? – IDarkCoder Sep 18 '18 at 07:27
  • 2
    Other than `new T[0]`, I don't know of any, there could be something that I don't know about of course. – Lasse V. Karlsen Sep 18 '18 at 07:34
  • 2
    Either `new T[]` or `new List()` should do the trick, as @LasseVågsætherKarlsen said – Ben Sep 18 '18 at 08:10
  • 2
    @IDarkCoder There is [Array.Empty](https://learn.microsoft.com/en-us/dotnet/api/system.array.empty?view=netframework-4.7.2) which is often preferable over `new T[0]` because the latter will always construct a new array instance, whereas Array.Empty reuses the same. – ckuri Sep 18 '18 at 10:52
  • That's exactly what I'm looking for! However I'm limited to .Net 4.5.2 so I can't use that yet, since its 4.6+ :( – IDarkCoder Sep 18 '18 at 10:59
2

I believe that it determines the type of result by the firs member which in your case is IList. The first case works because an array implements IList. With IEnumerable it's not true.

It's just my speculation, as there are no details in the documentation for ?? operator online.

UPD. As it pointed out in accepted question, there are a lot more details on the topic in C# Specification (ECMA or on GitHub)

Stas Ivanov
  • 1,173
  • 1
  • 14
  • 24
2

You are using the non-generic System.Collections.IList together with the generic System.Collections.Generic.IEnumerable<>, as the operands of the ?? operator. Since neither interface inherits the other, that will not work.

I suggest you do:

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

instead. This will work because any Array is a non-generic IList. (One-dimensional zero-indexed arrays are also generic IList<> at the same time, by the way.)

The "common" type picked by ?? will be non-generic IList in that case.

Array.Empty<T>() has the advantage of reusing the same instance every time it is called with the same type parameter T.

In general, I would avoid using non-generic IList. Note that there exists an invisible explicit cast from object to DrawingPoint in the foreach code you have (also with my suggestion above). That is something that will only be checked at run-time. If the IList contains other objects than DrawingPoint, it blows up with an exception. If you can use the more type-safe IList<>, then the types can be checked already as you type your code.


I see a comment by ckuri (to another answer in the thread) that already suggested Array.Empty<>. Since you do not have the relevant .NET version (according to comments there), maybe you should just do something like:

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

or just:

public static class EmptyArray<TElement>
{
  public static readonly TElement[] Value = { };
}

then:

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

Just like the Array.Empty<>() method, this will ensure we reuse the same empty array each time.


One final suggesting is forcing the IList to be generic by the Cast<>() extension method; then you can use Enumerable.Empty<>():

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

Note the use of ?. and the fact that we can use var now.

Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • I thought about creating a singleton like however I figured that int[0] will work until we get .NET 4.6... regarding the `IList`, I can't - without putting too much work into it which isn't worth it here - change the type of that collection because it's part of the `NotifyCollectionChangedEventHandler` of `ObservableCollection`. Although I'm unsure why the didn't use `IList` in this case, since the collection is generic. – IDarkCoder Sep 18 '18 at 14:18