On Wednesday, 30 April 2025 at 07:17:37 UTC, Kagamin wrote:
> Finally I had my chance to cope with nullable types in one of our C# codebases, and the experience wasn't nice.
That's what happens when you convert an old code base assuming nullability everywhere. And that's a good thing
> I had a prejudice that nullable types give some kind of promise that you will have only a few of them, but now that I think about it I can't remember anyone making this promise, and reality is quick to shatter this prejudice in a very ugly way.
That's simply not true and it's dependent of the nullable context. By default, the nullable context is set to consider reference types as not nullable. Probably you are complaining about uninitialized references.
SomeClass c1; // this cannot be null
SomeClass? c2 // this can be null
In the first case, the compiler will complain if you dare to not initialize c1 through flow analysis, and will not bother you to check for nullability in your code. In the second case the compiler will let you use c2 uninitialized, but will tap you on the shoulder if you try to use it without checking for null.
> If you have a nullable type, all code now sees it as nullable, and you must insert null checks everywhere, even if you know it's not null by that point, and the volume of this null check spam is uncomfortably large.
Please read again your own words, "if you have a nullable type, all code now sees it as nullable".
I find it normal, sometimes the compiler is not smart enough to determine if a null check is needed or not, but you can help him assuming the responsability. You can do it bluntly by using the notnull postfix operator (!) or by decorating your functions with specific attributes like NotNull or NotNullWhen.
> It's also not very clear what's
the difference between me spamming null checks everywhere by hand and processor doing the same automatically.
Nullable types are retrofitted in dotnet and all legacy interfaces return nullable types, this greatly increases number of null checks.
DTOs are destroyed. How can you even have a DTO with nonnullable field? Initialize it to a stub value in default constructor?
- You can use the new
required
keyword.
- You can use the
init
property setter.
- You can initialize it in the constructor.
- You can provide a default value.
- You can declare it as nullable reference (?).
Depending on your use case, choose the best for you. Irrespective of the path, the compiler never fails to clearly identify in this case if you need a null check or not.
> That's 1) NullObject pattern, 2) allocates extra
garbage. The number of null checks is greatly increased.
Null check operator has pretty high precedence. If you want to null check an expression, you'll need to surround it with braces, this is especially annoying with await expression as checking the result of await is the best place for null check. Exclamation also looks like negation, imagine parsing if(a! == b)
, this happens unexpectedly often.
I agree that the ?? operator precedence is surprising, but the compiler will not let you go without a null check if it's not 100% sure that a value is not null. That's the purpose of nullable references check.
You don't need a! == b
, you can use directly a == b
, there is no null check required here for the comparison to mandate the use of notnull operator (!)
Not null operator (!) is needed in two cases:
- When you dereference a nullable reference and the compiler cannot guarantee 100% that your reference cannot be null:
someNullable!.SomeProp
- When you pass a nullable reference to a function which does not accept nullable references:
func(someNullable!)
. Again, only if the compiler is not smart enough to guarantee 100% that someNullable
is not null before the call through flow analysis.