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

W poprzednich wpisach poznałeś typy danych w C#. Teraz czas zagłębić się w zmienne - jak je deklarować, inicjalizować i używać w nowoczesny sposób. Poznasz także stałe (const i readonly), oraz field keyword z C# 14 - rewolucyjną funkcję która zmienia sposób pracy z properties!

🔍 Czym jest zmienna?

Zmienna to nazwane miejsce w pamięci, które przechowuje wartość. Myśl o zmiennej jak o pudełku z etykietą - etykieta to nazwa zmiennej, a zawartość to jej wartość.

int wiek = 25;
// "wiek" = nazwa zmiennej (etykieta na pudełku)
// 25 = wartość zmiennej (zawartość pudełka)
// int = typ zmiennej (rozmiar pudełka)
Deklaracja i inicjalizacja zmiennych

Podstawowa składnia

// Deklaracja bez inicjalizacji
int liczba;  // zmienna istnieje, ale NIE ma wartości!

// Inicjalizacja (pierwsze przypisanie wartości)
liczba = 10;

// Deklaracja + inicjalizacja w jednej linii (najczęstsze!)
int liczba2 = 20;
string imie = "Jan";
bool czyAktywny = true;
⚠️ Nie możesz używać niezainicjalizowanej zmiennej!
int x;
Console.WriteLine(x);  // ❌ BŁĄD: Use of unassigned local variable 'x'

// Musisz najpierw przypisać wartość:
int y;
y = 5;
Console.WriteLine(y);  // ✅ OK

C# nie pozwala na używanie niezainicjalizowanych zmiennych lokalnych - to zapobiega błędom! W innych językach (np. C++) niezainicjalizowana zmienna ma "śmieciową" wartość z pamięci. 🛡️

Wiele zmiennych jednego typu

// Stary sposób - jedna po drugiej
int x = 10;
int y = 20;
int z = 30;

// Możesz deklarować wiele zmiennych w jednej linii
int a = 1, b = 2, c = 3;

// Możesz też mieszać zainicjalizowane i niezainicjalizowane
int d, e = 10, f;  // tylko e ma wartość
💡 Moja rekomendacja

Jedna zmienna = jedna linia. Czytelniejsze i łatwiejsze do debugowania:

// ✅ Czytelnie
int x = 10;
int y = 20;
int z = 30;

// ❌ Mniej czytelne (chyba że bardzo powiązane zmienne)
int x = 10, y = 20, z = 30;

Zasięg zmiennych (scope)

void MetodaPrzyklad()
{
    int x = 10;  // zmienna lokalna metody
    
    if (x > 5)
    {
        int y = 20;  // zmienna lokalna bloku if
        Console.WriteLine(x);  // ✅ x jest widoczna
        Console.WriteLine(y);  // ✅ y jest widoczna
    }
    
    Console.WriteLine(x);  // ✅ x jest widoczna
    Console.WriteLine(y);  // ❌ BŁĄD! y nie istnieje poza blokiem if
}
🔍 Zasada zasięgu (scope rule)

Zmienna istnieje tylko w bloku {} w którym została zadeklarowana.

  • Zmienna lokalna metody - istnieje w całej metodzie
  • Zmienna w if/for/while - istnieje tylko w tym bloku
  • Zmienne znikają gdy program wychodzi z bloku (automatyczne czyszczenie pamięci)
Konwencje nazewnictwa

Zasady nazewnictwa w C#

Rodzaj Konwencja Przykład
Zmienne lokalne camelCase int liczbaUzytkownikow;
Parametry metod camelCase void Metoda(string userName) { }
Pola prywatne (fields) _camelCase (z podkreślnikiem) private int _licznik;
Properties publiczne PascalCase public string UserName { get; set; }
Stałe PascalCase const int MaxRetries = 5;
Klasy, interfejsy PascalCase class UserService { }
💡 Przykład użycia konwencji
public class UserService
{
    // Pola prywatne - _camelCase
    private int _userCount;
    private string _connectionString;
    
    // Properties publiczne - PascalCase
    public int UserCount { get; set; }
    public string ConnectionString { get; set; }
    
    // Stała - PascalCase
    private const int MaxRetries = 5;
    
