- 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.
```csharp
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.
```csharp
#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.
```csharp
#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.
```csharp
#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.
```csharp
#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.
```csharp
#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.
```csharp
// 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.
```csharp
#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);
}
GitHub에서 보기