Cannot return null in function whose return type is generic

133 Views Asked by At

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?

3

There are 3 best solutions below

0
Magic1647 On BEST ANSWER

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.

0
Jonathan Willcock On

Please do not ask multiple questions in one.

"If I have a type, say an int, I can make it nullable by adding a question mark to the end of it". No you can't! int is not the same type as int?. One is an integer and the other is a nullable integer. Just because c# now supports nullable integers, it doesn't mean that non-nullable integers have disappeared. Your original code doesn't compile, precisely because the language still provides non-nullable types.

"Why can't I overload by changing the type constraints.." Because it hasn't been implemented. Such questions are off-topic here. It is tantamount to asking why pigs can't fly: because God/Evolution didn't supply them with wings. Why in such cases is unanswerable.

0
shingo On

To answer the first question, int? x = null is short for int? x = new Nullable<T>(), which is not the same as object? x = null at the IL level, the compiler cannot generate unified instructions for value types and reference types.

To answer the second question, that's because "A return type of a method is not part of the signature of the method for the purposes of method overloading."

Thus, if you follow your idea, you have to use out parameters.

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