Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 503
IDarkCoder Avatar asked Sep 18 '18 07:09

IDarkCoder


3 Answers

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.
like image 79
Lasse V. Karlsen Avatar answered Nov 17 '22 16:11

Lasse V. Karlsen


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)

like image 33
Stas Ivanov Avatar answered Nov 17 '22 17:11

Stas Ivanov


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.

like image 2
Jeppe Stig Nielsen Avatar answered Nov 17 '22 16:11

Jeppe Stig Nielsen