3

Why can't I fill the default generic property with the value null if the property is nullable and the generic type is not restricted in any way?

public class Class_0<TValue> where TValue : struct
{
    public TValue? Value { get; } = null; // is valid
}

public class Class_1<TValue> where TValue : class
{
    public TValue? Value { get; } = null; // is valid
}

public class Class_2<TValue>
{
    public TValue? Value { get; } = null;  // error
}

I need the property to be able to be either struct or class and always be filled with the value null. I can't write something like this: where TValue : struct, class.

4
  • 1
    What error do you get? I suspect it's only something like "Do not initialize values to their defaults", which is just a code-style warning. Commented Mar 10, 2024 at 17:54
  • @PMF "Cannot convert 'null' to type parameter 'TValue' because it could be a value type. Consider using 'default(T)' instead" Commented Mar 10, 2024 at 17:56
  • @PMF But if you put the default - public TValue? Value { get; } = default; then new Class_2<int>() will write null value 0 instead of null to the nullable property. Commented Mar 10, 2024 at 17:58
  • 2
    It's not as easy as I thought, sorry. It seems it's not possible to have a generic property that can both be a nullable value type and a reference type at the same time. Commented Mar 10, 2024 at 18:11

3 Answers 3

3

I would do that:

public class Class_2<TValue>
{
    public TValue Value { get; } = default!;
}

And then using it like that:

var o = Class_2<int?>();
var o = Class_2<int>();

var o = Class_2<object?>();
var o = Class_2<object>();

The reason is in dotnet refrence types (string, object,..) are nullable. Adding the question mark at the end (string?, object?,..) doesn't change their type. It just tell the IDE to remind you to check for nullabity.

public class Class_1<TValue> where TValue : class
{
    public TValue? Value { get; } = null;
}

On the other hand adding a question mark on struct types (int, long,..) change their type to (Nullable<int>, Nullable<long>).

public class Class_0<TValue> where TValue : struct
{
    public TValue? Value { get; } = null;
    public Nullable<TValue> SameAs { get; } = null;
}

In the last implementation when you declare a generic type parameter without any constraints, it is implicitly assumed to have the "class" constraint. This is why the default keyword was equivalent 0 and null was not accepted.

public class Class_2<TValue>
{
    public TValue? Value { get; } = null;
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the reply. But putting the responsibility for wrapping the value in nullable on the consumer of the class doesn't suit me.
1

This happens because of a leaky abstraction. The C# compiler's convenient ? syntax hides the fact that the types TValue? where TValue : class and TValue? where TValue : struct are incompatible CLR types. The first one is just TValue: the nullable annotation on reference types is not part of the CLR type system and is implemented as a bunch of attributes the compiler is aware of. The second one is the generic value type Nullable<TValue>. Given a single declaration like your Class_2<TValue>, the compiler cannot emit a CLR generic type declaration that would behave like both, because TValue? would need to be represented as !0 in the first case and System.Nullable`1<!0> in the second (the IL syntax !0 refers to the first generic argument of the active generic type context, in this case Class_2`1). So what does the compiler do? If you open your example in ILDasm, you will see that

public class Class_2<TValue>
{
    public TValue? Value { get; }
}

compiles to the generic type Class_2`1<TValue> with a property and a backing field of type TValue without any nullability annotations, i.e. the compiler just gives up and ignores the ? (I am using .NET 8). This may not be the ideal way of dealing with the situation, but it is the only one that provides backward compatibility. And unfortunately this means that you cannot write a single C# generic that would do what you want: you will have to have separate declarations for reference and value type arguments, or give up on built-in nullability and implement it manually.

2 Comments

Thanks for the reply. Another problem with the proposed way (besides implementation duplication) is that c# doesn't allow overloading classes and methods by a generic type, which means I can't have 2 identical entry points for my logic (two classes with the same names but generic delimited - one for structures, one for classes).
Right. It's annoying but there is nothing we can do. Entry points have to stay separate if you want them to use language-native nullability constructs. But you can often put your core logic into a common generic base class with only a little overhead, using something like internal readonly record struct MyNullable<T> (T Value, bool HasValue) if your core logic needs to pass around the value rather than just the null/non-null tag.
0

I think this achieves what you want. It accepts either a struct or a class as the generic type, and the compiler enforces nullability only where necessary.

(bear with me and ignore the T? for now)

Solution

public class GenericClass<T>
{
    public T? Value { get; } = default;
}

Validation

The compiler correctly hints to you whether to expect a nullable object back, or whether it's a non-nullable struct. Try each of the following, and notice what it initialises Value to:

var withNonNullableString = new GenericClass<string>(); // Value: null (caution!)
var withNullableString = new GenericClass<string?>(); // Value: null
var withNonNullableInt = new GenericClass<int>(); // Value: 0
var withNullableInt = new GenericClass<int?>(); // Value: null

Now, about the T? when using a struct. If you try this in a compiler, you'll note that the withNonNullableInt knows that withNonNullableInt.Value isn't nullable. Which is pretty smart.

So, I think it's achieving what you want. A generic class that behaves using the correct null / non-null semantics, depending if you pass it a class or a struct as the generic type.

Couple of things to be aware of

  • I put "caution!" above, because when using a non-nullable string as the generic type, Value will still be nullable. It's OK though; the compiler does correctly warns you that the .Value is in fact nullable.

  • This is all assuming you've got nullable type enabled.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.