    // Metoda - PascalCase
    public void AddUser(string userName, int userAge)  // parametry - camelCase
    {
        // Zmienne lokalne - camelCase
        int retryCount = 0;
        bool isSuccess = false;
        
        // ... logika
    }
}

Dobre praktyki nazewnictwa

❌ Złe nazwy zmiennych
int x;  // co to jest x?
int data;  // jaka data?
string s1, s2, s3;  // co przechowują?
int temp;  // temp czego?
bool flag;  // jaka flaga?
int numberOfTheUsersInTheSystemRightNow;  // za długie!
✅ Dobre nazwy zmiennych
int userAge;  // jasne!
DateTime orderDate;  // jasne!
string firstName, lastName, email;  // jasne!
int retryCount;  // jasne!
bool isAuthenticated;  // jasne!
int activeUsers;  // jasne i zwięzłe
  • Używaj pełnych słów - userCount zamiast usrCnt
  • Bądź opisowy - nazwa powinna mówić CO przechowuje
  • Nie za długie - max ~30 znaków
  • Bool = pytanie - isActive, hasPermission, canEdit
  • Kolekcje = liczba mnoga - users, orders, items
Stałe - const

Czym jest const?

const to compile-time constant - wartość która NIGDY się nie zmieni i jest znana podczas kompilacji.

// Deklaracja const
const int MaxUsers = 100;
const string AppName = "MojaAplikacja";
const double Pi = 3.14159;

// Próba zmiany wartości
MaxUsers = 200;  // ❌ BŁĄD: Cannot assign to const

// Użycie
if (currentUsers > MaxUsers)
{
    Console.WriteLine("Osiągnięto limit użytkowników");
}
🔍 Jak działa const?

Podczas kompilacji, kompilator zastępuje const jego wartością w każdym miejscu użycia:

// Twój kod:
const int MaxRetries = 5;
for (int i = 0; i < MaxRetries; i++) { }

// Po kompilacji (uproszczenie):
for (int i = 0; i < 5; i++) { }  // 5 wstawione bezpośrednio!

To nazywa się inlining - const nie istnieje w runtime!

Ograniczenia const

// ✅ Dozwolone typy dla const:
const int liczba = 10;
const double pi = 3.14;
const string tekst = "Hello";
const bool flag = true;
const char znak = 'A';

// ❌ NIE można użyć dla typów referencyjnych (poza string i null)
const DateTime data = DateTime.Now;  // ❌ BŁĄD!
const List lista = new List();  // ❌ BŁĄD!

// Const musi być zainicjalizowana podczas deklaracji
const int x;  // ❌ BŁĄD!
x = 10;

// Const musi mieć wartość znaną w compile-time
const int maxUsers = 100;  // ✅ OK
const int doubleMax = maxUsers * 2;  // ✅ OK (obliczane w compile-time)
const int runtime = GetMaxFromDatabase();  // ❌ BŁĄD! (runtime value)

Kiedy używać const?

💡 Przykłady dobrych użyć const
// Matematyczne stałe
const double Pi = 3.14159265359;
const double E = 2.71828182846;

// Limity aplikacji
const int MaxLoginAttempts = 3;
const int PasswordMinLength = 8;
const int SessionTimeoutMinutes = 30;

// Konfiguracja
const string AppVersion = "1.0.0";
const string DefaultCulture = "pl-PL";

// HTTP status codes
const int HttpOk = 200;
const int HttpNotFound = 404;
const int HttpServerError = 500;

// Role użytkowników
const string AdminRole = "Admin";
const string UserRole = "User";
const string GuestRole = "Guest";
Readonly - stałe runtime

Różnica między const a readonly

Cecha const readonly
Kiedy wartość? Compile-time (podczas kompilacji) Runtime (podczas wykonania)
Gdzie inicjalizacja? Tylko w deklaracji W deklaracji LUB w konstruktorze
Modyfikator Implicitly static Może być instance lub static
Dozwolone typy Tylko typy proste + string Wszystkie typy
Performance Szybsze (inlined) Minimalnie wolniejsze
public class Configuration
{
    // const - wartość znana w compile-time
    public const int MaxRetries = 5;
    
    // readonly - wartość może być ustawiona w runtime
    public readonly string ConnectionString;
    public readonly DateTime StartTime;
    
