default(T?) does not return null when T is a value type
Asked Answered
R

1

8

I've come across the following phenomenon and am absolutely bamboozled. I'm using C# 10 with nullables enabled.

default(int?) returns null as expected. The following function, however, returns whatever default(T) is

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

In the case of ShouldReturnNull<int>() we get 0. Shouldn't it also return null?

I have the following code in my program where this becomes an issue:

public T?[] FindKElements(...)
{
    var result = new (T, double)?[k];


    // ... populate result array,
    // possibly including null values...


    // return an array containing only the T part or null
    return result.Select(e => e is null ? default(T?) : e.Value.Item1).ToArray();
}

Is there a way to keep it this generic but with proper nulls instead when T is a value type? The compiler won't let me use null in place of default(T?).

Roomy answered 30/5, 2022 at 13:3 Comment(4)
this is ultimately pretty similar to this - just... ignore the enum bit, which the compiler already was :) ultimately, without a T : struct constraint, T? is ... woolly - it doesn't mean Nullable<T>; it means "T, but without NRT nulls" - and NRT nulls don't apply for your case, so: it just means: TBertram
Hm, I see. In my case, T is a type parameter of the surrounding class. Would I need to duplicate the class, once with where T : struct and once with where T : class in order to support both classes and structs?Roomy
without knowing what you need to do differently in your scenario (between structs and classes), I can't comment on that; however, one pattern I've seen quite often here is to, instead of relying on nulls, to return a (bool, T) (or a Maybe<T>, or whatever you want to call it) in all cases - which is essentially like Nullable<T>, but it works for both classes and structs - then you don't need to differentiateBertram
Thank you. That seems to be the best way then.Roomy
B
4

In the absence of a where T : struct constraint, T? does not mean Nullable<T>; it means "T, but note that it won't be null in the NRT sense" - and since NRT nulls never apply to your value-type scenario: it basically just means T; and the default value of a value-type T is not null (in any sense).

In the scenario where you need "null checking" that crosses both value-type and reference-type scenarios including support for value-types without a value, then the easiest approach is usually to forget about Nullable<T> and just track:

  1. do I have a value (bool), and
  2. what is the value (T)

separately, and explicitly; this could be via any of:

  • bool SomeMethod(out var T)
  • (HasValue: bool, Value: T) SomeMethod()
  • Maybe<T> SomeMethod()

(where Maybe<T> is just a custom struct that is composed of a bool HasValue and a T Value)

This is effectively creating something akin to Nullable<T>, but which applies to all values, regardless of type. Instead of checking for null, just check .HasValue first, and if true, assume that the value is meaningful.

Bertram answered 30/5, 2022 at 14:5 Comment(1)
Example of aforementioned Maybe<T> here: github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/…Grouper

© 2022 - 2024 — McMap. All rights reserved.