"Object reference not set to an instance of an object." - najbardziej znany błąd w historii C#. Przez lata, NullReferenceException był koszmarem każdego programisty. W 2015 roku kompilator NIE pomagał - dowiadywałeś się o problemie dopiero gdy aplikacja się wysypywała w runtime.
W 2026 roku wszystko się zmieniło! Nullable reference types (C# 8), null-conditional operators, null-coalescing assignment, i najnowszy null-conditional assignment z C# 14 sprawiają że null errors praktycznie zniknęły z nowoczesnego kodu C#.
💥 NullReferenceException - The Billion Dollar Mistake
Tony Hoare, twórca koncepcji null reference (1965), nazwał to swoim "billion dollar mistake". Szacuje się że null errors kosztowały przemysł IT miliardy dolarów w bugach, crashach i lost productivity.
C# 8 (2019) wprowadził nullable reference types - największą zmianę w systemie typów od początku języka. W C# 14 (2026) system jest jeszcze lepszy!
📅 Timeline - ewolucja null safety w C#
C# 1.0-7.3 (do 2018) - brak pomocy od kompilatora, NullReferenceException wszędzie
C# 9 (2020) - pattern matching improvements dla null
C# 10 (2021) - null parameter checking !!
C# 11 (2022) - required members
C# 14 (2026) - 🔥 null-conditional assignment obj?.Property = value
Problem - życie przed nullable reference types
2015 - kompilator nie pomaga
// C# 6 (2015) - kompilator nie widzi problemów z null
string GetUserName(int userId)
{
if (userId == 0)
return null; // ⚠️ Kompilator: OK! (ale to bomba zegarowa)
return "Jan";
}
void ProcessUser()
{
string name = GetUserName(0); // name = null
Console.WriteLine(name.ToUpper()); // 💥 NullReferenceException!
// Kompilator NIE ostrzega! Dowiesz się dopiero w runtime!
}
❌ 2015 - defensywny kod wszędzie
void ProcessOrder(Order order)
{
// Musisz sprawdzać null WSZĘDZIE
if (order == null)
throw new ArgumentNullException();
if (order.Customer == null)
throw new InvalidOperationException();
if (order.Customer.Address == null)
throw new InvalidOperationException();
if (order.Customer.Address.City == null)
throw new InvalidOperationException();
// Właściwa logika...
ProcessCity(order.Customer.Address.City);
}
// Verbose, podatne na błędy (łatwo coś pominąć)
✅ 2026 - kompilator wymusza poprawność
void ProcessOrder(Order order) // Order jest non-nullable
{
// order NIE MOŻE być null - typ gwarantuje!
// Kompilator wymusza null checks gdzie potrzeba
if (order.Customer?.Address?.City is string city)
{
ProcessCity(city); // city na pewno nie jest null
}
}
// Zwięźle, bezpieczne, kompilator pomaga! ✨
🔥 Nullable Reference Types - game changer (C# 8+)
Włączanie nullable reference types
🎉 C# 8 (2019) - Nullable Reference Types
Od teraz w C#:
string - NIE może być null (non-nullable)
string? - MOŻE być null (nullable)
Kompilator ostrzega gdy robisz coś niebezpiecznego z null!
// W .csproj - włącz nullable reference types
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> 👈 WŁĄCZ TO!
</PropertyGroup>
</Project>
// Teraz kompilator rozróżnia nullable i non-nullable:
// ✅ Non-nullable - nie może być null
string name = "Jan";
name = null; // ⚠️ WARNING CS8600: Converting null to non-nullable type
// ✅ Nullable - może być null
string? maybeName = null; // OK!
maybeName = "Anna"; // OK!
// ⚠️ Kompilator ostrzega przed niebezpiecznym użyciem
string? nullable = GetNullableString();
Console.WriteLine(nullable.ToUpper());
// ⚠️ WARNING CS8602: Dereference of a possibly null reference
// ✅ Bezpieczne użycie - kompilator widzi że sprawdziłeś null
if (nullable != null)
{
Console.WriteLine(nullable.ToUpper()); // ✅ OK, null check wykonany
}
Poziomy włączenia nullable
// W .csproj możesz kontrolować poziom
// Opcja 1: enable - pełne włączenie (REKOMENDOWANE dla nowych projektów)
<Nullable>enable</Nullable>
// Opcja 2: warnings - tylko warningi, nie errors
<Nullable>warnings</Nullable>
// Opcja 3: annotations - tylko adnotacje (?, !) ale bez warningów
<Nullable>annotations</Nullable>
// Opcja 4: disable - wyłączone (stare projekty)
<Nullable>disable</Nullable>
// Możesz też lokalnie wyłączać/włączać w kodzie:
#nullable enable
string nonNullable = "test"; // non-nullable
#nullable disable
string legacy = null; // OK, nullable disabled
#nullable restore // przywróć ustawienie z .csproj
Annotations - oznaczanie typów
// Non-nullable (domyślnie)
string firstName; // MUSI mieć wartość (nie null)
int age; // value type - zawsze non-nullable
List<string> names; // MUSI mieć wartość
// Nullable - dodaj ?
string? middleName; // może być null
List<string>? optionalList; // może być null
int? nullableAge; // nullable value type (to działało zawsze)
// Metody - return types
string GetName() { } // zwraca non-nullable string
string? GetOptionalName() { } // może zwrócić null
// Parametry
void Process(string name) { } // name nie może być null
void Process(string? optionalName) { } // może być null
// Properties
class Person
{
public string FirstName { get; set; } // non-nullable
public string? MiddleName { get; set; } // nullable
public string LastName { get; set; } // non-nullable
}
⚠️ Nullable reference types to compile-time feature!
Ważne: string i string? to ten sam typ w runtime! Różnica istnieje tylko podczas kompilacji.
string nonNull = "test";
string? nullable = "test";
// W runtime - oba są po prostu string
Console.WriteLine(nonNull.GetType()); // System.String
Console.WriteLine(nullable.GetType()); // System.String (to samo!)
// Możesz przypisać nullable do non-nullable (z warningiem)
string target = nullable; // ⚠️ warning, ale się skompiluje
To oznacza że nullable reference types NIE chroni przed null w runtime - to pomoc kompilatora, nie gwarancja runtime!
Null-conditional operator - ?. i ?[]
Operator ?. - bezpieczny dostęp do członków (C# 6+)
// Przed C# 6 - verbose null checks
string? name = GetName();
int? length;
if (name != null)
{
length = name.Length;
}
else
{
length = null;
}
// C# 6+ - null-conditional operator ?.
string? name2 = GetName();
int? length2 = name2?.Length; // Jeśli name2 jest null → zwróć null, inaczej Length
// Łańcuchowanie - short-circuit evaluation
Person? person = GetPerson();
string? city = person?.Address?.City;
// Jeśli person jest null → null
// Jeśli person.Address jest null → null
// Inaczej → person.Address.City
❌ Przed C# 6 - pyramid of doom
Order order = GetOrder();
string city = null;
if (order != null)
{
if (order.Customer != null)
{
if (order.Customer.Address != null)
{
city = order.Customer.Address.City;
}
}
}
// Zagnieżdżone if'y 😱
✅ C# 6+ - null-conditional
Order? order = GetOrder();
string? city = order?.Customer?.Address?.City;
// Jedna linia! ✨
Operator ?[] - bezpieczny dostęp do indeksera
// ?[] dla array/list/dictionary
int[]? numbers = GetNumbers();
int? first = numbers?[0]; // null jeśli numbers jest null
Dictionary? users = GetUsers();
User? admin = users?["admin"]; // null jeśli users jest null
// Łańcuchowanie z ?.
string? userName = users?["admin"]?.Name;
Null-conditional z wywołaniem metody
// ?. z metodami
Action? callback = GetCallback();
callback?.Invoke(); // wywołaj tylko jeśli nie null
// Przed C# 6 musielibyś:
if (callback != null)
{
callback.Invoke();
}
// Events - safe invocation
public event EventHandler? DataChanged;
void OnDataChanged()
{
DataChanged?.Invoke(this, EventArgs.Empty); // bezpieczne!
// Przed C# 6: if (DataChanged != null) DataChanged(this, EventArgs.Empty);
}
// Delegates
Func<int>? getValue = GetDelegate();
int? result = getValue?.Invoke();
Null-conditional z null-coalescing
// Kombinacja ?. i ?? - potężny pattern
// Jeśli user lub Name jest null, użyj "Guest"
User? user = GetUser();
string displayName = user?.Name ?? "Guest";
// Jeśli lista jest null lub pusta, użyj domyślnej
List<int>? numbers = GetNumbers();
int count = numbers?.Count ?? 0;
// Zagnieżdżone z wartością domyślną
Order? order = GetOrder();
string city = order?.Customer?.Address?.City ?? "Unknown";
Null-coalescing operators - ?? i ??=
Operator ?? - wartość domyślna dla null (C# 2+)
// ?? - "jeśli null, użyj wartości domyślnej"
string? name = GetName();
string displayName = name ?? "Unknown";
// Jeśli name jest null → "Unknown"
// Jeśli name ma wartość → użyj name
// Łańcuchowanie - pierwszy non-null
string? primary = GetPrimaryName();
string? secondary = GetSecondaryName();
string? tertiary = GetTertiaryName();
string final = primary ?? secondary ?? tertiary ?? "Default";
// Sprawdza kolejno primary, secondary, tertiary - użyje pierwszego non-null
// Z value types
int? nullableNumber = GetNumber();
int number = nullableNumber ?? 0; // 0 jeśli null
Operator ??= - null-coalescing assignment (C# 8+)
🎉 C# 8 - Null-coalescing assignment ??=
x ??= y - "jeśli x jest null, przypisz y"
// ??= - przypisz jeśli null
// Przed C# 8
string? name = GetName();
if (name == null)
{
name = "Default";
}
// C# 8+ - jedna linia!
string? name2 = GetName();
name2 ??= "Default"; // jeśli null, przypisz "Default"
// name2 NIE jest null - nic się nie dzieje
name2 = "Jan";
name2 ??= "Default"; // name2 nadal = "Jan" (nie null)
// Lazy initialization pattern
class Service
{
private List<User>? _cachedUsers;
public List<User> GetUsers()
{
// Załaduj tylko raz - jeśli null, utwórz
_cachedUsers ??= LoadUsersFromDatabase();
return _cachedUsers;
}
}
// Collections initialization
List<string>? items = null;
items ??= new List<string>(); // utwórz jeśli null
items.Add("test"); // teraz na pewno nie null
💡 Praktyczne przykłady ??=
// Dictionary - utwórz entry jeśli nie istnieje
Dictionary<string, List<int>> groups = new();
void AddToGroup(string key, int value)
{
groups[key] ??= new List<int>(); // utwórz listę jeśli null
groups[key].Add(value);
}
// Configuration - domyślne wartości
class Config
{
public string? Host { get; set; }
public int? Port { get; set; }
public void EnsureDefaults()
{
Host ??= "localhost";
Port ??= 8080;
}
}
// Memoization
class Calculator
{
private Dictionary<int, int>? _fibCache;
public int Fibonacci(int n)
{
_fibCache ??= new Dictionary<int, int>();
if (_fibCache.TryGetValue(n, out int cached))
return cached;
int result = /* calculate */;
_fibCache[n] = result;
return result;
}
}
🔥 Null-conditional assignment - C# 14
Największa nowość - obj?.Property = value
🎉 REWOLUCJA C# 14 - Null-conditional assignment
obj?.Property = value - przypisz wartość TYLKO jeśli obj nie jest null!
❌ Przed C# 14
User? user = GetUser();
// Musiałeś sprawdzić null przed przypisaniem
if (user != null)
{
user.LastLogin = DateTime.Now;
}
// Albo z null-conditional... NIE działało!
// user?.LastLogin = DateTime.Now; // ❌ błąd kompilacji!
// Verbose i podatne na błędy
✅ C# 14 - działa!
User? user = GetUser();
// ✅ Przypisz TYLKO jeśli user nie jest null
user?.LastLogin = DateTime.Now;
// Jedna linia! ✨
Praktyczne przykłady null-conditional assignment
// Przykład 1: Update properties bezpiecznie
User? currentUser = GetCurrentUser();
currentUser?.Name = "New Name";
currentUser?.Email = "new@example.com";
currentUser?.LastModified = DateTime.Now;
// Wszystkie przypisania wykonają się TYLKO jeśli currentUser nie jest null
// Przykład 2: Nested properties
Order? order = GetOrder();
order?.Customer?.Address?.City = "Warsaw";
// Przypisze TYLKO jeśli order, Customer i Address nie są null
// Przykład 3: Collections
class ShoppingCart
{
public List<Item>? Items { get; set; }
}
ShoppingCart? cart = GetCart();
cart?.Items = new List<Item>(); // ustaw TYLKO jeśli cart nie jest null
// Przykład 4: Events
class EventPublisher
{
public EventHandler? DataChanged { get; set; }
}
EventPublisher? publisher = GetPublisher();
publisher?.DataChanged = OnDataChanged; // przypisz TYLKO jeśli nie null
// Przykład 5: Indexers
Dictionary<string, User>? users = GetUsers();
users?["admin"] = new User { Name = "Admin" };
// Przypisz TYLKO jeśli users nie jest null
Kombinacja z innymi operatorami
// Null-conditional assignment + null-coalescing
User? user = GetUser();
user?.Name = newName ?? "Default"; // przypisz newName (lub Default jeśli null)
// Null-conditional assignment + null-coalescing assignment
Settings? settings = GetSettings();
settings?.Theme ??= "Dark"; // jeśli settings nie null i Theme jest null, ustaw "Dark"
// Łańcuchowanie
Order? order = GetOrder();
order?.ShippingAddress ??= order?.BillingAddress;
// Jeśli order nie null i ShippingAddress null, użyj BillingAddress
💡 Kiedy używać null-conditional assignment?
// ✅ Używaj gdy:
// 1. Update cache/state tylko jeśli istnieje
Cache? cache = GetCache();
cache?.LastUpdated = DateTime.Now;
// 2. Partial updates w optional objects
UserProfile? profile = GetProfile();
profile?.Bio = newBio;
profile?.AvatarUrl = newAvatar;
// 3. Event handlers assignment
Component? component = FindComponent();
component?.OnClick = HandleClick;
// 4. Configuration updates
AppConfig? config = LoadConfig();
config?.LogLevel = "Debug";
config?.MaxConnections = 100;
// ❌ NIE używaj gdy:
// - Null NIE jest oczekiwany (rzuć wyjątek zamiast ignorować)
// - Potrzebujesz wiedzieć że przypisanie nie powiodło się
Null-forgiving operator - ! (używaj ostrożnie!)
Operator ! - "wiem że to nie jest null"
// ! = null-forgiving operator - "ufaj mi, to nie jest null"
string? maybeNull = GetString();
// ⚠️ Kompilator: może być null!
Console.WriteLine(maybeNull.ToUpper());
// WARNING: Dereference of a possibly null reference
// ! = "wiem co robię, to nie jest null"
Console.WriteLine(maybeNull!.ToUpper()); // ! = ucisza warning
// To NADAL może rzucić NullReferenceException w runtime!
// ! nie zmienia runtime behavior - tylko wyłącza warning
⚠️ Używaj ! bardzo oszczędnie!
Operator ! mówi kompilatorowi "zamknij się". Jeśli się mylisz - dostaniesz NullReferenceException!
// ❌ ŹLE - niewłaściwe użycie !
string? input = Console.ReadLine();
Console.WriteLine(input!.ToUpper());
// Jeśli user nie wpisze nic - CRASH!
// ✅ DOBRZE - sprawdź null zamiast używać !
string? input2 = Console.ReadLine();
if (!string.IsNullOrEmpty(input2))
{
Console.WriteLine(input2.ToUpper()); // kompilator widzi check
}
Kiedy ! jest OK?
// ✅ OK - gdy WIESZ że nie może być null (ale kompilator nie wie)
class UserService
{
private User? _currentUser;
public void Login(User user)
{
_currentUser = user; // przypisujesz non-null
}
public string GetUserName()
{
// WIESZ że Login() było wywołane przed GetUserName()
return _currentUser!.Name;
// ! jest OK - masz gwarancję biznesową
}
}
// ✅ OK - inicjalizacja w konstruktorze (kompilator nie widzi)
class Component
{
private string _name = null!; // ! = "będzie zainicjalizowane"
public Component()
{
Initialize(); // tutaj ustawia _name
}
private void Initialize()
{
_name = "Component";
}
}
// ✅ OK - dependency injection framework
class Controller
{
private IService _service = null!; // framework ustawi przez reflection
public void SetService(IService service)
{
_service = service;
}
}
// Guard clauses - walidacja na początku
// ❌ Źle - zagnieżdżone if'y
void ProcessOrder(Order? order)
{
if (order != null)
{
if (order.Items != null)
{
if (order.Items.Count > 0)
{
// właściwa logika...
}
}
}
}
// ✅ Dobrze - guard clauses z early return
void ProcessOrder(Order? order)
{
if (order is null)
{
throw new ArgumentNullException(nameof(order));
}
if (order.Items is null or { Count: 0 })
{
throw new InvalidOperationException("No items");
}
// Właściwa logika - bez zagnieżdżenia!
foreach (var item in order.Items)
{
// ...
}
}
Null object pattern
// Null object pattern - zamiast null, zwróć "pusty" obiekt
interface ILogger
{
void Log(string message);
}
class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
}
// Null object - nie robi nic
class NullLogger : ILogger
{
public void Log(string message) { } // nic nie robi
}
class Service
{
private readonly ILogger _logger;
public Service(ILogger? logger = null)
{
// Zamiast null, użyj NullLogger
_logger = logger ?? new NullLogger();
}
public void DoWork()
{
_logger.Log("Working..."); // ZAWSZE bezpieczne - brak null checks!
}
}
Option/Maybe pattern - funkcyjne podejście
// Option - typ który reprezentuje "wartość lub brak wartości"
public readonly struct Option<T>
{
private readonly T _value;
private readonly bool _hasValue;
private Option(T value, bool hasValue)
{
_value = value;
_hasValue = hasValue;
}
public static Option<T> Some(T value) => new(value, true);
public static Option<T> None() => new(default!, false);
public bool IsSome => _hasValue;
public bool IsNone => !_hasValue;
public T ValueOr(T defaultValue) => _hasValue ? _value : defaultValue;
public Option<TResult> Map<TResult>(Func<T, TResult> mapper) =>
_hasValue ? Option<TResult>.Some(mapper(_value)) : Option<TResult>.None();
}
// Użycie
Option<User> FindUser(int id)
{
User? user = Database.Find(id);
return user != null ? Option<User>.Some(user) : Option<User>.None();
}
var userOption = FindUser(123);
string name = userOption.IsSome
? userOption.ValueOr(new User()).Name
: "Unknown";
// Funkcyjne operacje
var emailOption = FindUser(123)
.Map(user => user.Email)
.Map(email => email.ToLower());
ArgumentNullException.ThrowIfNull (C# 11+)
// C# 11 - helper dla null checks w parametrach
// Przed C# 11
void Process(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
// ...
}
// C# 11+ - jedna linia
void Process(string input)
{
ArgumentNullException.ThrowIfNull(input);
// Kompilator rozumie że input nie jest null po tej linii!
Console.WriteLine(input.ToUpper()); // ✅ OK
}
// C# 11 - multiple parameters
void Process(string input, List numbers, User user)
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(numbers);
ArgumentNullException.ThrowIfNull(user);
// Wszystkie są non-null tutaj
}
Required members - wymuszenie inicjalizacji (C# 11+)
// C# 11 - required members
class User
{
// required - MUSI być ustawione podczas tworzenia
public required string Name { get; init; }
public required string Email { get; init; }
// optional
public string? Phone { get; init; }
}
// ❌ Kompilator nie pozwoli na to:
var user1 = new User(); // BŁĄD: Required member 'Name' must be set
// ✅ Musisz ustawić required members
var user2 = new User
{
Name = "Jan",
Email = "jan@example.com"
};
// Eliminuje problem z null w konstruktorach!
💡 Kompletny przykład - defensywny kod w 2026
public class OrderService
{
private readonly ILogger _logger;
private Dictionary<int, Order>? _cache;
public OrderService(ILogger? logger = null)
{
_logger = logger ?? new NullLogger(); // Null object pattern
}
public Order? GetOrder(int orderId)
{
// ArgumentNullException dla value types już w C# 11
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(orderId);
// Lazy init z ??=
_cache ??= new Dictionary<int, Order>();
// TryGetValue - bezpieczne
if (_cache.TryGetValue(orderId, out Order? cached))
{
return cached;
}
// Null-conditional assignment (C# 14)
var order = LoadFromDatabase(orderId);
_cache?[orderId] = order;
return order;
}
public void ProcessOrder(Order? order)
{
// Guard clause
ArgumentNullException.ThrowIfNull(order);
// Pattern matching guard
if (order is not { Items.Count: > 0 })
{
throw new InvalidOperationException("Order has no items");
}
// Null-conditional
_logger.Log($"Processing order {order.Id}");
// Bezpieczne dostępy
var city = order.ShippingAddress?.City ?? "Unknown";
var discount = order.Customer?.DiscountLevel ?? 0;
// Null-conditional assignment (C# 14)
order?.LastProcessed = DateTime.Now;
}
}
Podsumowanie
Rewolucja w null safety! Od najbardziej popularnego błędu w C# do praktycznie zerowych NullReferenceException w 2026:
✅ 🔥 Nullable reference types (C# 8) - game changer! string vs string?
✅ Null-conditional operator (C# 6) - ?. i ?[] dla bezpiecznego dostępu
✅ Null-coalescing (C# 2+) - ?? dla wartości domyślnych
✅ Null-coalescing assignment (C# 8) - ??= przypisz jeśli null
✅ 🔥🔥 Null-conditional assignment (C# 14) - obj?.Property = value
✅ Guard clauses - early returns, defensywne kodowanie
✅ Required members (C# 11) - wymuszenie inicjalizacji
✅ ArgumentNullException.ThrowIfNull (C# 11) - helpers dla walidacji
W kolejnym wpisie skupimy się na klasach w nowoczesnym C# - poznasz klasy, primary constructors (C# 12), auto-properties z field keywords (C# 14), i wiele, wiele więcej!
Zadanie dla Ciebie 🎯
Stwórz klasę UserRepository z pełnym null safety:
Włącz nullable reference types w projekcie
Metoda User? FindById(int id) - zwraca User lub null
Metoda void Update(User user) - guard clause dla null
Metoda string GetUserEmail(int id) - użyj null-conditional + null-coalescing
Cache z lazy init używając ??=
Użyj null-conditional assignment (C# 14) do update LastAccessed
// Przykładowe użycie:
var repo = new UserRepository();
// Find - może zwrócić null
User? user = repo.FindById(123);
string email = user?.Email ?? "no-email@example.com";
// Update - wymaga non-null
repo.Update(user!); // lub sprawdź if (user != null)
// Get email - bezpieczne
string userEmail = repo.GetUserEmail(123); // "user@example.com" lub "unknown"
🎯 BONUS: Safe Configuration System
Stwórz system konfiguracji z pełnym null safety używając WSZYSTKICH ficzerów!
Wymagania:
Klasa Config z required members:
required string AppName
required string Environment
string? LogPath (nullable - optional)
Klasa ConfigService z:
Lazy-loaded cache używając ??=
Null-conditional dla optional settings
Null-conditional assignment (C# 14) dla LastUpdated