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 2015 roku klasy wyglądały tak samo jak w 2002 - verbose konstruktory, boilerplate dla properties, brak immutability. W 2026 roku klasy są zwięzłe, bezpieczne i eleganckie.

W tym wpisie poznasz primary constructors (C# 12) - rewolucję która eliminuje 90% boilerplate kodu, field keyword (C# 14) w auto-properties, init-only properties, property patterns, i file-scoped namespaces (C# 10).

📅 Timeline - ewolucja klas w C#
  • C# 1.0-5.0 (do 2015) - klasyczne klasy, verbose konstruktory
  • C# 3.0 (2007) - Auto-properties: { get; set; }
  • C# 6 (2015) - Auto-property initializers, expression-bodied
  • C# 9 (2020) - Init-only properties: { get; init; }
  • C# 10 (2021) - File-scoped namespaces, global usings
  • C# 11 (2022) - Required members
  • C# 12 (2023) - 🔥 Primary constructors dla klas!
  • C# 14 (2026) - 🔥 field keyword w auto-properties
Podstawowa składnia klas - przypomnienie

Klasa w 2015 roku

// C# 6 (2015) - klasyczna klasa
namespace MyApp.Models
{
    public class Person
    {
        // Fields (pola prywatne)
        private string _firstName;
        private string _lastName;
        private int _age;
        
        // Constructor
        public Person(string firstName, string lastName, int age)
        {
            _firstName = firstName;
            _lastName = lastName;
            _age = age;
        }
        
        // Properties (właściwości publiczne)
        public string FirstName
        {
            get { return _firstName; }
            set { _firstName = value; }
        }
        
        public string LastName
        {
            get { return _lastName; }
            set { _lastName = value; }
        }
        
        public int Age
        {
            get { return _age; }
            set { _age = value; }
        }
        
        // Computed property
        public string FullName
        {
            get { return _firstName + " " + _lastName; }
        }
        
        // Methods
        public void Introduce()
        {
            Console.WriteLine($"Hi, I'm {FullName}, {Age} years old.");
        }
    }
}

// 40+ linii kodu dla prostej klasy! 😱

Klasa w 2026 roku - pierwszy krok optymalizacji

// C# 6+ - auto-properties (znaczna poprawa!)
namespace MyApp.Models;  // file-scoped namespace (C# 10)

public class Person
{
    // Auto-properties - bez ręcznych fields!
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    
    // Constructor - nadal trzeba
    public Person(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
    
    // Computed property
    public string FullName => $"{FirstName} {LastName}";
    
    // Method
    public void Introduce() => 
        Console.WriteLine($"Hi, I'm {FullName}, {Age} years old.");
}

// ~20 linii - 50% mniej! Ale można jeszcze lepiej...
🔥 Primary Constructors - rewolucja! (C# 12)

Problem z tradycyjnymi konstruktorami

// Tradycyjna klasa - DUŻO boilerplate
public class Product
{
    // Properties
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
    
    // Constructor - powtarzanie nazw 3 razy!
    public Product(string name, decimal price, string category)
    {
        Name = name;        // powtórzenie
        Price = price;      // powtórzenie
        Category = category;  // powtórzenie
    }
}

// Dla każdego property:
// 1. Deklaracja property
// 2. Parametr konstruktora
// 3. Przypisanie w konstruktorze
// = 3x repetition! 😱

Rozwiązanie - Primary Constructor!

🎉 C# 12 - Primary Constructors dla klas

Parametry konstruktora bezpośrednio w deklaracji klasy! Eliminuje 90% boilerplate!

// C# 12 - Primary constructor
public class Product(string name, decimal price, string category)
{
    // Parametry (name, price, category) są dostępne w całej klasie!
    
    // Możesz je użyć w properties
    public string Name { get; } = name;
    public decimal Price { get; } = price;
    public string Category { get; } = category;
    
    // Lub bezpośrednio w metodach
    public void Display() => 
        Console.WriteLine($"{name}: ${price} ({category})");
}

// Jeszcze krócej!
❌ Przed C# 12 - boilerplate
public class Logger
{
    private readonly string _logPath;
    private readonly LogLevel _minLevel;
    
    public Logger(string logPath, LogLevel minLevel)
    {
        _logPath = logPath;
        _minLevel = minLevel;
    }
    
    public void Log(string message)
    {
        if (ShouldLog())
            File.AppendAllText(_logPath, message);
    }
}

// 15 linii
✅ C# 12 - primary constructor
public class Logger(string logPath, LogLevel minLevel)
{
    public void Log(string message)
    {
        if (ShouldLog())
            File.AppendAllText(logPath, message);
        // używasz logPath bezpośrednio!
    }
}

// 7 linii! ✨

Primary constructors - szczegóły

// Parametry primary constructora są dostępne w całej klasie
public class Service(ILogger logger, IDatabase database)
{
    // W properties
    public ILogger Logger { get; } = logger;
    
    // W field initializers
    private readonly string _id = Guid.NewGuid().ToString();
    
    // W innych konstruktorach (muszą wywołać primary constructor)
    public Service(ILogger logger) : this(logger, new DefaultDatabase())
    {
        // Dodatkowa logika
    }
    
    // W metodach
    public void DoWork()
    {
        logger.Log("Working...");  // bezpośrednie użycie parametru!
        database.Save(data);
    }
    
    // W computed properties
    public string Description => $"Service with {logger.GetType().Name}";
}
⚠️ Primary constructor parameters - zasięg

Parametry primary constructora:

  • ✅ Są dostępne w CAŁEJ klasie (methods, properties, field initializers)
  • ✅ Są captured - możesz ich używać w dowolnym miejscu
  • ⚠️ Nie stają się automatycznie polami/properties - musisz jawnie utworzyć jeśli chcesz eksponować
public class Example(string value)
{
    // value jest dostępne, ale NIE jest publiczne
    public void Method() => Console.WriteLine(value);  // ✅ OK
    
    // Jeśli chcesz publiczny dostęp - utwórz property
    public string Value { get; } = value;
}

Validation w primary constructors

// Walidacja parametrów - potrzebujesz explicit body
public class Person(string name, int age)
{
    // Validation w property init
    public string Name { get; } = !string.IsNullOrEmpty(name) 
        ? name 
        : throw new ArgumentException(nameof(name));
    
    public int Age { get; } = age >= 0 && age <= 150
        ? age
        : throw new ArgumentOutOfRangeException(nameof(age));
}

// Lub użyj normalnego konstruktora dla złożonej walidacji
public class Product(string name, decimal price)
{
    public string Name { get; } = name;
    public decimal Price { get; } = price;
    
    // Dodatkowy konstruktor z walidacją
    public Product(string name, decimal price, bool validate) : this(name, price)
    {
        if (validate)
        {
            ArgumentNullException.ThrowIfNull(name);
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price);
        }
    }
}
💡 Praktyczne przykłady primary constructors
// 1. Dependency injection - idealny use case!
public class UserController(
    IUserService userService,
    ILogger logger,
    IMapper mapper)
{
    public async Task GetUserAsync(int id)
    {
        logger.LogInformation("Getting user {Id}", id);
        var user = await userService.FindByIdAsync(id);
        return mapper.Map(user);
    }
}

// 2. Configuration classes
public class AppSettings(string apiKey, string baseUrl, int timeout)
{
    public string ApiKey { get; } = apiKey;
    public string BaseUrl { get; } = baseUrl;
    public int Timeout { get; } = timeout;
    
    public HttpClient CreateClient() => new()
    {
        BaseAddress = new Uri(baseUrl),
        Timeout = TimeSpan.FromSeconds(timeout)
    };
}

// 3. Immutable data containers
public class Order(int id, DateTime date, List items)
{
    public int Id { get; } = id;
    public DateTime Date { get; } = date;
    public IReadOnlyList Items { get; } = items.AsReadOnly();
    
    public decimal Total => items.Sum(i => i.Price * i.Quantity);
}
Init-only properties - immutability (C# 9+)

Problem z set - mutability

// Przed C# 9 - mutable properties
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var person = new Person { Name = "Jan", Age = 30 };
person.Name = "Anna";  // ❌ Można zmienić po utworzeniu!
// Czasami tego NIE chcemy!

// Rozwiązanie 1 - readonly (wymaga konstruktora)
public class ImmutablePerson
{
    public string Name { get; }
    public int Age { get; }
    
    public ImmutablePerson(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

// Nie możesz użyć object initializer! ❌
// var p = new ImmutablePerson { Name = "Jan" };  // BŁĄD!

Rozwiązanie - init accessor!

🎉 C# 9 - Init-only properties

{ get; init; } - można ustawić TYLKO podczas inicjalizacji!

// C# 9 - init-only properties
public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

// ✅ Możesz użyć object initializer
var person = new Person { Name = "Jan", Age = 30 };

// ❌ NIE możesz zmienić po utworzeniu
person.Name = "Anna";  // BŁĄD kompilacji!
person.Age = 31;  // BŁĄD kompilacji!

// Best of both worlds: object initializer + immutability! ✨

init z walidacją używając field keyword

// init + field keyword (C# 14) = walidacja + immutability!
public class Product
{
    public string Name
    {
        get => field;
        init => field = !string.IsNullOrEmpty(value)
            ? value
            : throw new ArgumentException(nameof(Name));
    }
    
    public decimal Price
    {
        get => field;
        init => field = value >= 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(Price));
    }
}

var product = new Product 
{ 
    Name = "Laptop",
    Price = 2999.99m
};

// product.Price = 1999.99m;  // ❌ BŁĄD - init nie pozwala!
❌ get; set; - mutable
public class Order
{
    public int Id { get; set; }
    public DateTime Date { get; set; }
    public decimal Total { get; set; }
}

var order = new Order { Id = 1, Date = DateTime.Now, Total = 100 };
order.Total = 200;  // ❌ Można zmienić!
order.Id = 999;  // ❌ ID też można zmienić!
✅ get; init; - immutable
public class Order
{
    public int Id { get; init; }
    public DateTime Date { get; init; }
    public decimal Total { get; init; }
}

var order = new Order { Id = 1, Date = DateTime.Now, Total = 100 };
// order.Total = 200;  // ✅ BŁĄD!
// order.Id = 999;  // ✅ BŁĄD!
// Bezpieczne! ✨
🔥 field keyword w auto-properties (C# 14)

Przypomnienie - field keyword poznany wcześniej

// field keyword daje dostęp do backing field w auto-property
public class Person
{
    // Auto-property z walidacją używając field
    public string Name
    {
        get => field;
        set => field = value?.Trim() ?? throw new ArgumentNullException();
    }
    
    public int Age
    {
        get => field;
        set => field = value >= 0 && value <= 150
            ? value
            : throw new ArgumentOutOfRangeException();
    }
}

field + primary constructors = perfect combo!

// C# 14 - field + primary constructors + init
public class Product(string name, decimal price, string category)
{
    // Init-only property z walidacją używając field
    public string Name
    {
        get => field;
        init => field = !string.IsNullOrWhiteSpace(value)
            ? value.Trim()
            : throw new ArgumentException(nameof(Name));
    } = name;  // Inicjalizacja z primary constructor parameter!
    
    public decimal Price
    {
        get => field;
        init => field = value >= 0
            ? value
            : throw new ArgumentOutOfRangeException(nameof(Price));
    } = price;
    
    public string Category { get; init; } = category;
    
    // Computed property używa primary constructor parameters
    public string Display => $"{name}: ${price} in {category}";
}

// Idealne połączenie:
// ✅ Primary constructor (zwięzłość)
// ✅ Init-only properties (immutability)
// ✅ field keyword (walidacja)
// ✅ Object initializer syntax (czytelność)
💡 Kompletny przykład - nowoczesna klasa 2026
// Nowoczesna klasa używająca WSZYSTKICH ficzerów C# 14
public class User(string email, string passwordHash)
{
    // Primary constructor parameters dostępne w całej klasie
    
    // Init-only z field keyword i walidacją
    public string Email
    {
        get => field;
        init
        {
            if (!value.Contains('@'))
                throw new ArgumentException("Invalid email");
            field = value.ToLower();  // normalizuj
        }
    } = email;
    
    // Property bez custom logiki - zwykły init
    public string PasswordHash { get; init; } = passwordHash;
    
    // Mutable property (może się zmieniać)
    public DateTime? LastLogin { get; set; }
    
    // Required member (C# 11) - MUSI być ustawione
    public required string Name { get; init; }
    
    // Optional
    public string? Phone { get; init; }
    
    // Computed property używa primary constructor parameter
    public bool IsVerified => !string.IsNullOrEmpty(email);
    
    // Method używa primary constructor parameters
    public bool ValidatePassword(string password) =>
        BCrypt.Verify(password, passwordHash);
}

// Użycie
var user = new User("jan@example.com", "hash123")
{
    Name = "Jan Kowalski",  // required - musisz ustawić
    Phone = "+48123456789"  // optional
};

// user.Email = "new@email.com";  // ❌ BŁĄD - init!
user.LastLogin = DateTime.Now;  // ✅ OK - set
Property patterns - pattern matching na properties

Property patterns w switch expressions

// Property patterns - sprawdzanie wartości properties
public class Product
{
    public string Name { get; init; }
    public decimal Price { get; init; }
    public string Category { get; init; }
    public bool InStock { get; init; }
}

// Pattern matching na properties
string GetStatus(Product product) => product switch
{
    { InStock: false } => "Out of stock",
    { Price: > 1000 } => "Premium product",
    { Category: "Electronics", Price: < 100 } => "Cheap electronics",
    { Category: "Books" } => "Book available",
    _ => "Regular product"
};

// Nested property patterns
public class Order
{
    public Customer Customer { get; init; }
    public List Items { get; init; }
}

public class Customer
{
    public string Name { get; init; }
    public bool IsPremium { get; init; }
}

decimal CalculateDiscount(Order order) => order switch
{
    { Customer.IsPremium: true, Items.Count: > 10 } => 0.25m,
    { Customer.IsPremium: true } => 0.15m,
    { Items.Count: > 5 } => 0.10m,
    _ => 0
};

Property patterns w if statements

Product product = GetProduct();

// Property pattern w if
if (product is { InStock: true, Price: < 100 })
{
    Console.WriteLine("Cheap and available!");
}

// Relational patterns
if (product is { Price: >= 100 and < 500 })
{
    Console.WriteLine("Mid-range product");
}

// List patterns (C# 11)
Order order = GetOrder();

if (order is { Items: [var first, .., var last] })
{
    Console.WriteLine($"First: {first.Name}, Last: {last.Name}");
}

// Not pattern
if (product is not { Category: "Electronics" })
{
    Console.WriteLine("Not electronics");
}
💡 Praktyczne przykłady property patterns
// Przykład 1: User authentication
public class LoginRequest
{
    public string Email { get; init; }
    public string Password { get; init; }
    public bool RememberMe { get; init; }
}

string ValidateLogin(LoginRequest request) => request switch
{
    { Email: null or "" } => "Email required",
    { Password: null or { Length: < 6 } } => "Password too short",
    { Email: var e } when !e.Contains('@') => "Invalid email",
    _ => "Valid"
};

// Przykład 2: Order processing
public class OrderItem
{
    public string ProductId { get; init; }
    public int Quantity { get; init; }
    public decimal Price { get; init; }
}

string GetShippingMethod(Order order) => order switch
{
    { Items.Count: 0 } => "No shipping needed",
    { Customer.IsPremium: true } => "Express shipping (free)",
    { Items: [{ Price: > 1000 }] } => "Insured shipping",
    { Items.Count: > 10 } => "Bulk shipping",
    _ => "Standard shipping"
};

// Przykład 3: Discount calculation
decimal GetFinalPrice(Product product, Customer customer) =>
    (product, customer) switch
    {
        ({ Category: "Books", Price: < 20 }, _) => product.Price * 0.9m,
        ({ InStock: false }, _) => 0,
        (_, { IsPremium: true }) => product.Price * 0.85m,
        ({ Category: "Electronics" }, { IsPremium: true }) => product.Price * 0.80m,
        _ => product.Price
    };
File-scoped namespaces - mniej wcięć (C# 10)

Problem z tradycyjnymi namespace

// Przed C# 10 - każdy plik ma dodatkowy poziom wcięcia
namespace MyApp.Services
{
    using System;
    using System.Collections.Generic;
    
    public class UserService
    {
        public void DoSomething()
        {
            if (condition)
            {
                foreach (var item in items)
                {
                    // Kod na 5 poziomie wcięcia! 😱
                }
            }
        }
    }
}

Rozwiązanie - file-scoped namespace!

🎉 C# 10 - File-scoped namespaces

namespace MyApp.Services; - namespace dla całego pliku bez nawiasów!

// C# 10 - file-scoped namespace (bez nawiasów!)
namespace MyApp.Services;

using System;
using System.Collections.Generic;

public class UserService
{
    public void DoSomething()
    {
        if (condition)
        {
            foreach (var item in items)
            {
                // Kod na 4 poziomie wcięcia - o jeden mniej! ✨
            }
        }
    }
}

// Bez dodatkowego {} dla namespace - mniej wcięć!
❌ Block-scoped namespace
namespace MyApp.Models
{
    public class Product
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
    
    public class Order
    {
        public List Items { get; set; }
    }
}

// Dodatkowe {} i wcięcie dla całego pliku
✅ File-scoped namespace
namespace MyApp.Models;

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Order
{
    public List Items { get; set; }
}

// Bez dodatkowego poziomu! Czytelniej! ✨

Global usings - DRY dla using statements (C# 10)

// Przed C# 10 - powtarzasz using w każdym pliku
// File1.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace MyApp;

// File2.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace MyApp;

// File3.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace MyApp;

// Powtórzenie 3x! 😱

// C# 10 - global usings (jeden raz dla całego projektu!)
// GlobalUsings.cs (konwencja - jeden plik)
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;

// Teraz w innych plikach:
// File1.cs
namespace MyApp;  // Bez using - są global!

public class Service
{
    public List GetNumbers() => // List jest dostępny!
        Enumerable.Range(1, 10).ToList();  // Linq jest dostępny!
}

// File2.cs
namespace MyApp;  // Bez using!

public class Another
{
    public async Task DoWork() => // Task jest dostępny!
        await Task.Delay(1000);
}
🔍 Global usings - best practices
  • Utwórz plik GlobalUsings.cs w root projektu
  • Dodaj tylko naprawdę często używane namespaces
  • Typowe global usings:
    • global using System;
    • global using System.Collections.Generic;
    • global using System.Linq;
    • global using System.Threading.Tasks;
  • Specyficzne dla projektu usings mogą być nadal lokalne

Implicit usings - automatyczne! (.NET 6+)

// .NET 6+ - implicit usings włączone domyślnie
// W .csproj:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <ImplicitUsings>enable</ImplicitUsings>  👈 Włączone domyślnie!
  </PropertyGroup>
</Project>

// To automatycznie dodaje global usings:
// - System
// - System.Collections.Generic
// - System.IO
// - System.Linq
// - System.Net.Http
// - System.Threading
// - System.Threading.Tasks

// Nie musisz ich pisać! Są już dostępne! ✨
Podsumowanie

Rewolucja w klasach! Od 40+ linii boilerplate do zwięzłych, bezpiecznych klas w 2026:

  • Podstawy klas - ewolucja od 2015 do 2026
  • 🔥 Primary constructors (C# 12) - eliminacja 90% boilerplate!
  • Init-only properties (C# 9) - object initializer + immutability
  • 🔥 field keyword (C# 14) - walidacja w auto-properties
  • Primary constructors + field + init - idealne combo!
  • Property patterns - pattern matching na properties
  • File-scoped namespaces (C# 10) - mniej wcięć
  • Global usings (C# 10) - DRY dla using statements
  • Implicit usings (.NET 6+) - automatyczne usings

W kolejnym wpisie poznasz Records - immutable data types - positional records, with expressions, record structs!

Zadanie dla Ciebie 🎯

Przepisz starą klasę używając WSZYSTKICH nowoczesnych ficzerów:

// STARA klasa (2015):
namespace MyApp.Models
{
    public class Customer
    {
        private string _name;
        private string _email;
        private bool _isPremium;
        private DateTime _registeredDate;
        
        public Customer(string name, string email, bool isPremium)
        {
            _name = name;
            _email = email;
            _isPremium = isPremium;
            _registeredDate = DateTime.Now;
        }
        
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
        
        public string Email
        {
            get { return _email; }
            set { _email = value; }
        }
        
        public bool IsPremium
        {
            get { return _isPremium; }
            set { _isPremium = value; }
        }
        
        public DateTime RegisteredDate
        {
            get { return _registeredDate; }
        }
        
        public string GetDisplayName()
        {
            return _name + " (" + _email + ")";
        }
    }
}

Przepisz używając:

  1. File-scoped namespace
  2. Primary constructor
  3. Init-only properties dla Name, Email (immutable)
  4. field keyword z walidacją (Email musi zawierać @)
  5. Mutable property dla IsPremium (może się zmieniać)
  6. Expression-bodied member dla GetDisplayName
🎯 BONUS: Shopping Cart System

Stwórz system koszyka zakupowego używając WSZYSTKICH ficzerów C# 14!

Klasy do stworzenia:

  1. Product (primary constructor):
    • Init-only: Name, Price, Category
    • field keyword z walidacją (Price >= 0)
    • Computed: PriceWithTax
  2. CartItem (primary constructor):
    • Init-only: Product, Quantity
    • Computed: Subtotal
  3. ShoppingCart:
    • Mutable: Items (List<CartItem>)
    • Computed: Total, ItemCount
    • Methods: AddItem, RemoveItem, Clear
  4. Customer (primary constructor):
    • Required: Email (field z walidacją)
    • Init-only: Name
    • Mutable: IsPremium

Użyj:

  • ✅ File-scoped namespaces
  • ✅ Primary constructors dla Product, CartItem, Customer
  • ✅ Init-only properties (immutability)
  • ✅ field keyword z walidacją
  • ✅ Required members dla Customer.Email
  • ✅ Property patterns w discount calculation
  • ✅ Expression-bodied members

Przykład użycia:

var cart = new ShoppingCart();

var laptop = new Product("Laptop", 2999.99m, "Electronics");
var mouse = new Product("Mouse", 49.99m, "Accessories");

cart.AddItem(new CartItem(laptop, 1));
cart.AddItem(new CartItem(mouse, 2));

var customer = new Customer("jan@example.com", "Jan Kowalski")
{
    IsPremium = true
};

decimal discount = (cart, customer) switch
{
    ({ Total: > 1000 }, { IsPremium: true }) => 0.20m,
    ({ Total: > 500 }, { IsPremium: true }) => 0.15m,
    ({ ItemCount: > 5 }, _) => 0.10m,
    _ => 0
};

Console.WriteLine($"Total: ${cart.Total}, Discount: {discount:P0}");

Ten projekt demonstruje nowoczesne OOP w C# 14 - zwięzłe, bezpieczne, eleganckie! 🚀✨