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# 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: