Why Null Handling Matters
Null reference exceptions are among the most common bugs in C# applications. Traditional null checking with if statements clutters code and makes logic harder to follow. Modern C# provides operators that handle null values concisely and safely.
The null-coalescing operator (??) and null-conditional operator (?.) let you write defensive code without verbose checks. These operators make your intent clear while preventing null-related crashes.
You'll learn how these operators work, when to use each one, and how to chain them together for clean null-safe code.
The Null-Coalescing Operator (??)
The ?? operator provides a default value when the left operand is null. It's perfect for assigning fallback values to variables that might be null.
// Traditional null check
string userName = GetUserName();
string displayName;
if (userName == null)
{
displayName = "Guest";
}
else
{
displayName = userName;
}
// Using ?? operator
string displayName = GetUserName() ?? "Guest";
// With nullable value types
int? userAge = GetAge();
int age = userAge ?? 18; // Use 18 if null
// Multiple null checks
string config = userConfig ?? defaultConfig ?? "fallback";
Console.WriteLine($"Name: {displayName}, Age: {age}");
The ?? operator evaluates the left side first. If it's not null, that value is returned immediately without evaluating the right side. This short-circuit behavior matters when the right side has side effects or is expensive to compute.
The Null-Conditional Operator (?.)
The ?. operator accesses members only if the object is not null. If the object is null, the entire expression evaluates to null instead of throwing a NullReferenceException.
public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string City { get; set; }
public string Country { get; set; }
}
Person person = GetPerson();
// Traditional null checking
string city = null;
if (person != null && person.Address != null)
{
city = person.Address.City;
}
// Using ?. operator
string city = person?.Address?.City;
// With method calls
int? length = person?.Name?.Length;
// With indexers
string firstChar = person?.Name?[0].ToString();
Chaining ?. operators makes navigating nested object hierarchies safe. Each ?. checks for null before continuing. If any part is null, the whole chain returns null.
Combining ?? and ?. Operators
You'll often use both operators together. The ?. operator safely navigates potentially null objects, while ?? provides defaults when values are missing.
// Safe navigation with default value
string city = person?.Address?.City ?? "Unknown";
// Method calls with defaults
int nameLength = person?.Name?.Length ?? 0;
// Complex scenarios
var orders = customer?.Orders?.Where(o => o.Total > 100)?.ToList() ?? new List();
// With LINQ
var firstOrder = customer?.Orders?.FirstOrDefault()?.OrderDate ?? DateTime.Now;
// Event invocation (safe)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
This pattern eliminates most defensive null checking. Your code expresses intent clearly: try to get a value, use a default if unavailable.
Null-Coalescing Assignment (??=)
The ??= operator assigns a value only if the left operand is currently null. This simplifies lazy initialization and caching patterns.
private List _cache;
public List GetCache()
{
// Traditional way
if (_cache == null)
{
_cache = LoadData();
}
return _cache;
// Using ??= operator
_cache ??= LoadData();
return _cache;
}
// Dictionary initialization
Dictionary counts = null;
counts ??= new Dictionary();
// Multiple fields
config ??= LoadConfig();
logger ??= CreateLogger();
database ??= ConnectToDatabase();
The ??= operator only evaluates and assigns the right side if the left side is null. This makes lazy initialization concise and efficient.
Practical Patterns and Best Practices
These operators enable several common patterns that make code more maintainable and less error-prone.
// Configuration with fallbacks
public class AppSettings
{
public string GetSetting(string key)
{
return _userSettings?[key]
?? _appSettings?[key]
?? _defaultSettings[key];
}
}
// Safe collection operations
public int GetTotalOrders(Customer customer)
{
return customer?.Orders?.Count() ?? 0;
}
// Event handling
public class ViewModel
{
public event EventHandler DataChanged;
protected void OnDataChanged()
{
DataChanged?.Invoke(this, EventArgs.Empty);
}
}
// Validation with defaults
public class UserProfile
{
private string _displayName;
public string DisplayName
{
get => _displayName ?? FirstName ?? Email ?? "Anonymous";
set => _displayName = value;
}
}
These patterns reduce boilerplate while making your code's null-handling strategy explicit. Readers immediately understand that you're providing fallbacks and handling nulls safely.