    public Configuration(string connectionString)
    {
        // readonly można ustawić w konstruktorze
        ConnectionString = connectionString;
        StartTime = DateTime.Now;  // ✅ runtime value!
    }
    
    public void TryChangeValues()
    {
        // MaxRetries = 10;  // ❌ BŁĄD - const nie można zmienić
        // ConnectionString = "new";  // ❌ BŁĄD - readonly nie można zmienić po konstruktorze
    }
}

// Użycie:
var config = new Configuration("Server=localhost;Database=MyDB");
Console.WriteLine(config.ConnectionString);  // działa!
Console.WriteLine(config.StartTime);  // czas utworzenia obiektu
🔍 Kiedy używać const vs readonly?
  • const - gdy wartość jest NAPRAWDĘ stała i znana przed uruchomieniem (Pi, MaxRetries, AppVersion)
  • readonly - gdy wartość jest stała PO inicjalizacji, ale zależy od runtime (ConnectionString, DateTime.Now, Guid.NewGuid())

readonly może być dla typów referencyjnych

public class UserService
{
    // readonly dla typów referencyjnych
    private readonly List _allowedRoles;
    private readonly ILogger _logger;
    
    public UserService(ILogger logger)
    {
        _logger = logger;  // przypisanie w konstruktorze
        _allowedRoles = new List { "Admin", "User", "Guest" };
    }
    
    public void TestReadonly()
    {
        // ❌ Nie możesz zmienić referencji
        // _logger = new ConsoleLogger();  // BŁĄD!
        // _allowedRoles = new List();  // BŁĄD!
        
        // ✅ ALE możesz modyfikować zawartość obiektu!
        _allowedRoles.Add("SuperAdmin");  // ✅ OK
        _logger.Log("message");  // ✅ OK
    }
}
⚠️ readonly chroni referencję, NIE zawartość!

Dla typów referencyjnych, readonly oznacza że nie możesz zmienić wskaźnika (na co wskazuje zmienna), ale MOŻESZ modyfikować obiekt na który wskazuje.

private readonly List<int> _numbers = new List<int>();

void Method()
{
    _numbers = new List<int>();  // ❌ Nie możesz zmienić referencji
    _numbers.Add(5);  // ✅ Możesz modyfikować zawartość!
    _numbers.Clear();  // ✅ OK
}

Jeśli chcesz prawdziwie immutable collection, użyj ImmutableList<T> z System.Collections.Immutable.

