.NET & C# Development · Lesson 7 of 229
Working with Null in C# — Nullable Reference Types
Working with Null in C# — Nullable Reference Types
NullReferenceException is the most common exception in C# codebases. C# 8 introduced nullable reference types (NRT) to make null-safety a compile-time concern rather than a runtime one.
Enabling Nullable Reference Types
Add to your .csproj:
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <!-- optional but recommended -->
</PropertyGroup>Or per-file with a pragma:
#nullable enable // turn on for this file
#nullable disable // turn off (legacy code)The Two Null Worlds
// Before NRT (nullable context disabled) — everything was implicitly nullable
string name = null; // no warning — dangerous
// With NRT enabled:
string name1 = null; // WARNING: cannot assign null to non-nullable string
string? name2 = null; // OK — explicitly nullable
// Value types have always had this distinction:
int x = null; // compile error — int cannot be null
int? y = null; // OK — Nullable<int>Null-State Analysis
The compiler tracks null-state: maybe-null vs not-null.
string? GetName(bool flag)
{
if (flag) return "Alice";
return null;
}
string? name = GetName(true);
// name is "maybe-null" here — compiler warns on dereference
Console.WriteLine(name.Length); // WARNING: dereference of possibly null reference
// After a null check, state becomes "not-null"
if (name != null)
Console.WriteLine(name.Length); // OK
// Pattern matching also narrows the type
if (name is string s)
Console.WriteLine(s.Length); // OK — s is not nullNull-Handling Operators
string? input = null;
// Null-coalescing: return right side if left is null
string result = input ?? "default"; // "default"
// Null-coalescing assignment: assign only if null
input ??= "assigned"; // input is now "assigned"
// Null-conditional: short-circuit chain on null
string? upper = input?.ToUpper()?.Trim(); // null if input is null
// Null-conditional with indexer
List<string>? list = null;
string? first = list?[0]; // null instead of ArgumentNullException
// Null-forgiving operator ! (use sparingly — suppresses warning)
string definitelyNotNull = input!; // tells compiler "trust me"
// Only use when you have guarantees the compiler can't seeAnnotating Methods and Properties
public class UserService
{
private readonly Dictionary<int, string> _users = new();
// Return type is string? — caller must handle null
public string? GetName(int id)
=> _users.TryGetValue(id, out var name) ? name : null;
// [NotNull] attribute: output parameter guaranteed not null when returns true
public bool TryGetName(int id, [System.Diagnostics.CodeAnalysis.NotNull] out string? name)
{
name = _users.GetValueOrDefault(id);
return name != null;
}
}
var service = new UserService();
string? name = service.GetName(1);
// Force the caller to handle null:
string display = name ?? "Unknown";
// Or guard with exception:
if (name is null)
throw new InvalidOperationException("User not found");
Console.WriteLine(name.Length); // now safeRequired Properties and Constructors (C# 11+)
public class Order
{
// required — must be set during object initializer
public required string Reference { get; init; }
public required int CustomerId { get; init; }
public string? Notes { get; init; } // explicitly optional
}
// Compiler error if you miss a required property:
var order = new Order
{
Reference = "ORD-001",
CustomerId = 42,
// Notes is optional — omitting is fine
};Null Object Pattern
Replace null checks with a safe "do nothing" object:
public interface INotificationService
{
void Send(string message);
}
public class EmailNotificationService : INotificationService
{
public void Send(string message) => Console.WriteLine($"Email: {message}");
}
// Null object — safe default
public class NullNotificationService : INotificationService
{
public void Send(string message) { /* intentionally empty */ }
}
// Consumer never checks for null
public class OrderProcessor(INotificationService notifications)
{
public void Process(Order order)
{
// Do work...
notifications.Send($"Order {order.Reference} processed");
}
}
// Inject real or null object — no null checks inside OrderProcessor
var processor = new OrderProcessor(new NullNotificationService());Common Pitfalls
// PITFALL 1: Null-forgiving operator hiding a real bug
var name = GetMaybeName()!; // hides potential null — exception at runtime
// BETTER: Handle null explicitly
var name = GetMaybeName() ?? throw new InvalidOperationException("Name required");
// PITFALL 2: string.IsNullOrEmpty vs string.IsNullOrWhiteSpace
string? s = " ";
if (string.IsNullOrEmpty(s)) // false — not empty, just whitespace
throw new ArgumentException();
if (string.IsNullOrWhiteSpace(s)) // true — catches whitespace too
throw new ArgumentException();
// PITFALL 3: Mixing nullable and non-nullable in collections
List<string?> mixed = new() { "a", null, "b" };
foreach (string? item in mixed)
{
// Must handle null per item
Console.WriteLine(item?.ToUpper() ?? "(null)");
}Interview Answer
"Nullable reference types (NRT), enabled via
<Nullable>enable</Nullable>, split C# reference types into two categories:string(non-nullable — compiler warns if null is assigned) andstring?(nullable — caller must handle null). The compiler performs null-state analysis: after a null check or pattern match, the state narrows tonot-nulland dereferences are safe. Key operators:??(null-coalescing),??=(null-coalescing assignment),?.(null-conditional). The null-forgiving operator!suppresses warnings — use only when you have guarantees the compiler can't see. In practice: enable NRT project-wide, userequiredproperties for mandatory fields, and prefer the null object pattern over pervasive null checks in service code."