| name | csharp-nullable-types |
| user-invocable | false |
| 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
{
public void Declaration()
{
int? nullableInt = null;
int? anotherInt = 42;
double? nullableDouble = null;
bool? nullableBool = true;
Nullable<int> genericNullable = null;
}
public void NullChecking()
{
int? value = GetNullableValue();
if (value.HasValue)
{
int actualValue = value.Value;
Console.WriteLine(actualValue);
}
if (value != null)
{
Console.WriteLine(value.Value);
}
int? doubled = value?.GetHashCode();
}
public void GettingValues()
{
int? value = 42;
int val1 = value.Value;
int val2 = value.GetValueOrDefault();
int val3 = value.GetValueOrDefault(10);
int val4 = value ?? 0;
}
public void NullableExpressions()
{
int? a = 10;
int? b = 20;
int? c = null;
int? sum1 = a + b;
int? sum2 = a + c;
bool? equal = a == b;
bool? greater = a > c;
bool? and = (a > 5) & (c > 5);
}
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;
}
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
{
public string Name { get; set; } = string.Empty;
public string? MiddleName { get; set; }
public NullableReferenceTypes(string name, string? middleName = null)
{
Name = name;
MiddleName = middleName;
}
public string FormatName(string firstName, string? middleName,
string lastName)
{
if (middleName != null)
{
return $"{firstName} {middleName} {lastName}";
}
return $"{firstName} {lastName}";
}
public string? FindPerson(int id)
{
return id > 0 ? "Found" : null;
}
public void CollectionExamples()
{
List<string> names = new List<string> { "Alice", "Bob" };
List<string?> nullableNames = new List<string?>
{
"Alice", null, "Charlie"
};
Dictionary<string, string?> dict =
new Dictionary<string, string?>
{
{ "key1", "value1" },
{ "key2", null }
};
}
public T? FindById<T>(int id) where T : class
{
return default(T);
}
public void NotNullAssertion()
{
string? maybeName = GetName();
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
{
public string GetDisplayName(string? name)
{
return name ?? "Unknown";
}
public string GetFirstNonNull(string? a, string? b, string? c)
{
return a ?? b ?? c ?? "Default";
}
public void NullCoalescingAssignment()
{
string? name = null;
name ??= "Default Name";
name ??= "Another Name";
}
public void CollectionCoalescing()
{
List<string>? list = null;
list ??= new List<string>();
list.Add("item");
}
private List<string>? _items;
public List<string> Items => _items ??= new List<string>();
public string ComplexCoalescing(User? user)
{
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; }
}
public string? GetCity(Person? person)
{
return person?.Address?.City;
}
public string? GetFirstPhone(Person? person)
{
return person?.PhoneNumbers?[0];
}
public int? GetNameLength(Person? person)
{
return person?.Name.Length;
}
public string GetCityOrDefault(Person? person)
{
return person?.Address?.City ?? "Unknown";
}
public string? GetFirstItem(string[]? items)
{
return items?[0];
}
public void InvokeCallback(Action? callback)
{
callback?.Invoke();
}
public event EventHandler? DataChanged;
public void OnDataChanged()
{
DataChanged?.Invoke(this, EventArgs.Empty);
}
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
{
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) { }
}
public void ProcessData(string? data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
Console.WriteLine(data.Length);
}
public string FormatData(string? data)
{
if (data == null) return string.Empty;
return data.ToUpper();
}
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{
if (key == "valid")
{
value = "Found";
return true;
}
value = null;
return false;
}
public void UseTryGet()
{
if (TryGetValue("valid", out string? result))
{
Console.WriteLine(result.Length);
}
}
private string? _cachedValue;
[MemberNotNull(nameof(_cachedValue))]
private void EnsureCacheLoaded()
{
_cachedValue ??= LoadValue();
}
public void UseCache()
{
EnsureCacheLoaded();
Console.WriteLine(_cachedValue.Length);
}
[return: NotNullIfNotNull(nameof(input))]
public string? Transform(string? input)
{
return input?.ToUpper();
}
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
{
public void Initialize([NotNull] ref string? value)
{
value ??= "default";
}
[return: MaybeNull]
public T GetDefaultValue<T>()
{
return default(T);
}
[DoesNotReturn]
public void ThrowError(string message)
{
throw new System.InvalidOperationException(message);
}
public void UseDoesNotReturn(string? value)
{
if (value == null)
{
ThrowError("Value is null");
}
System.Console.WriteLine(value.Length);
}
private string? _data;
[MemberNotNullWhen(true, nameof(_data))]
private bool IsDataLoaded()
{
return _data != null;
}
public void UseData()
{
if (IsDataLoaded())
{
System.Console.WriteLine(_data.Length);
}
}
[return: NotNullIfNotNull(nameof(source))]
public string? ProcessString(string? source)
{
return source?.Trim();
}
}
Migration Strategies
Strategies for adopting nullable reference types in existing code.
#nullable disable
public class LegacyClass
{
public string Name { get; set; }
}
#nullable restore
#nullable enable
public class ModernClass
{
public string Name { get; set; } = string.Empty;
public string? MiddleName { get; set; }
}
#nullable restore
#nullable enable
public class MigratingClass
{
#pragma warning disable CS8618
public string Name { get; set; }
#pragma warning restore CS8618
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
{
public void UseExternalLibrary()
{
string? data = GetExternalData();
if (data != null)
{
ProcessData(data);
}
}
public string? GetExternalDataSafe()
{
string result = GetExternalData();
return string.IsNullOrEmpty(result) ? null : result;
}
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
NotNullWhen attribute for TryGet pattern methods
- Validate arguments early with guard clauses
- Use
ThrowIfNull helper 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 disable too 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