readonly struct (C# 7.2+)

// readonly struct - cała struktura jest immutable
public readonly struct Point
{
    public int X { get; }
    public int Y { get; }
    
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
    
    // Wszystkie properties muszą być read-only
    // Wszystkie fields muszą być readonly
}

var point = new Point(10, 20);
// point.X = 30;  // ❌ BŁĄD - nie ma settera
Auto-implemented Properties - przypomnienie

Stary problem - backing fields

Zanim poznamy field keyword, przypomnijmy jak działały properties w starym C#:

// Stary sposób (C# 1.0-2.0) - pełne properties z backing field
public class Osoba
{
    // Backing field - prywatne pole przechowujące wartość
    private string _imie;
    
    // Property - publiczny dostęp z get i set
    public string Imie
    {
        get { return _imie; }
        set { _imie = value; }
    }
}

// To samo, krócej - auto-implemented property (C# 3.0+)
public class Osoba2
{
    // Kompilator automatycznie tworzy backing field!
    public string Imie { get; set; }
    
    // Pod spodem kompilator generuje:
    // private string k__BackingField;
    // public string Imie { 
    //     get { return k__BackingField; } 
    //     set { k__BackingField = value; }
    // }
}
🔍 Czym jest backing field?

Backing field to prywatne pole które faktycznie przechowuje wartość property. Property jest tylko "okienkiem" do tego pola.

// Property z logiką
private string _email;
public string Email
{
    get { return _email; }
    set 
    { 
        if (value.Contains("@"))
            _email = value;
        else
            throw new ArgumentException("Invalid email");
    }
}

_email to backing field - faktyczne miejsce w pamięci gdzie jest email.

Problem z auto-properties

public class User
{
    // Auto-property - prosty get/set
    public string Name { get; set; }
    
    // Co jeśli chcę dodać walidację?
    // Muszę zmienić na pełne property + backing field!
    
    private string _email;  // Ręczny backing field
    public string Email
    {
        get { return _email; }
        set 
        { 
            if (!value.Contains("@"))
                throw new ArgumentException("Invalid email");
            _email = value; 
        }
    }
}
⚠️ Problem refactoringu

Kiedy zaczynasz z auto-property (public string Email { get; set; }) a potem chcesz dodać logikę, musisz:

  1. Utworzyć backing field (private string _email;)
  2. Przepisać property na pełną wersję z get/set
  3. Przenieść wartość początkową do backing field

To jest uciążliwe i podatne na błędy!

🔥 field keyword - rewolucja w C# 14!

Nowy sposób - dostęp do backing field

🎉 NOWOŚĆ C# 14 - field keyword

field to nowe kontekstowe słowo kluczowe które daje dostęp do backing field wewnątrz property!

Nie musisz już tworzyć private string _name; - kompilator robi to automatycznie, a Ty masz dostęp przez field!

// C# 14 - z field keyword
public class User
{
    // Auto-property z dostępem do backing field!
    public string Email { get; set; }
    
    // Dodaję walidację używając 'field'
    public string ValidatedEmail 
    { 
        get => field;  // field = wygenerowany backing field
        set 
        {
            if (!value.Contains("@"))
                throw new ArgumentException("Invalid email");
            field = value;  // przypisanie do backing field
        }
    }
}
🔍 Jak działa field?

field to specjalne słowo kluczowe dostępne TYLKO wewnątrz property accessor (get/set). Odnosi się do automatycznie wygenerowanego backing field.

public string Name 
{ 
    get => field;
    set => field = value; 
}

// Kompilator generuje pod spodem:
private string k__BackingField;
public string Name 
{ 
    get => k__BackingField;
    set => k__BackingField = value; 
}

field to alias do tego tajemniczego <Name>k__BackingField!

Porównanie: przed i po field keyword

❌ Przed C# 14 - ręczny backing field
public class User
{
    // Ręczny backing field
    private string _name;
    
    public string Name
    {
        get { return _name; }
        set 
        {
            if (string.IsNullOrEmpty(value))
                throw new ArgumentException();
            _name = value.Trim();
        }
    }
    
    // Kolejne property
    private int _age;
    public int Age
    {
        get { return _age; }
        set
        {
            if (value < 0 || value > 150)
                throw new ArgumentException();
            _age = value;
        }
    }
}
// Dużo boilerplate kodu!
✅ C# 14 - z field keyword
public class User
{
    // Bez ręcznych backing fields!
    
    public string Name
    {
        get => field;
        set 
        {
            if (string.IsNullOrEmpty(value))
                throw new ArgumentException();
            field = value.Trim();
        }
    }
    
    // Kolejne property
    public int Age
    {
        get => field;
        set
        {
            if (value < 0 || value > 150)
                throw new ArgumentException();
            field = value;
        }
    }
}
// Mniej kodu, więcej czytelności! ✨

Inicjalizacja z field

public class Product
{
    // Inicjalizacja wartości domyślnej z field
    public int Stock { get; set; } = 0;  // automatyczny backing field = 0
    
    // Możesz użyć field w get/set jeśli potrzebujesz
    public decimal Price 
    { 
        get => field;
        set => field = value < 0 ? 0 : value;  // cena nie może być ujemna
    } = 9.99m;  // wartość domyślna
    
    // Lazy initialization z field
    private List? _tags;
    public List Tags 
    { 
        get => field ??= new List();  // utwórz jeśli null
        set => field = value;
    }
}
💡 Praktyczny przykład - walidacja i formatowanie
public class Person
{
    // Imię - zawsze pierwsza litera wielka
    public string FirstName
    {
        get => field;
        set => field = char.ToUpper(value[0]) + value[1..].ToLower();
    }
    
    // Email - walidacja
    public string Email
    {
        get => field;
        set
        {
            if (!value.Contains('@'))
                throw new ArgumentException("Invalid email format");
            field = value.ToLower();  // zawsze małe litery
        }
    }
    
    // Wiek - zakres
    public int Age
    {
        get => field;
        set
        {
            if (value < 0 || value > 150)
                throw new ArgumentOutOfRangeException(nameof(value));
            field = value;
        }
    }
    
    // Data urodzenia - computed property
    public DateTime? BirthDate { get; set; }
    
    public int CalculatedAge => BirthDate.HasValue 
        ? DateTime.Now.Year - BirthDate.Value.Year 
        : 0;
}

// Użycie:
var person = new Person 
{ 
    FirstName = "jAN",  // zostanie sformatowane na "Jan"
    Email = "JAN@EXAMPLE.COM",  // zostanie sformatowane na "jan@example.com"
    Age = 25 
};

field z init accessor

public class Product
{
    // field działa też z init!
    public string Name 
    { 
        get => field;
        init 
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Name cannot be empty");
            field = value.Trim();
        }
    }
    
    public decimal Price 
    { 
        get => field;
        init => field = value < 0 ? 0 : value; 
    }
}

