Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-04-01
Udostępnij Udostępnij Kontakt
Wprowadzenie

"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# 6 (2015) - null-conditional operator ?., null-coalescing ??
  • C# 8 (2019) - 🔥 Nullable reference types - REWOLUCJA!
  • C# 8 (2019) - null-coalescing assignment ??=
  • 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;
    }
}
🔍 Alternatywy dla ! - używaj ich zamiast!
string? maybeNull = GetString();

// ❌ ! - ucisza warning
Console.WriteLine(maybeNull!.ToUpper());

// ✅ Lepsze alternatywy:

// 1. Null check
if (maybeNull != null)
{
    Console.WriteLine(maybeNull.ToUpper());
}

// 2. Null-conditional
Console.WriteLine(maybeNull?.ToUpper());

// 3. Null-coalescing
Console.WriteLine((maybeNull ?? "default").ToUpper());

// 4. Pattern matching
if (maybeNull is string text)
{
    Console.WriteLine(text.ToUpper());
}

// 5. Throw jeśli null
string nonNull = maybeNull ?? throw new InvalidOperationException("Value is null");
Wzorce defensywnego kodowania

Guard clauses - early returns

// 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
  • Null-forgiving operator (C# 8) - ! (używaj ostrożnie!)
  • 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:

  1. Włącz nullable reference types w projekcie
  2. Metoda User? FindById(int id) - zwraca User lub null
  3. Metoda void Update(User user) - guard clause dla null
  4. Metoda string GetUserEmail(int id) - użyj null-conditional + null-coalescing
  5. Cache z lazy init używając ??=
  6. 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:

  1. Klasa Config z required members:
    • required string AppName
    • required string Environment
    • string? LogPath (nullable - optional)
  2. Klasa ConfigService z:
    • Lazy-loaded cache używając ??=
    • Null-conditional dla optional settings
    • Null-conditional assignment (C# 14) dla LastUpdated
    • Guard clauses dla wszystkich metod
  3. Metody:
    • Config? LoadConfig(string path)
    • void SaveConfig(Config config, string path)
    • string GetSetting(string key, string defaultValue)
    • void UpdateSetting(string key, string? value)
  4. Użyj wszystkich operatorów: ?., ?[], ??, ??=, obj?.Property = value, !

Przykład użycia:

var service = new ConfigService();

// Required members wymuszają inicjalizację
var config = new Config 
{ 
    AppName = "MyApp",
    Environment = "Production"
    // LogPath jest optional
};

service.SaveConfig(config, "config.json");

// Null-safe reads
string appName = service.GetSetting("AppName", "Default");
string logPath = service.GetSetting("LogPath", "/var/log");

// Null-conditional assignment
service.CurrentConfig?.LastUpdated = DateTime.Now;

Ten projekt demonstruje WSZYSTKIE aspekty null safety w C# 14. Produkcyjny, bezpieczny kod! 🚀🛡️