Home > Software design >  Type inference for generic method differs between nullable classes and structs
Type inference for generic method differs between nullable classes and structs

Time:12-08

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.

CodePudding user response:

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.

  • Related