Search code examples
c#genericscompiler-errorsnullabletype-constraints

Cannot return null in function whose return type is generic


If I have a type, say, an int, I can make it nullable by adding a question mark to the end of it. For instance, the following code compiles:

int? x = null;

while the following code, with the question mark removed, does not:

int x = null; //throws a build error

My understanding was that I could do this to any type; just add a question mark to the end of it, and I could always assign a null value to it.

However, for some reason, this does not work with generic functions. Consider the following:

T? ReturnNull<T>() => null;

It throws compiler error CS0403:

CS0403: Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using default('T') instead.

What!? If I add the question mark to the name of any type (class or struct), I can assign it a null value and it works fine!

Intriguingly, if I require T to be a class or to be a struct:

T? ReturnNull<T>() where T : class => null;
T? ReturnNull<T>() where T : struct => null;

It works fine. I though I could just workaround the issue by overloading the two above functions (a type may not be both class and struct, right?).

No, I couldn't. I thought wrong. The following throws a build error:

T? ReturnNull<T>() where T : class => null;
T? ReturnNull<T>() where T : struct => null;

CS0128: A local variable or function named 'ReturnNull' is already defined in this scope

WHAT!? I can overload functions by changing the parameters! Why can't I overload functions by changing the type constraints for the generic arguments? Classes and structs don't overlap! EVER!

I'm baffled. I've been very nice to my IDE, not teased it or anything; that can't be the problem. Why isn't this working?


Solution

  • I'm reading this article Constraints on type parameters and Unconstrained type parameter annotations from learn.microsoft.com

    These might be the reason why

    The addition of nullable reference types complicates the use of T? in a generic type or method. T? can be used with either the struct or class constraint, but one of them must be present. When the class constraint was used, T? referred to the nullable reference type for T. Beginning with C# 9, T? can be used when neither constraint is applied. In that case, T? is interpreted as T? for value types and reference types. However, if T is an instance of Nullable, T? is the same as T. In other words, it doesn't become T??.

    Which means you need to put explicit constraint to those method like:

    public class FOO
    {
        T? ReturnNull<T>(T? t) where T : struct => null;
        T? ReturnNull<T>(T? t) where T : class => null;
    }
    

    About why this code doesn't work

    According to Member Overloading - microsoft.com and Generic methods - microsoft.com

    Member overloading means creating two or more members on the same type that differ only in the number or type of parameters but have the same name.

    The same rules for type inference apply to static methods and instance methods. The compiler can infer the type parameters based on the method arguments you pass in; it cannot infer the type parameters only from a constraint or return value. Therefore type inference does not work with methods that have no parameters. Type inference occurs at compile time before the compiler tries to resolve overloaded method signatures. The compiler applies type inference logic to all generic methods that share the same name. In the overload resolution step, the compiler includes only those generic methods on which type inference succeeded.

    which means the two of your ReturnNull generic function are having same signature.

    //your code
    T? ReturnNull<T>() where T : class => null;
    T? ReturnNull<T>() where T : struct => null;
    

    Both functions above did not pass any argument and type inference does not work with methods that have no parameters, result in both are having same siganature.

    with parameters

    If you add parameter of type T, the type inference will apply and make it two different signatures:

    T? ReturnNull<T>(T? t) where T : struct => null;
    T? ReturnNull<T>(T? t) where T : class => null;
    

    without parameters when calling functions

    If you really don't want parameters when calling it, you can gave them different type with dummy default value:

    public static class FOO
    {
        public static T? ReturnNull<T>(int? v = null) where T : struct => null;
        public static T? ReturnNull<T>(double? v = null) where T : class => null;
    }
    

    and use it likes:

    System.Console.WriteLine(FOO.ReturnNull<int>()?.ToString()??"null");
    System.Console.WriteLine(FOO.ReturnNull<List<int>>()?.ToString()??"null");
    //output:
    //null
    //null
    

    Note: overloading does not work for local function. That means even with different parameter, two local functions with the same name will not compile.