5

I was wondering why this was the case, as it went against what I would have expected. This has to do with how Generic Nullable types are interpreted in C#.

  public static int? MyFunction(){
    return default;
  }

  public static T MyFunctionGeneric<T>() {
    return default;
  }

  public static T? MyFunctionGenericNullable<T>(){
    return default;
  }

  public static void main(string[] args){
    Console.WriteLine(MyFunction());
    Console.WriteLine(MyFunctionGeneric<int?>());
    Console.WriteLine(MyFunctionGenericNullable<int>());
  }

As expected, when I define the return type as int? the default returned value is null.

This is also the case when I define a generic that returns its own type, because int? is passed in, the default returned value is null.

However, in the third case, even though the return type is technically int?, the default returned value is 0. I would have to pass in int? as my type parameter to get a null response. Note that this happens even when I explicitly state return default(T?)

This is a little bit unexpected, does anyone have an explanation for it?

2
  • 1
    Awesome question! Great repro! I agree that the behaviour is surprising. May I suggest that you make the declarations of the first three functions static so that people can just cut and paste this code into a console app to reproduce it. Commented Nov 4, 2024 at 20:04
  • An int is an int. A int? is an int?. But an int is definitely not an int?. Commented Nov 4, 2024 at 20:10

3 Answers 3

6

It's all very confusing because as the docs state:

The rules are necessarily detailed because of history and the different implementation for a nullable value type and a nullable reference type.

reading on in the docs, you see an answer to your question (in bold):

If the type argument for T is a reference type, T? references the corresponding nullable reference type. For example, if T is a string, then T? is a string?.

If the type argument for T is a value type, T? references the same value type, T. For example, if T is an int, the T? is also an int.

If the type argument for T is a nullable reference type, T? references that same nullable reference type. For example, if T is a string?, then T? is also a string?.

If the type argument for T is a nullable value type, T? references that same nullable value type. For example, if T is a int?, then T? is also a int?.

If we want different behavior, we need to use generic constraints. The documentation doesn't list all useful constraints which in your case would be where T:struct

This would be compiled with int? as return type if used with int as T

public static T? MyFunctionGenericNullable<T>()
where T : struct {
    return default;
}

and you would be getting the null like in the first two cases.

Sign up to request clarification or add additional context in comments.

1 Comment

I believe the docs are confusing too, because if one inspects the IL and metadata of generic types/methods that use T? (with T not constrained to class or struct) it turns out that the compiler just ignores the ?. stackoverflow.com/a/78137173/77724
4

In C#, T? is shorthand for two different things depending on whether T is a value type or a reference type.

T? for Value Types (e.g., int?, double?)

When T is a value type, T? is shorthand for Nullable<T>.

T? for Reference Types (Nullable Reference Types)

Starting in C# 8.0, T? is also used for nullable reference types when T is a reference type (e.g., string?, MyClass?). This feature allows you to indicate that a reference type can hold null (without actually being boxed into a Nullable<T> wrapper). Example:

string? myString = null; // This is a nullable reference type, not `Nullable<string>`

In this context, string? means a nullable reference type string that may hold a null value. T? for reference types is only a compile-time annotation that provides nullability warnings and checks. It doesn't affect runtime behaviour or introduce a Nullable<T> wrapper.

What about generic T?

It's all about the constraints.

Since there is no constraint on T in your example, the compiler treats default differently based on what T is instantiated with:

If T is a reference type, default will be null because default for reference types is null.

If T is a value type, default will be the default value of that type (e.g., 0 for int).

You can constrain your MyFunctionGenericNullable<T> so that T is a value type with a generic constraint:

public static T? MyFunctionGenericNullableValueType<T>() where T: struct {
    return default;
}

...and you'll see the behaviour you likely expected originally. (Does not return 0.)

Comments

0

The reason for this is that marking a function as returning T? just means that it can return null. But the type of T in this function is still int as passed in the type parameter but int? translates into a totally different type: Nullable<int>.

1 Comment

This is misleading, for example you couldn't implement MyFunctionGenericNullable with Nullable<T> temp = default; return temp; At least, not unless you added where T: struct as a constraint.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.