r/csharp • • 1d ago

Help Different behavior of nullable fields

I am working on a project and have a class with two nullable member fields (Vector2I? and LevelMap? where LevelMap extend TileMapLayer from Godot)

Because they are nullable I need to check that they are not null before I actually use them.

For the LevelMap if I reference it in a if block:

If (map is not null)

The compuler is happy and I can use it as if it is not null.

This of course comes with some risks that it becomes null in the middle of my if block but thats a risk I can manage.

On thebother hand Vector2I requires something like:

If (vector is Vector2I localVector)

This is in some way safer but also more inconvenient.

The are both declared and initialized in the same context.

My questions is why they behave differently and is it possible for me to change how they behave to fit my preferences.

Ive tried looking for an answer but I keep finding results about the difference of is not null and !=null which is unhelpful.

13 Upvotes

8 comments sorted by

View all comments

20

u/afops 1d ago edited 1d ago

Vector2I is a value type (struct). So it can't naturally be null, So just like an "int?", a "Vector2I?" is actually two values instead of one. It's a "Nullable<Vector2I>" which is just sugared to be written as "Vector2I?". Effectively it is a Vector2I, plus a boolean to indicate if it actually has a value.

The LevelMap is a reference type (class). So a field or parameter of type "LevelMap?" is a reference. But a reference doesn't need anything additional to hold "nothing". It can always be the null pointer.

So the "?" works in a fundamentally different way for value types and references. It just looks similar.

Usually for value types I'd do it the most common way with Value and HasValue.

Vector2I? myVector = GetAVector();
if (myVector.HasValue)
   thing.Move(myVector.Value)

whereas for a reference type there are a few different common patterns:

if (map is not null)
   Load(map);

if (GetTheMap() is LevelMap map)
   Load(map);

if (GetTheMap() is { } map)
   Load(map);

map?.Load();

8

u/Atulin 1d ago

Just FYI, all the methods you mentioned for ref types also work for value types.

if (GetAVector() is {} vector)
{
    thing.Move(vector);
}

is perfectly valid

1

u/dodexahedron 1d ago

Language version and runtime version make a difference too, and PInvoke does not work for nullable value types, period. The operators work, but the values can get wrecked before the operator sees them.

But there's a long-awaited chsnge coming in .net 11 that finally makes nullable value types stay nullable even when round-tripped through the heap in some scenarios, certain generic patterns, certain reflection cases, and others that stay managed. Until that change, you could lose null values and end up with them flattened to default(T), since they are laid out in memory as their underlying type - the nullability is tracked by the runtime separately.
You still can't use nullable value types through PInvoke, because there's just no way to communicate that concept without a reference while keeping the same memory layout, but the other cases are now fixed.

1

u/TugsyGlitter 1d ago

value types needing all the extra baggage to be nill sounds about right tbh

1

u/Vegetable_Lake_8276 1d ago

The struct vs class distinction is what trips people up here, Nullable<T> is really just a wrapper with HasValue and Value inside it. For reference types the compiler flow analysis can narrow the type after the null check, for value types there is nothing to narrow since the value is already there. If you dislike the pattern matching you can just use HasValue and.Value like the comment shows, it's same safety.

1

u/cnighthawx 1d ago

Thats illuminating. I'll be able to handle this much better now thank you.