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:
Utworzyć backing field (private string _email;)
Przepisać property na pełną wersję z get/set
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:
✅ 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:
Property AccountNumber (string, init-only, walidacja: dokładnie 26 cyfr)
Property Balance (decimal, private set, nie może być ujemny)
Property Owner (string, init-only, nie może być pusty, trim)
Metoda Deposit(decimal amount) - dodaje do Balance
Metoda Withdraw(decimal amount) - odejmuje od Balance (tylko jeśli wystarczająco środków)
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:
Stałe (const):
DefaultPort = 8080
MaxConnections = 100
AppName = "MyApp"
Readonly fields:
readonly Guid InstanceId (generowane w konstruktorze)
readonly DateTime StartupTime (DateTime.Now w konstruktorze)
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)