Expressing intent
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:
GetFooList
is a static method that returns a nullable type,List<FooBar>?
.fooList
is assigned the value returned byGetFooList
.- The compiler generates a warning on
fooList.Find(f => f.Name == "Bar");
because the value assigned tofooList
might benull
. - Assuming
fooList
isn'tnull
,Find
might 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
salesTax
parameter is defined as being a nullableIStateSalesTax
. - Within the method body, the
salesTax
is conditionally assigned using the null-coalescing operator.- This ensures that if
salesTax
was passed in asnull
that 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 source
parameter that is defaulted tonull
. - The wrapped
_source
is 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 ofToString
to thestr
variable.- The
str
variable is of typestring?
(nullable string).
- The
- It writes the value of
str
to 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.