var product = new Product 
{ 
    Name = "  Laptop  ",  // zostanie trim'owane
    Price = -100  // zostanie ustawione na 0
};

// product.Name = "Desktop";  // ❌ BŁĄD - init nie pozwala po konstrukcji

Kiedy używać field keyword?

  • ✅ Gdy chcesz dodać walidację do auto-property
  • ✅ Gdy chcesz formatować/transformować wartość przy set
  • ✅ Gdy potrzebujesz lazy initialization
  • ✅ Gdy chcesz logować zmiany wartości
  • ✅ Gdy refactorujesz auto-property i potrzebujesz dostępu do backing field
  • ❌ Nie używaj jeśli prosty get/set wystarcza (zwykłe { get; set; })
💡 field w praktyce - Observable property
public class ViewModel
{
    // Event informujący o zmianie
    public event PropertyChangedEventHandler? PropertyChanged;
    
    // Property z notyfikacją o zmianie
    public string UserName
    {
        get => field;
        set
        {
            if (field != value)  // tylko jeśli wartość się zmieniła
            {
                field = value;
                PropertyChanged?.Invoke(this, 
                    new PropertyChangedEventArgs(nameof(UserName)));
            }
        }
    }
}

// Przed field keyword musielibyśmy:
// private string _userName;
// public string UserName { get => _userName; set { if (_userName != value) ...

// field keyword = mniej kodu! 🎉
Pattern matching z zmiennymi

Deklaracja zmiennych w pattern matching

C# pozwala deklarować zmienne bezpośrednio w wyrażeniach pattern matching:

// Pattern matching z deklaracją zmiennej
object obj = "Hello";

if (obj is string tekst)  // deklaracja zmiennej 'tekst'
{
    Console.WriteLine($"Długość: {tekst.Length}");  // tekst jest typu string
}

// Możesz użyć tego w switch expressions
string GetDescription(object value) => value switch
{
    int liczba => $"Liczba: {liczba}",
    string tekst => $"Tekst o długości {tekst.Length}",
    null => "Null",
    _ => "Nieznany typ"
};

// Pattern matching z właściwościami
public record Person(string Name, int Age);

string Describe(Person person) => person switch
{
    { Age: < 18 } => "Niepełnoletni",
    { Age: >= 18 and < 65 } => "Dorosły",
    { Age: >= 65 } => "Senior",
    _ => "Unknown"
};

Discard pattern (_)

// _ = "nie obchodzi mnie ta wartość"

// Przykład 1: ignorowanie wartości z metody
string GetUserName() => "Jan";
int GetUserAge() => 25;

_ = GetUserName();  // wywołaj metodę, ale ignoruj wynik
var age = GetUserAge();  // tę wartość chcemy

// Przykład 2: pattern matching
object value = 42;
if (value is int _)  // sprawdź czy to int, ale nie przypisuj do zmiennej
{
    Console.WriteLine("To jest int");
}

// Przykład 3: tuple deconstruction
var (name, _, age) = GetPersonData();  // ignoruj środkowy element
Console.WriteLine($"{name}, {age}");

(string, string, int) GetPersonData() => ("Jan", "Kowalski", 30);

