Expressing intent
- 5 minutes
In the previous unit, you learned how the C# compiler can perform static analysis to help guard against NullReferenceException. You also learned how to enable a nullable context. In this unit, you'll learn more about explicitly expressing your intent within a nullable context.
Declaring variables
With a nullable context enabled, you have more visibility into how the compiler sees your code. You can act upon the warnings generated from a nullable-enabled context, and in doing so, you're explicitly defining your intentions. For example, let's continue examining the FooBar code and scrutinize the declaration and assignment:
// Define as nullable
FooBar? fooBar = null;
Note the ? added to FooBar. This tells the compiler that you explicitly intend for fooBar to be nullable. If you don't intend for fooBar to be nullable, but you still want to avoid the warning, consider the following:
// Define as non-nullable, but tell compiler to ignore warning
// Same as FooBar fooBar = default!;
FooBar fooBar = null!;
This example adds the null-forgiving (!) operator to null, which instructs the compiler that you're explicitly initializing this variable as null. The compiler won't issue warnings about this reference being null.
A good practice is to assign your non-nullable variables non-null values when they're declared, if possible:
// Define as non-nullable, assign using 'new' keyword
FooBar fooBar = new(Id: 1, Name: "Foo");
Operators
As discussed in the previous unit, C# defines several operators to express your intent around nullable reference types.
Null-forgiving (!) operator
You were introduced to the null-forgiving operator (!) in the previous section. It tells the compiler to ignore the CS8600 warning. This is one way to tell the compiler that you know what you're doing, but it comes with the caveat that you should actually know what you're doing!
When you initialize non-nullable types while a nullable context is enabled, you may need to explicitly ask the compiler for forgiveness. For example, consider the following code:
#nullable enable
using System.Collections.Generic;
var fooList = new List<FooBar>
{
new(Id: 1, Name: "Foo"),
new(Id: 2, Name: "Bar")
};
FooBar fooBar = fooList.Find(f => f.Name == "Bar");
// The FooBar type definition for example.
record FooBar(int Id, string Name);
In the preceding example, FooBar fooBar = fooList.Find(f => f.Name == "Bar"); generates a CS8600 warning, because Find might return null. This possible null would be assigned to fooBar, which is non-nullable in this context. However, in this contrived example, we know that Find will never return null as written. You can express this intent to the compiler with the null-forgiving operator:
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!;
Note the ! at the end of fooList.Find(f => f.Name == "Bar"). This tells the compiler that you know that the object returned by the Find method might be null, and it's okay.
You can apply the null-forgiving operator to an object inline prior to a method call or property evaluation, too. Consider another contrived example:
List<FooBar>? fooList = FooListFactory.GetFooList();
// Declare variable and assign it as null.
FooBar fooBar = fooList.Find(f => f.Name == "Bar")!; // generates warning
static class FooListFactory
{
public static List<FooBar>? GetFooList() =>
new List<FooBar>
{
new(Id: 1, Name: "Foo"),
new(Id: 2, Name: "Bar")
};
}
// The FooBar type definition for example.
record FooBar(int Id, string Name);
In the preceding example:
GetFooListis a static method that returns a nullable type,List<FooBar>?.fooListis assigned the value returned byGetFooList.- The compiler generates a warning on
fooList.Find(f => f.Name == "Bar");because the value assigned tofooListmight benull. - Assuming
fooListisn'tnull,Findmight returnnull, but we know it won't, so the null-forgiving operator is applied.
You can apply the null-forgiving operator to fooList to disable the warning:
FooBar fooBar = fooList!.Find(f => f.Name == "Bar")!;
Note
You should use the null-forgiving operator judiciously. Using it simply to dismiss a warning means that you're telling the compiler not to help you discover possible null mishaps. Use it sparingly, and only when you are certain.
For more information, reference ! (null-forgiving) operator (C# reference).
Null-coalescing (??) operator
When working with nullable types, you may need to evaluate whether they're currently null and take certain action. For example, when a nullable type has either been assigned null or they're uninitialized, you may need to assign them a non-null value. That's where the null-coalescing operator (??) is useful.
Consider the following example:
public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
salesTax ??= DefaultStateSalesTax.Value;
// Safely use salesTax object.
}
In the preceding C# code:
- The
salesTaxparameter is defined as being a nullableIStateSalesTax. - Within the method body, the
salesTaxis conditionally assigned using the null-coalescing operator.- This ensures that if
salesTaxwas passed in asnullthat it will have a value.
- This ensures that if
Tip
This is functionally equivalent to the following C# code:
public void CalculateSalesTax(IStateSalesTax? salesTax = null)
{
if (salesTax is null)
{
salesTax = DefaultStateSalesTax.Value;
}
// Safely use salesTax object.
}
Here's an example of another common C# idiom where the null-coalescing operator can be useful:
public sealed class Wrapper<T> where T : new()
{
private T _source;
// If given a source, wrap it. Otherwise, wrap a new source:
public Wrapper(T source = null) => _source = source ?? new T();
}
The preceding C# code:
- Defines a generic wrapper class, where the generic type parameter is constrained to
new(). - The constructor accepts a
T sourceparameter that is defaulted tonull. - The wrapped
_sourceis conditionally initialized to anew T().
For more information, check out ?? and ??= operators (C# reference).
Null-conditional (?.) operator
When working with nullable types, you may need to conditionally perform actions based on the state of a null object. For example: in the previous unit, the FooBar record was used to demonstrate NullReferenceException by dereferencing null. This was caused when its ToString was called. Consider this same example, but now applying the null-conditional operator:
using System;
// Declare variable and assign it as null.
FooBar fooBar = null;
// Conditionally dereference variable.
var str = fooBar?.ToString();
Console.Write(str);
// The FooBar type definition.
record FooBar(int Id, string Name);
The preceding C# code:
- Conditionally dereferences
fooBar, assigning the result ofToStringto thestrvariable.- The
strvariable is of typestring?(nullable string).
- The
- It writes the value of
strto standard output, which is nothing. - Calling
Console.Write(null)is valid, so there's no warnings. - You would get a warning if you were to call
Console.Write(str.Length)because you'd be potentially dereferencing null.
Tip
This is functionally equivalent to the following C# code:
using System;
// Declare variable and assign it as null.
FooBar fooBar = null;
// Conditionally dereference variable.
string str = (fooBar is not null) ? fooBar.ToString() : default;
Console.Write(str);
// The FooBar type definition.
record FooBar(int Id, string Name);
You can combine operator to further express your intent. For example, you could chain the ?. and ?? operators:
FooBar fooBar = null;
var str = fooBar?.ToString() ?? "unknown";
Console.Write(str); // output: unknown
For more information, reference ?. and ?[] (null-conditional) operators.
Summary
In this unit, you learned about expressing your nullability intent in code. In the next unit, you'll apply what you've learned to an existing project.