Throughout this chapter, we have consistently disabled the C# nullable warning:
CS8618: Non-nullable field/property is uninitialized. Consider
declaring as nullable.
When you declare reference type (1) non-nullable fields or (2) non-nullable automatically implemented properties, it is obvious that these fields and properties need to be initialized before the containing object is fully instantiated. Not doing so would leave those fields and properties with a default null value—so they shouldn’t be declared as non-nullable.
The problem is that frequently the non-nullable fields and properties are initialized indirectly, outside the immediate scope of the constructor and therefore beyond the scope of the compiler’s code analysis, even if they are still, in fact, initialized, perhaps via a method or property that the constructor invokes.10 Following are some examples of this practice:
In most cases, the reference type non-nullable field or non-nullable automatically implemented property (referred to as a non-nullable field/property in this section—“reference type” is implied) is assigned indirectly via properties or methods that the constructor invokes. Unfortunately, the C# compiler doesn’t recognize an indirect assignment of a non-nullable field/property.
Furthermore, all non-nullable fields/properties need to ensure that they are not assigned a value of null. In the case of fields, they need to be wrapped in properties with the setter validation ensuring that a null value will not be assigned. (Remember that field validation relies on the guideline that we do not access fields outside of the property that wraps them.) The result is that non-nullable read-write fully implemented reference type properties should have validation preventing null assignment.
Non-nullable automatically implemented properties need to be limited to read-only encapsulation, with any values assigned during instantiation and validated as not null prior to assignment. Read-write non-nullable reference type automatically implemented properties should be avoided, especially with public setters, since preventing null assignment is problematic. Although the uninitialized non-null property compiler warning can be avoided by assigning the property from the constructor, this is not enough: The property is read-write, so it could be assigned null after instantiation, thereby voiding your intent for it to be non-nullable.
Listing 6.36 demonstrates how to inform the compiler and avoid the false warning that a non-nullable field/property is uninitialized. The end goal is to allow the programmer to inform the compiler that the properties/fields are non-nullable so that the compiler can inform callers about the (non-)nullability of those properties/fields.
The code snippet to handle non-nullable properties/fields that are not directly initialized by the constructor has several important qualities (listed in no particular order here):
For a non-nullable property, it is seemingly nonsensical to declare the backing field as nullable. This is necessary, however, since the compiler is oblivious to non-nullable field/property assignments outside from the constructor. Fortunately, this is a case where you, as the programmer, are justified in using the null-forgiveness operator when returning the field because of the not-null check in the setter that ensures the field is never null.
As pointed out earlier in this section, non-nullable automatically implemented reference-type properties need to be read-only to avoid invalid null assignments. However, you still need to validate any parameters that may be assigned during instantiation, as shown in Listing 6.37.
One could debate whether a private setter should be allowed on non-nullable automatically implemented reference-type properties. While possible, the more appropriate question to ask is whether your class could mistakenly assign null to the property. If you don’t encapsulate the field with validation in a setter, can you be sure you won’t mistakenly assign a null value? While the compiler will verify your intent during instantiation, it is questionable that the developer will always remember to check for null in values coming into your class that should be non-nullable—as occurs in the constructor shown in Listing 6.37.
In C# 11, the designers add the ability to mark either a field or a property as required. This designates them as “required” such that they must be assigned inside the object initializer during construction (see Listing 6.38).
By adding the required modifier, we can no longer instantiate a book without supplying an object initializer that provides value for both the Isbn number and the Title (see Listing 6.39).
Notice that since we don’t include an explicit constructor for Book, we rely on the automatically generated default constructor to instantiate the book. This is ideal since the required members define how to construct the object in place of any constructor. Providing a constructor with parameters for the required members will result in having to specify the values both as constructor arguments and, redundantly, in the object initializer. A constructor with a Title parameter, for example, will result in an instantiation like this:
Book book = new("A People’s History of the United States")
{
Title= "A People’s History of the United States",
Isbn="978-0062397348"
};
To avoid this redundancy, you can decorate a constructor with the SetRequireParameters attribute, instructing the compiler to disable all the object initializer requirements when invoking the associated constructor. (Effectively, the SetRequireParameters attribute instructs the compiler that the developer will take care of setting all the required members so the compiler can ignore checking for initialization assignment. Unfortunately, however, there is little to no verification that such initialization did occur—the compiler doesn’t check. See Listing 6.40 for an example of how to use the SetsRequiredMembers attribute.
And, given such a constructor, we can instantiate a book without setting any required members:
Book book = new(42) {
Subtitle = "The Comprehensive, Expert Guide " +
"to C# for Programmers at All Levels" };
The disadvantage, of course, it this assumes that the constructor has valid values to assign the required members. There is no point in telling the compiler to ignore the required members if in fact there are no available ways to determine the valid required member values. Doing so will allow the constructor invocation to not set the required members while leaving invalid values. Furthermore, is there enough of an advantage in using a constructor to set non-required rather than just allowing them to be set via an object initializer. On the flip side, obviously don’t mark a value as required when the default value of the data type is valid.
As you would expect, you cannot have a required public type with a private setter. Rather, the setter on a required member must match the visibility of the type that contains the member. Since Book is public, its Isbn property setter must also be public.
One last important point to note. Adding a required member after your class is already published to production will cause existing code that instantiates the type to no longer compile because the added required members will need to be specified in the object initializer. This makes the new version incompatible with existing code; therefore, you should avoid it.
________________________________________