// Przykład 4: out parameter (gdy nie potrzebujesz wartości)
if (int.TryParse("123", out _))  // ignoruj sparsowaną wartość
{
    Console.WriteLine("To jest poprawna liczba");
}
🔍 Kiedy używać discard (_)?
  • Gdy musisz wywołać metodę, ale nie potrzebujesz wyniku
  • Gdy pattern matching sprawdza typ, ale nie używasz zmiennej
  • Gdy dekonstruujesz tuple, ale nie potrzebujesz wszystkich wartości
  • Gdy TryParse sprawdza poprawność, ale wartość nie jest używana
Podsumowanie

W tym wpisie nauczyłeś się wszystkiego o zmiennych, stałych i nowym field keyword! To był kluczowy wpis:

  • Deklaracja zmiennych – składnia, inicjalizacja, zasięg (scope)
  • Konwencje nazewnictwa – camelCase vs PascalCase, dobre praktyki
  • const – compile-time constants, kiedy używać
  • readonly – runtime constants, różnica vs const
  • readonly struct – immutable structures (C# 7.2+)
  • Auto-implemented properties – problem z backing fields
  • 🔥 field keyword – REWOLUCJA w C# 14! Dostęp do backing field bez boilerplate
  • field z walidacją – praktyczne przykłady użycia
  • field z init – immutable properties z logiką
  • Pattern matching – deklaracja zmiennych w pattern matching
  • Discard (_) – ignorowanie niepotrzebnych wartości

W kolejnym wpisie zagłębimy się w String'i w 2026 roku – raw string literals, UTF-8 literals, string interpolation improvements, Span<char> i wydajność!

Zadanie dla Ciebie 🎯

Stwórz klasę BankAccount używając field keyword:

  1. Property AccountNumber (string, init-only, walidacja: dokładnie 26 cyfr)
  2. Property Balance (decimal, private set, nie może być ujemny)
  3. Property Owner (string, init-only, nie może być pusty, trim)
  4. Metoda Deposit(decimal amount) - dodaje do Balance
  5. Metoda Withdraw(decimal amount) - odejmuje od Balance (tylko jeśli wystarczająco środków)
  6. Użyj field w properties do walidacji!
// Przykładowe użycie:
var account = new BankAccount 
{ 
    AccountNumber = "12345678901234567890123456",
    Owner = "  Jan Kowalski  "  // zostanie trim'owane
};

account.Deposit(100);
Console.WriteLine(account.Balance);  // 100

account.Withdraw(30);
Console.WriteLine(account.Balance);  // 70

account.Withdraw(100);  // Powinno wyrzucić wyjątek (niewystarczające środki)
🎯 BONUS: System konfiguracji z walidacją

Stwórz klasę AppConfiguration która demonstruje wszystko czego się nauczyłeś:

Wymagania:

  1. Stałe (const):
    • DefaultPort = 8080
    • MaxConnections = 100
    • AppName = "MyApp"
  2. Readonly fields:
    • readonly Guid InstanceId (generowane w konstruktorze)
    • readonly DateTime StartupTime (DateTime.Now w konstruktorze)
  3. Properties z field keyword:
    • ServerUrl (string, walidacja: musi zaczynać się od "http://" lub "https://")
    • Port (int, zakres: 1-65535, domyślnie DefaultPort)
    • MaxThreads (int, zakres: 1-1000, walidacja)
    • IsDebugMode (bool, gdy ustawione loguj zmiany do konsoli)
  4. Init-only properties z field:
    • Environment (string: "Development", "Staging", "Production")
  5. Computed property:
    • Uptime (TimeSpan, obliczane: DateTime.Now - StartupTime)

Przykład użycia:

var config = new AppConfiguration 
{ 
    Environment = "Production",
    ServerUrl = "https://api.example.com",
    Port = 443,
    MaxThreads = 50,
    IsDebugMode = false
};

Console.WriteLine($"Instance ID: {config.InstanceId}");
Console.WriteLine($"Started at: {config.StartupTime}");
Console.WriteLine($"Running for: {config.Uptime}");

// Próba ustawienia niepoprawnej wartości
// config.Port = 70000;  // powinno wyrzucić wyjątek
// config.ServerUrl = "ftp://invalid";  // powinno wyrzucić wyjątek

To ćwiczenie łączy const, readonly, field keyword, walidację i computed properties. To jest profesjonalny kod produkcyjny! 🚀✨