csharp-nullable-types
Use when C# nullable reference types and value types for null safety, nullable annotations, and patterns for handling null values.
$ Installieren
git clone https://github.com/TheBushidoCollective/han /tmp/han && cp -r /tmp/han/jutsu/jutsu-csharp/csharp-nullable-types ~/.claude/skills/han// tip: Run this command in your terminal to install the skill
name: csharp-nullable-types description: Use when C# nullable reference types and value types for null safety, nullable annotations, and patterns for handling null values. allowed-tools:
- Read
- Write
- Edit
- Grep
- Glob
- Bash
C# Nullable Types
Nullable types in C# help prevent null reference exceptions, one of the most common programming errors. C# 8.0 introduced nullable reference types, providing compile-time null-safety checking. This skill covers nullable value types, nullable reference types, null-coalescing operators, and patterns for safe null handling.
Nullable Value Types
Value types (int, double, bool, struct) normally cannot be null. Nullable value types enable representing "no value" state.
using System;
public class NullableValueTypes
{
// Declaration
public void Declaration()
{
int? nullableInt = null;
int? anotherInt = 42;
double? nullableDouble = null;
bool? nullableBool = true;
// Generic syntax (equivalent)
Nullable<int> genericNullable = null;
}
// Checking for null
public void NullChecking()
{
int? value = GetNullableValue();
// HasValue property
if (value.HasValue)
{
int actualValue = value.Value;
Console.WriteLine(actualValue);
}
// Comparison with null
if (value != null)
{
Console.WriteLine(value.Value);
}
// Null-conditional operator
int? doubled = value?.GetHashCode();
}
// Getting values
public void GettingValues()
{
int? value = 42;
// Value property (throws if null)
int val1 = value.Value;
// GetValueOrDefault
int val2 = value.GetValueOrDefault(); // Returns 0 if null
int val3 = value.GetValueOrDefault(10); // Returns 10 if null
// Null-coalescing operator
int val4 = value ?? 0; // Returns 0 if null
}
// Nullable in expressions
public void NullableExpressions()
{
int? a = 10;
int? b = 20;
int? c = null;
// Arithmetic (null if any operand is null)
int? sum1 = a + b; // 30
int? sum2 = a + c; // null
// Comparison
bool? equal = a == b; // false
bool? greater = a > c; // null
// Logical operators
bool? and = (a > 5) & (c > 5); // null
}
// Nullable structs
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
public void NullableStructs()
{
Point? point = null;
if (point.HasValue)
{
int x = point.Value.X;
int y = point.Value.Y;
}
// Null-conditional with structs
int? x = point?.X;
}
private int? GetNullableValue() => null;
}
Nullable Reference Types
C# 8.0+ provides nullable reference types, enabling compile-time null-safety for reference types.
#nullable enable
using System;
using System.Collections.Generic;
public class NullableReferenceTypes
{
// Non-nullable by default
public string Name { get; set; } = string.Empty;
// Explicitly nullable
public string? MiddleName { get; set; }
// Constructor
public NullableReferenceTypes(string name, string? middleName = null)
{
Name = name; // Must be non-null
MiddleName = middleName; // Can be null
}
// Method with nullable parameters
public string FormatName(string firstName, string? middleName,
string lastName)
{
// Compiler warns if middleName used without null check
if (middleName != null)
{
return $"{firstName} {middleName} {lastName}";
}
return $"{firstName} {lastName}";
}
// Nullable return type
public string? FindPerson(int id)
{
// May return null
return id > 0 ? "Found" : null;
}
// Collections of nullable types
public void CollectionExamples()
{
// List of non-null strings
List<string> names = new List<string> { "Alice", "Bob" };
// List of nullable strings
List<string?> nullableNames = new List<string?>
{
"Alice", null, "Charlie"
};
// Dictionary with nullable values
Dictionary<string, string?> dict =
new Dictionary<string, string?>
{
{ "key1", "value1" },
{ "key2", null }
};
}
// Nullable generic types
public T? FindById<T>(int id) where T : class
{
// Returns null if not found
return default(T);
}
// Not-null assertion operator
public void NotNullAssertion()
{
string? maybeName = GetName();
// Tell compiler this won't be null (use with caution!)
string name = maybeName!;
}
private string? GetName() => null;
}
Null-Coalescing Operators
Null-coalescing operators provide concise null handling.
#nullable enable
using System;
using System.Collections.Generic;
public class NullCoalescingOperators
{
// Null-coalescing operator (??)
public string GetDisplayName(string? name)
{
// Returns "Unknown" if name is null
return name ?? "Unknown";
}
// Chaining
public string GetFirstNonNull(string? a, string? b, string? c)
{
return a ?? b ?? c ?? "Default";
}
// Null-coalescing assignment (??=)
public void NullCoalescingAssignment()
{
string? name = null;
// Assign only if null
name ??= "Default Name"; // name is now "Default Name"
name ??= "Another Name"; // name stays "Default Name"
}
// With collections
public void CollectionCoalescing()
{
List<string>? list = null;
// Initialize if null
list ??= new List<string>();
// Add items
list.Add("item");
}
// In property initialization
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
// Complex expressions
public string ComplexCoalescing(User? user)
{
// Multiple levels
return user?.Profile?.DisplayName ??
user?.Email ??
"Guest";
}
public class User
{
public Profile? Profile { get; set; }
public string? Email { get; set; }
}
public class Profile
{
public string? DisplayName { get; set; }
}
}
Null-Conditional Operator
The null-conditional operator safely accesses members and calls methods on potentially null references.
#nullable enable
using System;
using System.Collections.Generic;
public class NullConditionalOperator
{
public class Person
{
public string Name { get; set; } = string.Empty;
public Address? Address { get; set; }
public List<string>? PhoneNumbers { get; set; }
}
public class Address
{
public string City { get; set; } = string.Empty;
public string? PostalCode { get; set; }
}
// Basic null-conditional
public string? GetCity(Person? person)
{
// Returns null if person or Address is null
return person?.Address?.City;
}
// With indexers
public string? GetFirstPhone(Person? person)
{
// Returns null if person, PhoneNumbers is null, or empty
return person?.PhoneNumbers?[0];
}
// With method calls
public int? GetNameLength(Person? person)
{
return person?.Name.Length;
}
// Combining operators
public string GetCityOrDefault(Person? person)
{
return person?.Address?.City ?? "Unknown";
}
// With arrays
public string? GetFirstItem(string[]? items)
{
return items?[0];
}
// With delegates
public void InvokeCallback(Action? callback)
{
callback?.Invoke();
}
// With events
public event EventHandler? DataChanged;
public void OnDataChanged()
{
DataChanged?.Invoke(this, EventArgs.Empty);
}
// Chaining multiple operations
public void ChainedOperations(Person? person)
{
string? result = person?
.Address?
.City
.ToUpper()
.Substring(0, 3);
}
}
Null Handling Patterns
Common patterns for safely handling null values.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
public class NullHandlingPatterns
{
// Null object pattern
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message) =>
Console.WriteLine(message);
}
public class NullLogger : ILogger
{
public void Log(string message) { }
}
// Guard clauses
public void ProcessData(string? data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
// data is non-null here
Console.WriteLine(data.Length);
}
// Early return
public string FormatData(string? data)
{
if (data == null) return string.Empty;
return data.ToUpper();
}
// TryGet pattern
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
// Simulate lookup
if (key == "valid")
{
value = "Found";
return true;
}
value = null;
return false;
}
public void UseTryGet()
{
if (TryGetValue("valid", out string? result))
{
// result is guaranteed non-null here
Console.WriteLine(result.Length);
}
}
// MemberNotNull attribute
private string? _cachedValue;
[MemberNotNull(nameof(_cachedValue))]
private void EnsureCacheLoaded()
{
_cachedValue ??= LoadValue();
}
public void UseCache()
{
EnsureCacheLoaded();
// _cachedValue is guaranteed non-null
Console.WriteLine(_cachedValue.Length);
}
// NotNullIfNotNull attribute
[return: NotNullIfNotNull(nameof(input))]
public string? Transform(string? input)
{
return input?.ToUpper();
}
// AllowNull and DisallowNull
private string _name = string.Empty;
[AllowNull]
public string Name
{
get => _name;
set => _name = value ?? string.Empty;
}
private string LoadValue() => "loaded";
}
Nullable Annotations
Attributes that provide additional null-state information to the compiler.
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
public class NullableAnnotations
{
// NotNull - parameter won't be null when method returns
public void Initialize([NotNull] ref string? value)
{
value ??= "default";
}
// MaybeNull - return may be null despite non-nullable type
[return: MaybeNull]
public T GetDefaultValue<T>()
{
return default(T);
}
// DoesNotReturn - method never returns normally
[DoesNotReturn]
public void ThrowError(string message)
{
throw new System.InvalidOperationException(message);
}
public void UseDoesNotReturn(string? value)
{
if (value == null)
{
ThrowError("Value is null");
}
// Compiler knows value is not null here
System.Console.WriteLine(value.Length);
}
// MemberNotNullWhen
private string? _data;
[MemberNotNullWhen(true, nameof(_data))]
private bool IsDataLoaded()
{
return _data != null;
}
public void UseData()
{
if (IsDataLoaded())
{
// _data is guaranteed non-null
System.Console.WriteLine(_data.Length);
}
}
// NotNullIfNotNull
[return: NotNullIfNotNull(nameof(source))]
public string? ProcessString(string? source)
{
return source?.Trim();
}
}
Migration Strategies
Strategies for adopting nullable reference types in existing code.
// Gradual migration approach
// File 1: Keep nullability disabled
#nullable disable
public class LegacyClass
{
public string Name { get; set; } // Implicitly nullable
}
#nullable restore
// File 2: Enable nullability for new code
#nullable enable
public class ModernClass
{
public string Name { get; set; } = string.Empty; // Non-nullable
public string? MiddleName { get; set; } // Explicitly nullable
}
#nullable restore
// File 3: Suppress warnings during migration
#nullable enable
public class MigratingClass
{
// Suppress specific warnings
#pragma warning disable CS8618
public string Name { get; set; }
#pragma warning restore CS8618
// Or use ! operator temporarily
public string GetName() => Name!;
}
Working with External Libraries
Handling nullability when working with libraries that don't use nullable annotations.
#nullable enable
using System;
public class ExternalLibraryHandling
{
// Assume external library method
// public string GetData() { ... }
// Defensive programming
public void UseExternalLibrary()
{
// Treat return as potentially null
string? data = GetExternalData();
if (data != null)
{
ProcessData(data);
}
}
// Create wrappers
public string? GetExternalDataSafe()
{
string result = GetExternalData();
return string.IsNullOrEmpty(result) ? null : result;
}
// Extension methods for safety
public static class StringExtensions
{
public static string OrEmpty(this string? value)
{
return value ?? string.Empty;
}
public static bool IsNullOrEmpty(
[NotNullWhen(false)] this string? value)
{
return string.IsNullOrEmpty(value);
}
}
private string GetExternalData() => "data";
private void ProcessData(string data) { }
}
Best Practices
- Enable nullable reference types in new projects from the start
- Use
string?for optional string parameters and return values - Initialize non-nullable properties in constructors or with default values
- Prefer null-coalescing operators over explicit null checks
- Use
NotNullWhenattribute forTryGetpattern methods - Validate arguments early with guard clauses
- Use
ThrowIfNullhelper for parameter validation - Avoid using
!operator unless absolutely certain value is non-null - Leverage compiler warnings to find potential null reference issues
- Document nullability expectations in public APIs
Common Pitfalls
- Using
!operator to silence warnings without ensuring non-null - Not initializing non-nullable properties in all constructors
- Returning null from methods declared with non-nullable return types
- Forgetting to check for null before dereferencing nullable references
- Mixing nullable and non-nullable contexts causing confusion
- Over-using nullable types when default values would suffice
- Not propagating null checks through method chains
- Assuming external library methods respect nullability
- Ignoring nullable warnings during migration
- Using
#pragma warning disabletoo broadly
When to Use Nullable Types
Use nullable types when you need:
- Representing the absence of a value distinctly from default value
- Database fields that can be NULL mapped to C# properties
- Optional parameters in methods and constructors
- API responses that may or may not contain data
- Preventing null reference exceptions at compile time
- Gradual migration of legacy code to null-safe patterns
- Clear contracts about which values can be null
- Type-safe handling of optional configuration values
- Integration with external APIs that may return null
- Functional programming patterns with Option/Maybe types
Resources
Repository
