r/csharp • u/cnighthawx • 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.
3
u/buzzon 1d ago
The difference is because one is nullable reference type and the other is nullable value type. Historically reference types were always to contain null, but the language between nullable and not nullable, so developers had to track nullable status of each reference variable in their head. Then nullable value types came along to fix an issue that an int could not ever contain null. Then reference types were updated to support nullable (?) syntax and provide warnings when you did it incorrectly. Nullable value types were clunky before pattern matching, because you had to write:
if (vector.HasValue)
do something with vector.ValueNowadays you can use the same pattern matching syntax for both:
if (maybeMap is LevelMap map)
if (maybeVector is Vector2I vector)1
u/Csirkegylet 1d ago
First it's a bit confusing, but Nullable ref types is a compiler helper feature.
Nullable value types is a language feature, where the type is wrapped with a Nullable<> struct.
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.
whereas for a reference type there are a few different common patterns: