I am trying C# 8.0 out, and I want to enable the null reference checking for the entire project. I am hoping I can improve my code design, and without disabling the nullabil
You’re concerned that while your objects aren't intended to have null
members, those members will inevitably be null
during the construction of your object graph.
Ultimately, this is a really common problem. It affects, yes, deserialization, but also the creation of entities, data transfer objects, and view models. Often, these members are to be null for a very brief period between constructing the object and setting the properties. Other times, they might sit in limbo during a longer period as your code e.g. fully populates a dependency data set, as required here with your interconnected object graph.
Fortunately, Microsoft has addressed this exact scenario, offering us two different approaches.
The first approach, as @andrew-hanlon notes in his answer, is to use the null-forgiving operator. What may not be immediately obvious, however, is that you can use this directly on your non-nullable members, thus entirely eliminating your intermediary classes (e.g., SerializedPerson
in your example). In fact, depending on your exact business requirements, you might be able to reduce your Person
class down to something as simple as:
class Person
{
internal Person() {}
public string Name { get; internal set; } = null!;
public IReadOnlyList<Person> Friends { get; internal set; } = null!;
}
The second approach gives you the same exact results, but does so by providing hints to Roslyn's static flow analysis via nullable attributes. These require more annotations than the null-forgiving operator, but are also more explicit about what's going on. In fact, I actually prefer this approach just because it's more obvious and intuitive to developers otherwise unaccustomed to the syntax.
class Person
{
internal Person() {}
[NotNull, DisallowNull]
public string? Name { get; internal set; };
[NotNull, DisallowNull]
public IReadOnlyList<Person>? Friends { get; internal set; };
}
In this case, you're explicitly acknowledging that the members can be null
by adding the nullability indicator (?
) to the return types (e.g., IReadOnlyList<Person>?
). But you're then using the nullable attributes to tell consumers that even though the members are marked as nullable:
[NotNull]
: A nullable return value will never be null.[DisallowNull]
: A nullable input argument should never be null.Regardless of which approach you use, the end results are the same. Without the null-forgiving operator on a non-nullable property, you would have received the following warning on your members:
CS8618
: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.
Alternatively, without using the [NotNull]
attribute on a nullable property, you would have received the following warning when attempting to assign its value to a non-nullable variable:
CS8600
: Converting null literal or possible null value to non-nullable type.
Or, similarly, upon trying to call a member of the value:
CS8602
: Dereference of a possibly null reference.
Using one of these two approaches, however, you can construct the object with default (null
) values, while still giving downstream consumers confidence that the values will, in fact, not be null
—and, thus, allowing them to consume the values without necessitating guard clauses or other defensive code.
Conversely, when using either of these approaches, you will still get the following warning when attempting to assign a null
value to either of these members:
CS8625
: Cannot convert null literal to non-nullable reference type.
That's right: You'll even get that when assigning to the string?
property because that's what the [DisallowNull]
is instructing the compiler to do.
It’s up to you which of these approaches you take. As they both yield the same results, it’s a purely stylistic preference. Either way, you’re able to keep the members null
during construction, while still realizing the benefits of C#’s non-nullable types.
While certainly highlighted due to non-nullable defaults in C# 8, this is really a common 'circular dependency' problem that has always existed with interdependent construction.
As you found, one standard solution is to use a restricted setter. With C# 8, you may want to use a 'null-forgiving' null!
during deserialization of the object graph - allowing you to differentiate an unconstructed set from a valid empty set, and minimize allocation.
Example:
class Person
{
internal Person(string name, IReadOnlyList<Person> friends)
{
Name = name; Friends = friends
}
public string Name { get; }
public IReadOnlyList<Person> Friends {get; internal set;}
}
class SerializedPerson { ... }
IEnumerable<Person> LoadPeople(string path)
{
var serializedPeople = LoadFromFile(path);
// Note the use of null!
var people = serializedPeople.Select(p => new Person(p.Name, null!));
foreach(var person in people)
{
person.Friends = GetFriends(person, people, serializedPeople);
}
return people;
}
If I understand correctly, your question boils down to:
How do I avoid compiler warnings
CS8618
: Non-nullable property 'Name' is uninitialized. Consider declaring the property as nullable.
for simple model classes which are used for serialization?
You can solve the problem by creating a default constructor for which you suppress the warning. Now you want to make sure that this constructor is only used by your deserialization routine (e.g. System.Text.Json
or Entity Framework). To avoid unintentional use add annotation [Obsolete]
with parameter error=true
which would raise compiler error CS0618.
As code:
public class PersonForSerialize
{
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
[Obsolete("Only intended for de-serialization.", true)]
public PersonForSerialize()
#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
{
}
// Optional constructor
public PersonForSerialize(string name, IReadOnlyList<string> friends)
{
Name = name;
Friends = friends;
}
public string Name { get; set; }
public IReadOnlyList<string> Friends { get; set; }
}
Note1: You can let Visual Studio auto-generate the optional constructor using a quick action.
Note2: If you really mean to use the constructor marked as obsolete, you need to remove the error=true
parameter. Now you can suppress the warning when calling the parameter-less constructor via #pragma warning disable CA1806
.