Nullable Reference Types in Unity
We want to avoid null reference exceptions in C# code.
C# 8.0 introduced nullable and non-nullable reference types. Basically, it’s a way to say if it’s okay for your variables to be null or not. Static analysis is then used to point out issues. It makes it easier to avoid NullReferenceExceptions and avoids “scared code” that null checks everything.
I’ll walk you through my experience with it. To learn more about the concept, read here: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
Turning it on
Turn it on for an entire Unity project
Create a csc.rsp file next to the asmdef file for a project with this content:
Turn it on for a single cs file
Add the following to a .cs file:
Fields that have Unity’s [SerializeField] attribute will become non-null reference types and are expected to be initialized to something. They can’t, because the fields are of UnityEngine.Object type and can’t be default-initialized, they’ll only get their references set as part of the deserialization process.
- (Recommended) Suppress the rule if the reference type extends UnityEngine.Object.
- This has already been implemented in Microsoft.Unity.Analyzers using the DiagnosticSuppressor and works in Unity and should work in Visual Studio, but it doesn’t work in Rider because they don’t have support for the DiagnosticSuppressor concept. I have notified the Rider developers about the need for that here: https://youtrack.jetbrains.com/issue/RIDER-45021
- Suppress that specific rule.
- We can do that, but we are missing out on an important part of the puzzle that guarantees that the variable will not be null.
- Assign each field to null with the null-forgiving operator “!”
- “[SerializeField] GameObject prefab = null!;”
- This basically means that the field is non-nullable but we still assign null to it and ask it to shut up.
- As with other workarounds, we’re overriding the safety net that should guarantee that it’s non-nullable.
- Disable nullable reference type annotations for the [SerializeField] section.
- #nullable disable annotations
- [SerializeField] GameObject prefab;
[SerializeField] Material material;
#nullable restore annotations
- This gives the prefab reference type a nullability of Oblivious. Variables of this type can be dereferenced or assigned without warnings, so for those specific variables we won’t get any help from the static analysis.
Unassigned fields that get assigned in Init methods
Many Unity games have their own sort of “constructor” concept where they inject dependencies, instead of relying on the real constructor, since Unity will default-construct them.
These “constructors”, often called “Init” or similar, will then assign values to the fields.
From the compiler’s perspective, those non-nullable fields may still be unassigned when other methods would dereference them, and so it warns about that.
- (Recommended) Implement a DiagnosticSuppressor that suppresses the “unassigned warning” IF the field is assigned in an Init method. This also requires that Rider supports the DiagnosticSuppressor, as mentioned above.
- The other options are similar to the SerializeField problem above.
TryGet(string key, out string value) and similar APIs
Methods like these have more complex rules than the simple concept of nullable or non-nullable. When the method returns true, ‘value’ is guaranteed to not be null.
There are attributes available to annotate these complex rules in .NET Standard 2.1 but Unity 2020 only supports .NET Standard 2.0 for now. Update: It seems like Unity 2021 supports .NET Standard 2.1 now so this might be available there.
My recommendation is to roll out Nullable Reference Types slowly, area by area, by developers who feel comfortable with it. Wait with solution-wide enable until the mentioned problems have viable solutions.
- Nullable Reference Types can increase the code quality in certain areas.
- The developers need time to get used to the concept.
- Nullable Reference Types can be enabled in some parts of the code but disabled in others.