2

I have the following class:

class GenericClass<T>
{
    public T? A { get; init; }
    public T B { get; init; }

    public GenericClass(T? a, T b)
    {
        A = a;
        B = b;
    }
}

I want to use it for any sort of T, no matter if struct, class, enum. I merely want to make sure that property A can always be null while property B's nullable-ness is the same as T's. But this does not seem to work.

If I write

static void MyTestMethod()
{
    var vStruct = new GenericClass<bool>(null, true);
    var vNullableStruct = new GenericClass<bool?>(null, true);
    var vClass = new GenericClass<string>(null, "hello");
    var vNullableClass = new GenericClass<string?>(null, "hello");
    var vEnum = new GenericClass<Color>(null, Color.red);
    var vNullableEnum = new GenericClass<Color?>(null, Color.red);
}

where Color is defined as enum Color { red, green, blue },

I get compiler error CS1503 for vStruct and vEnum:

Argument '1': cannot convert from <NULL> to bool/Color

In both cases, the mouseover for new GenericClass<...> shows both arguments as non-nullable although I clearly defined A as nullable. Why?

I can add a type constraint: class GenericClass<T> where T : struct, which will leave the two previously erroneous lines (vStruct and vEnum) error-free, but will mark all others as erroneous. This time with compiler error CS0453:

The type bool?/string/string?/Color must be a non-nullable value type in order to use it as parameter T in the generic type or method GenericClass<T>.

Which makes perfectly sense; they are not struct.

But how do I make my GenericClass<T> work for both classes and structs/enums?

Kjara
  • 2,504
  • 15
  • 42
  • 2
    Despite superficial similarities, nullable-ness is implemented *entirely differently* for structs and classes.. The compiler *cannot* emit a definition of the generic class that will work for both. – Damien_The_Unbeliever Jun 15 '22 at 07:35
  • I think it's coming in C#11 – McNets Jun 15 '22 at 07:47
  • 1
    This is a weakness of the nullable system in c#. It is one reason a custom Option/Maybe type may still be useful. – JonasH Jun 15 '22 at 07:54
  • @JonasH If the official `Option` type isn't a monad we're going to have another holy-war [like JavaScript's `Promise`](https://github.com/promises-aplus/promises-spec/issues/94) – Dai Jun 15 '22 at 11:50
  • 1
    @McNets I think that's the `never-`default`" constraint you're referring to – Dai Jun 15 '22 at 11:52

1 Answers1

3

The comment by sjb-sjb below this answer provides the answer to my "why":

As it is, in C#9, with T unconstrained (or even constrained to notnull), the syntax "T?" means "the defaultable type corresponding to T" instead of "the nullable type corresponding to T" as it does everywhere else in the language. However if you constrain T to class or struct then "T?" starts meaning "the nullable type corresponding to T" again.

Thanks to JonasH's comment I came up with a solution/workaround. I created a custom Nullable<T> type with an implicit conversion from T? to Nullable<T> so that I can use almost the same syntax in construction of GenericClass<...> as I used in my question:

struct Nullable<T>
{
    public static Nullable<T> Null => new Nullable<T>(false, default(T)!);
    public bool HasValue { get; init; }

    private T? _value;
    public T Value
    {
        get => HasValue ? _value
            : throw new System.InvalidOperationException("no value present!");
        init => _value = value;
    }

    private Nullable(bool hasValue, T value)
    {
        HasValue = hasValue;
        _value = value;
    }

    public static implicit operator Nullable<T>(T? n)
        => n == null ? Null : new Nullable<T>(true, n);

}

The two problematic lines

var vStruct = new GenericClass<bool>(null, true);
var vEnum = new GenericClass<Color>(null, Color.red);

still throw CS1503, but I can change them to

var vStruct = new GenericClass<bool>(Nullable<bool>.Null, true);
var vEnum = new GenericClass<Color>(Nullable<Color>.Null, Color.red);

and it works. I could do the same for the other lines, but I do not need to; the implicit conversion is used there.

Kjara
  • 2,504
  • 15
  • 42
  • `public static Nullable NULL => new Nullable(false, default(T)!);` can be just `public static Nullable Null => default;` btw. (Also, don't use ALL_CAPS names for public types and members in C# or .NET) – Dai Jun 15 '22 at 11:51
  • 2
    A tip, create a non generic `Nullable`-type without any fields and a static `Nullable Null = new ()` property, with a implicit conversion operator from this type to your `Nullable`, allowing you to write `new GenericClass(Nullable.Null, true)`. – JonasH Jun 15 '22 at 12:04
  • 1
    @JonasH or even easier: create an `enum Nullable { Null }`. Then add the implicit conversion `public static implicit operator Nullable(Nullable n) => Null;` to `Nullable`. – Kjara Jun 16 '22 at 12:06