Skip to content

Commit

Permalink
clean up of headers
Browse files Browse the repository at this point in the history
fixup some content
  • Loading branch information
RLittlesII committed Jan 2, 2025
1 parent 45d25d0 commit 3c78f37
Showing 1 changed file with 55 additions and 45 deletions.
100 changes: 55 additions & 45 deletions drafts/posts/null-is-the-absence-of-a-thing.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,31 @@ Tags:
- Nullability
---

# Null, the mistake we still have not learned
## Null, the mistake we still have not learned

[Null Reference](https://en.wikipedia.org/wiki/Null_pointer) is what Tony Hoare called his billion dollar mistake. I always say that learning from your mistakes makes you smart, learning from others makes you wise. So this post is going to try and impart some wisdom from what I have learned turning on the C# 8 Nullable Reference Type feature in large codebases.

### Disclaimer
#### Disclaimer

This is not a critique of the feature itself. This is about `null`, how to take advantage of the feature, and things I think I know after doing this a few times.
This is **not** a critique of the feature itself. This is about `null`, how to take advantage of the feature, and things I think I know after enabling the feature a few times.

# The Absence of a Reference
## The Absence of a Reference

![null toilet paper](../../src/images/some.none.null.png)

image borrowed from [another blog](https://blog.matesic.info/image.axd?picture=/Blog%20posts/2019/NULL/Papers1.png)

The above image demonstrates the "absensce of a reference". There is a distinct difference between "No Paper" and "Null". Notice that in the `null` case there is no reference or understanding there is paper at all! It could be an image of a hand towel holder. There is no reference to the entity we care about. `null` doesn't just mean you have none of a given type, it means there is no actual connection to the type at all. So when we return `null` prior to turning on Nullable Reference Types, we are technically violating the contract we have established with our consumer.
The above image demonstrates the "absensce of a reference". There is a distinct difference between "No Paper" and "Null". Notice that in the `null` case there is no reference or understanding there is paper at all! It could be an image of a hand towel holder. There is no reference to the entity we care about. `null` doesn't just mean you have none of a given type, it means there is no actual connection to the type at all. So when we return `null` prior to turning on Nullable Reference Types, we are potentially violating the contract we have established with our consumer.

The language feature solves this by allowing developers to provide a type for the reference you expect. Sort of a type safe `null`. The compiler can now understand `null` in the context of `type` because every `type` has _nullabilty_; the ability to be `null`. Declaring `Nullable<T>` (`T?`), you are communicating to the compiler you expect it to handle the _null state_ for the provided `type`. If you attempt to assign `null` to `T`, the compiler complains because `T` does not have `?` which provides access to the _null state_ .
The language feature solves this by allowing developers to provide a type for the reference you expect. Sort of a type safe `null`. The compiler can now understand `null` in the context of `type` because every `type` has _nullability_; the ability to be `null`. Declaring `Nullable<T>` (`T?`), you are communicating to the compiler you expect it to handle the _null state_ for the provided `type`. If you attempt to assign `null` to `T`, the compiler complains because `T` does not have `?` which provides access to the _null state_ .

## Returning `null`
### Returning `null`

When you return `T?`, you are forcing any consumer of that return value to verify the _nullabilty_ and the _null state_ of what you provide them. Whether it's a method or a property, every inspection will require an evaluation of the _null state_. Before this feature, you didn't have the information available at compile time, and lack of doing an evaluation would result in a [`NullReferenceException`](https://learn.microsoft.com/en-us/dotnet/api/system.nullreferenceexception)
When you return `T?`, you are forcing any consumer of that return value to verify the _nullability_ and the _null state_ of what you provide them. Whether it's a method or a property, every inspection will require an evaluation of the _null state_. Before this feature, you didn't have the information available at compile time, and lack of doing an evaluation would result in a [`NullReferenceException`](https://learn.microsoft.com/en-us/dotnet/api/system.nullreferenceexception).

## `null` method arguments
### `null` method arguments

When accepting `T?` as a method argument you are taking in something that is potentially `null`. You don't know the context of why it's null, so if your method requires the value to exist, you can check for null and throw.
When accepting `T?` as a method argument you are taking in a value that is potentially `null`. You don't know the context of why it's null, so if your method requires the value to exist, you can check for null and throw.

Take the MAUI current application

Expand All @@ -58,15 +58,15 @@ Application.Current.SomeExtension(); // if this is null for a reason we don't e
```

# Turning on C# Nullability
## Turning on C# Nullability

Turning on C# Nullable Reference Types is like shining a spot light on all the imperfections in a codebase. The feature is fully analyzed by the compiler. This means that once you turn it on, every oppportunity for you to access an object with _null state_ has to be either guarded against or tolerant of `null`. As a result you can turn the feature on at the assembly level, or at the file level. I would recommend starting small and understanding how hidden the absence of state is prevelant in the code base you turn it on for.

## Handle `null`
### Handle `null`

Unhandled `null` returns generally result in [`System.NullReferenceException`](https://learn.microsoft.com/en-us/dotnet/api/system.nullreferenceexception). When defining an API surface with nullability turned on you cannot return `null` from a method or property without making the return value nullable. Once you change the type to `T?`, you will get compilation errors (with the warnings as errors) when you are potentially creating a `NullReferenceException`. This will allow you to handle these concerns accordingly and evaluate if this is recoverable for your application.

## Don't return `null` if you can return a default value
### Don't return `null` if you can return a default value

Given the below signature we could optimize the implementation (and solidifying the contract), by changing it.

Expand All @@ -88,17 +88,17 @@ public IEnumerable<Thing> Things()
}
```

In this we have provided a default value for the return. Our consumer now only needs to take the return value and start processing. They don't have to check for the existence of a value, we've guarnteed _something_ gest returned.
In this we have provided a default value for the return. Our consumer now only needs to take the return value and start processing. They don't have to check for the existence of a value, we've guarnteed _something_ gets returned.

## `bool?` is not the new "3 way state"
### `bool?` is not the new "3 way state"

Yes, the _null state_ creats a built in 3 state `enum`. I know it's tempting to use this in places. Resist. While it is fine for some use cases, it is not scalable. The second you need a fourth value, you have a lot of code to modify.

## methods can accept null arguments and still guard against them being null
### methods can accept null arguments and still guard against them being null

Back to our `Application.Current?` example, you don't expect it to be null, but it _can_ be null. If you don't want it to be null when you are using it, you should guard against it. We don't want the objects _null state_ to potentially bite us, we have to take control over it.
Back to our `Application.Current?` example, you don't expect it to be null, but it _can_ be null. If you don't want it to be null when you are using it, you should guard against it. We don't want the objects _null state_ to potentially bite us, we have to take responsibility for it.

## nullable default method parameters are fine internal to a system, but shouldn't cross "subsystem" boundaries
### nullable default method parameters are fine internal to a system, but shouldn't cross "subsystem" boundaries

This next tip is more for developers making public API's that others consume. Given the method with the following signature. If in the future I decide to change `bool? shouldCare = null` to `bool? shouldCare`, by the rules of semver, this is a breaking API change. It's not the end of the world, but I have made this mistake and burned a major version for no reason!

Expand All @@ -109,39 +109,40 @@ public interface IDoStuff
}
```

## Use `System.Diagnostics.CodeAnalysis` attributes
### Use `System.Diagnostics.CodeAnalysis` attributes

[`System.Diagnostics.CodeAnalysis`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.codeanalysis) has attributes you can use to decorate your API surface, to indicate the presence of the _null state_. They are supported by the compiler. If you know your method _might_ return `null`, consider using `MaybeNullAttribute`. If you want to state an argument `is not null` you can use the `NotNullAttribute`.

## Encapsulate your nullability, don't force every consumer to check for null if you can guard against it for them
### Encapsulate your nullability, don't force every consumer to check for null if you can guard against it for them

If you have a `private` method that returns `null`, consider checking that return if you don't really want it to be `null`. You can capture the _null state_ and throw an exception with meaning. You can return a default value
If you have a `private` method that returns `null`, consider checking that return if you don't really want it to be `null`. You can capture the _null state_ and throw an exception with meaning. You can return a default value. You can ensure t hat the private `null` value doesn't bubble up to the public API surface. There are a lot of options. The option you should avoid if possible, letting the `null` return run rampant all the way through your call stack.

## Don't act like it's not there. This "see no evil" approach causes defects
### Don't act like it's not there. The "see no evil" approach causes defects

- C# 8 feature
- Turning on the feature will help you appreciate how the absence of a thing™ can severly hurt your software
Turning on the Nullable Reference Type feature will help you appreciate how the absence of a thing™ can severly affect your software. There are times where the business expects a value to exist, but the software doesn't provide a value. Remember some of us are writing software for users to consume. It has to behave and function a certain way based on a set of requirements. So when it doesn't behave how we expect, we have to figure out why. Ensuring your business logic properly accounts for and handles _null state_ is important for long term system health.

## Only Return Null When
### Only Return Null When

- Doing so won't kill your application ... yes ... I have seen devs do this
- The application can recover from the null value being passed
- Doing so won't kill your application
- The application can recover from the _null state_ inspection
- You expect every consumer of the method to gracefully handle a `null` return value

## The `default` value of a reference object is `null`
### The `default` value of a reference object is `null`

Next we'll talk about `default`. In general value types have a value. The `default(int)` is `0`. So if you create a reference type, guess what the `default` value is? That's right `null`. That means we can't return things like `default(T)!` and expect the application not to throw `NullReferenceException`. This was one of those no brainer
Next we'll talk about `default`. In general value types have a value. The `default(int)` is `0`. So if you create a reference type, guess what the `default` value is? That's right `null`. That means we can't return things like `default(T)!` and expect the application not to throw `NullReferenceException`. This was one of those no brainer things when I thought about it. But I had to think about it. This means that every reference type by default is `null`. Not just once I turn on the feature, it has always been and I haven't always ensured there is a default value.

## `OrDefault()` methods return `null`
It's worth talking about [sentinel values](https://en.wikipedia.org/wiki/Sentinel_value) here. I haven't historically been a fan of them in C#. The problem is checking for _null state_ doesn't actually guarntee the default value. It's just the default of a constructed object is `null`. So while I am not a fan of sentinel values, I do think there is a place for a type having a `Default`.

### `OrDefault()` methods return `null`

- SingleOrDefault()
- FirstOrDefault()

These methods return the `default` value if one is not found. The `default` value for a reference type with _null state_ is `null`. So if you don't guard the return value, you could have `NullReferenceException` problems all over your code.

# Tips for handling `null` in your code
## Tips for handling `null` in your code

## File or Assembly at a time
### File or Assembly at a time

I would recommend a few approaches

Expand All @@ -152,35 +153,44 @@ I would recommend a few approaches
- If you have multiple assemblies (like a lot of .NET applications I have seen)
- Start with a single assembly

## Start with the edges of the application either the data layer or the UI layer
### Start with the edges of the application either the data layer or the UI layer

I wouldn't recommend jumping in at the beginning with your business logic. It becomes overwhelming to retro fit at first. I recommend starting with your data layer, or your UI layer. These are usually the most straight forward and forgiving from what I have seen. Basically you want to pick an edge and work your way in, starting in the middle gets weird.

## Small commits so you can easily walk backwards
Make small commits. As you open up a cohesive area, commit. I have backed out adding _nullability_ realizing I can encapsulate a `null` object.
### Small commits so you can easily walk backwards

## Turn on C# nullability warnings as errors
Make small commits. As you open up a cohesive area, commit. I have backed out adding _nullability_ realizing I can encapsulate a `null` object. Don't rush to completion, take the time to understand what are the concerns and constraints of your system and assign and handle nullability accordingly.

### Turn on C# nullability warnings as errors

Read the list of [nullable warnings](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings) and chose which ones you want to make errors. I don't have a recommendation, I am still learning it depends on the code base which errors matter.

## Don't use Null Reference Exceptions as your catch all unhandled exception, it's not exceptional, you control it
### Don't use Null Reference Exceptions as your catch all unhandled exception, it's not exceptional, you control it

Inpsect objects that have _nullability_. Do not allow `NullReferenceException` to run rampant. They are not exceptional, nor are they informative. You are better off explicitly handling _nunllability_ and

Inpsect objects that have _nullability_. Do not allow `NullReferenceException` to run rampant. They are not exceptional, nor are they informative.
### Don't use nullable enums, rather default the enum with a `None` or `NA` or even `Default`

## Don't use nullable enums, rather default the enum with a `None` or `NA` or even `Default`
If you are using an `enum` I would suggest using a default value of `None` or `NA` for your `enum `. This will allow you to inspect anything as default
If you are using an `enum` I would suggets using a default value of `None` or `NA` for your `enum`. This will allow you to inspect anything as default

### Avoid `!` operator

## Avoid `!` operator
At first glance this operator seems like a good idea, but it defeats the purpose of the effort. This operator tells the compiler that you got it. You know that the compiler may _think_ it can be `null`, but you know it can't. This operator defeats the purpose of turning on this language feature in most places. The whole reason we are turning on the feature is so the compiler can help us identify potential `NullReferenceException`. Instead we use `!` all over the place and still end up with those exceptions. Use it sparingly. Initialization and in LINQ lambdas are the main places I find myself using it.

## Pay attention to methods that return null but could benefit from returning a `default`
### Pay attention to methods that return null but could benefit from returning a `default`

- `IEnumerable<T>?` => `[]`
- `bool?` => `false`

### use object initialization syntax

Some developers prefer constructors, some object initialization. If you prefer object initialization for concerns like data transfer objects, condsider using the [`init`](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/init) keyword. This will signal to the compiler that the object can should be initialized with a value, and that the value should not be null, because you cannot set it again after it has received it's initial value.

## for Dtos use `init` if you prefer object initialization syntax
### write some tests that verify behaviors

Some developers prefer constructors, some object initialization. If you prefer object initialization for concerns like data transfer objects, condsider using the `init` keyword. This will signal to the compiler that the object can should be initialized with a value, and that the value should not be null, because you cannot set it again after it has received it's initial value.
I am not going to tell you to test all the things™. I am going to tell you that verifying the behavior of something that returns `null` is a good idea. You want to know how your system responds to `null` inputs and outputs. You don't want to leave it to chance. A few well constructed tests can give you some clarity and potentially reduce regressions.

## Links
### Links

- [Nullable References Documentation](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references)
- [Nullable migration strategies](https://learn.microsoft.com/en-us/dotnet/csharp/nullable-migration-strategies?source=recommendations)

0 comments on commit 3c78f37

Please sign in to comment.