1

In the following code, I get error CS1061 about a not found method or extension method "EmptyIfNull".

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;

ImmutableDictionary<string, string>? dic = new[]
{ KeyValuePair.Create("Hello", "World") }.ToImmutableDictionary();

ImmutableArray<string>? arr = new[]
{ "Hello", "World" }.ToImmutableArray();

Console.WriteLine(string.Join(' ', dic.EmptyIfNull()));
Console.WriteLine(string.Join(' ', arr.EmptyIfNull()));

static class EnumerableExtensions
{
    public static IEnumerable<TSource> EmptyIfNull<TSource>(
        this IEnumerable<TSource>? source)
    {
        return source ?? Enumerable.Empty<TSource>();
    }
}

@ SharpLab.io

error CS1061: 'ImmutableArray?' does not contain a definition for 'EmptyIfNull' and no accessible extension method 'EmptyIfNull' accepting a first argument of type 'ImmutableArray?' could be found (are you missing a using directive or an assembly reference?)

I know about two ways to fix the code to produce the expected output:

  1. Change ImmutableArray<string>? arr to ImmutableArray<string> arr or var arr. That defeats the purpose of the EmptyIfNull() extension, though.
  2. Change arr.EmptyIfNull() to arr.EmptyIfNull<string>().

I believe the behavior for immutable arrays is different from the one for immutable dictionaries due to the fact that immutable arrays are structs while immutable dictionaries are classes. I just do not understand why type inference fails for the structs, and when the extension gets the type parameter explicitly, it the code works. By the way, I tested that when I assign null to the dic and arr variables and use the latter fix to the code, it actually works as expected, producing an empty enumerable.

I have been warned that the immutable array is boxed when used as an enumerable, which comes with a performance penalty. But I find it a non-issue here because the EmptyIfNull() extension is meant to be used to fix inputs to LINQ queries where a collection happens to be null instead of empty. The LINQ query will have a much higher overhead.

I am using .NET 6 and C# 10 if it changes anything. But the issue seems to be the same on SharpLab.io, which runs a more modern custom build of .NET SDK and its latest C# version. I believe such specifics should be mostly irrelevant to this question.

Palec
  • 12,743
  • 8
  • 69
  • 138
  • 1
    Does this answer your question? [extension method on type and nullable](https://stackoverflow.com/questions/742336/extension-method-on-type-and-nullabletype). `ImmutableArray?` is a `Nullable>`, which is not an `IEnumerable`. `IEnumerable?` doesn't match this type because it's merely a nullable annotation for a reference type. – Jeroen Mostert Dec 08 '22 at 11:37
  • 1
    Interestingly, you can get it to find the extension method if called as `arr.EmptyIfNull()`. – Jeroen Mostert Dec 08 '22 at 11:48
  • Also note that working with value types via interface can introduce performance hit due to boxing (i.e. potentially you would not want to use LINQ over `ImmutableArray`). – Guru Stron Dec 08 '22 at 11:49
  • Yeah, that's the bit that confused me. My current assumption is that it's the type inference which has a problem here, but the overload resolution is fine with it – canton7 Dec 08 '22 at 11:50
  • Thanks, @GuruStron. canton7 already warned me and deleted his comment after my reaction. Still, this is a useful warning in general. I will include my reasoning around that in the question. – Palec Dec 08 '22 at 11:51
  • 2
    Another interesting one is `EmptyIfNull(this TCollection? source) where TCollection : IEnumerable` and then calling it as `arr.EmptyIfNull?, string>()`, which will give a very specific "nullable types can not satisfy any interface constraints" error. – Jeroen Mostert Dec 08 '22 at 11:54
  • @JeroenMostert, I agree with your reasoning and it answers my question, but the duplicate target seems to be unrelated to interfaces and "unconstrained type parameter annotations", which seem to be at the core of my misunderstanding. – Palec Dec 08 '22 at 11:59
  • 1
    LINQ-wise, you'll indeed find that a `ImmutableArray?` can't match *any* of the extension methods in the standard `Enumerable` class, except those that operate on the non-generic `IEnumerable`; i.e. `arr.AsEnumerable()` doesn't work, `arr.AsEnumerable()` does. Type inference refuses to go the extra mile for nullables here, probably for very good though arcane reasons that only Eric Lippert could explain coherently. :P (And, yes, the linked question is in hindsight not directly relevant -- related, but not exactly the same thing.) – Jeroen Mostert Dec 08 '22 at 12:00
  • 2
    Last but not least `arr?.EmptyIfNull()!` is accepted without complaint and resolves correctly, due to the way the `?.` operator coerces the value back to an unboxed value type if not null. Of course this mostly defeats the purpose of the extension method due to the requirement for knowing that we're dealing with a value type, but you do get to omit the type parameter. – Jeroen Mostert Dec 08 '22 at 12:05
  • @JeroenMostert Just as Enumerable.Cast() is the way to get from the non-generic enumerables world to generic enumerables world of LINQ, my EnumerableExtensions.EmptyIfNull() was meant as a way to get from the nullable enumerables world to the LINQ world. But it seems the class × struct differences make it impossible to work with type inference. I see no other way than to specify the type explicitly at the call site. – Palec Dec 08 '22 at 12:06
  • 1
    Oh, forget that last comment -- obviously this will still yield `null` for a `null` input! The fact that I had to add `!` should have been a giveaway. :P The "correct" way to write that (tongue in cheek) is `(arr?.EmptyIfNull()).EmptyIfNull())`. – Jeroen Mostert Dec 08 '22 at 12:08
  • Interesting anyway! :-D – Palec Dec 08 '22 at 12:09
  • By the way, as shown above, `dic` contains one entry and `arr` two entries is that the intent? – John Alexiou Dec 08 '22 at 13:15
  • @JohnAlexiou, the actual items do not matter. I just wanted to provide a value that is non-null, non-empty, so that the program spits out something when fixed in one of the ways I described in the question. – Palec Dec 08 '22 at 17:33

1 Answers1

1

I think what's going on here is...

Let's simplify it:

int? x = 3;
x.Test();

static class EnumerableExtensions
{
    public static void Test<T>(this IEquatable<T>? source)
    {
    }
}

int is a struct. Therefore int? is syntactic sugar for Nullable<int>. While int may implement IEquatable<int>, Nullable<int> does not. However, if you box the int?, it turns into a boxed int, which does implement IEquatable<int>. Confused yet?

If you ask the compiler to find an overload of Test which accepts an int?, I think the type inference is failing because the only overload accepts IEquatable<int>, and int? doesn't implement IEquatable<int>. However when you specify T as int directly this bypasses type inference, and the compiler works out that it needs to box x, which gives it a boxed int, which does implement IEquatable<int>, and everything is OK.

The detail is probably in this section of the spec, if you like reading such things.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • Great food for thought, thanks! I am just struggling to match `EmptyIfNull(this IEnumerable? source)` against `Test(this IEquatable source)`. I see a difference in the question mark. But I came to believe that the question mark is ignored for structs, as mentioned here: https://endjin.com/blog/2022/02/csharp-10-generics-nullable-references-improvements-allownull – Palec Dec 08 '22 at 11:47
  • The question mark is irrelevant here -- I've added it in, but it's of no consequence – canton7 Dec 08 '22 at 11:48
  • 2
    `ImmutableArray` is a struct which implements the `IEnumerable` interface. `int` is a struct which implements the `IEquatable` interface – canton7 Dec 08 '22 at 11:49
  • Now with the question mark, it is clear. Thanks! – Palec Dec 08 '22 at 12:00