W 2015 roku pisałeś metody w jeden sposób. W 2026 roku masz rewolucyjne nowe możliwości! Expression-bodied members (C# 6), local functions (C# 7), static lambdas (C# 9), parameter modifiers w lambdach (C# 14), i async/await który zmienił sposób pisania kodu asynchronicznego.
W tym wpisie zobaczysz ewolucję metod i funkcji - co się zmieniło od 2015 do 2026, jakie nowe możliwości daje C# 14, i jak pisać nowoczesny, zwięzły kod który jest czytelniejszy niż kiedykolwiek.
C# 11 (2022) - Static abstract members w interfejsach
C# 12 (2023) - Primary constructors
C# 14 (2026) - 🔥 Parameter modifiers w lambdach (ref, out, in, params)
Expression-bodied members - rewolucja zwięzłości (C# 6+)
C# 6 (2015) - początek rewolucji
❌ Przed C# 6 (2015)
class Person
{
private string _firstName;
private string _lastName;
public string GetFullName()
{
return _firstName + " " + _lastName;
}
public string FullName
{
get { return _firstName + " " + _lastName; }
}
}
// Verbose, dużo {} i return
✅ C# 6+ - expression-bodied
class Person
{
private string _firstName;
private string _lastName;
// Expression-bodied method
public string GetFullName() =>
$"{_firstName} {_lastName}";
// Expression-bodied property
public string FullName =>
$"{_firstName} {_lastName}";
}
// Zwięźle, czytelnie! ✨
C# 7 - rozszerzenie na więcej członków
class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
// C# 6 - methods i read-only properties
public double Area => Width * Height;
public double Perimeter() => 2 * (Width + Height);
// C# 7 - constructors!
public Rectangle(double width, double height) =>
(Width, Height) = (width, height);
// C# 7 - destructors (finalizers)
~Rectangle() => Console.WriteLine("Rectangle destroyed");
// C# 7 - getters i setters!
private double _diagonal;
public double Diagonal
{
get => Math.Sqrt(Width * Width + Height * Height);
set => _diagonal = value;
}
}
🎉 C# 7 rozszerza expression-bodied na:
✅ Constructors (konstruktory)
✅ Destructors (finalizery)
✅ Property getters i setters
✅ Indexers
Praktyczne przykłady - kiedy używać
record Product(string Name, decimal Price)
{
// ✅ DOBRZE - proste obliczenia
public decimal PriceWithTax => Price * 1.23m;
public bool IsExpensive => Price > 1000;
public string DisplayName => $"{Name} (${Price})";
// ✅ DOBRZE - proste metody
public decimal CalculateDiscount(decimal percentage) => Price * (percentage / 100);
public bool IsInPriceRange(decimal min, decimal max) => Price >= min && Price <= max;
// ❌ ŹLE - za skomplikowane, użyj normalnego bloku {}
public string GetDescription() =>
$"Product: {Name}, Price: {Price}, Tax: {PriceWithTax - Price}, " +
$"Category: {(IsExpensive ? "Premium" : "Standard")}, " +
$"Available: {(Price > 0 ? "Yes" : "No")}";
// To powinno być w {} - nieczytelne!
}
🔍 Zasada: Expression-bodied gdy mieści się w 1-2 linii
Jeśli wyrażenie jest krótkie i czytelne (1-2 linie, max ~80 znaków) - używaj =>.
Jeśli dłuższe lub wymaga wielu operacji - użyj normalnego bloku { }.
🔥 Local Functions - funkcje wewnętrzne (C# 7+)
Problem przed C# 7
// ❌ Przed C# 7 - private methods zaśmiecają klasę
class Calculator
{
public int ProcessData(int[] numbers)
{
var filtered = FilterNegatives(numbers); // helper używany tylko tu
var doubled = DoubleValues(filtered); // helper używany tylko tu
return SumValues(doubled); // helper używany tylko tu
}
// Te 3 metody używane TYLKO w ProcessData, ale są w całej klasie
private int[] FilterNegatives(int[] nums) => nums.Where(n => n >= 0).ToArray();
private int[] DoubleValues(int[] nums) => nums.Select(n => n * 2).ToArray();
private int SumValues(int[] nums) => nums.Sum();
}
// Zaśmieca klasę metodami używanymi tylko raz!
Rozwiązanie - Local Functions!
🎉 C# 7 - Local Functions
Funkcje zdefiniowane wewnątrz innych metod. Widoczne tylko w tej metodzie!
// ✅ C# 7+ - local functions
class Calculator
{
public int ProcessData(int[] numbers)
{
// Local functions - zdefiniowane wewnątrz metody
int[] FilterNegatives(int[] nums) => nums.Where(n => n >= 0).ToArray();
int[] DoubleValues(int[] nums) => nums.Select(n => n * 2).ToArray();
int SumValues(int[] nums) => nums.Sum();
// Użycie
var filtered = FilterNegatives(numbers);
var doubled = DoubleValues(filtered);
return SumValues(doubled);
}
// Klasa jest czysta - brak pomocniczych metod! ✨
}
// FilterNegatives() nie jest widoczna poza ProcessData
C# 8 - Static Local Functions
int Calculate(int x, int y)
{
int localVar = 10;
// Local function - MA dostęp do zmiennych z metody nadrzędnej
int Add()
{
return x + y + localVar; // ✅ może użyć x, y, localVar
}
// C# 8 - Static local function - NIE MA dostępu do zmiennych
static int Multiply(int a, int b)
{
return a * b;
// return x + y; // ❌ BŁĄD! static nie widzi x, y
}
return Add() + Multiply(x, y);
}
🔍 Kiedy używać static local functions?
static wymusza że funkcja NIE przechwytuje zmiennych z otoczenia (closure). To:
Zapobiega przypadkowemu użyciu zmiennych z zewnątrz
Lepsza performance (brak alokacji dla closure)
Jasno komunikuje intencję - "ta funkcja jest niezależna"
Praktyczne przykłady local functions
// Przykład 1: Walidacja z wieloma krokami
(bool isValid, string error) ValidateUser(string username, string password)
{
// Local validation helpers
bool IsValidUsername()
{
if (string.IsNullOrWhiteSpace(username)) return false;
if (username.Length < 3) return false;
return true;
}
bool IsValidPassword()
{
if (string.IsNullOrWhiteSpace(password)) return false;
if (password.Length < 6) return false;
return true;
}
// Użycie
if (!IsValidUsername()) return (false, "Invalid username");
if (!IsValidPassword()) return (false, "Invalid password");
return (true, "");
}
// Przykład 2: Iterator z walidacją
IEnumerable<int> GetSquares(int count)
{
// Walidacja PRZED iteratorem
if (count < 0)
throw new ArgumentException("count must be >= 0");
// Local iterator function
return Iterator();
IEnumerable Iterator()
{
for (int i = 0; i < count; i++)
{
yield return i * i;
}
}
}
// Przykład 3: Rekurencja
int Factorial(int n)
{
if (n < 0) throw new ArgumentException();
return Calculate(n);
// Local recursive function
static int Calculate(int num)
{
if (num <= 1) return 1;
return num * Calculate(num - 1);
}
}
💡 Local functions vs Lambda - kiedy czego używać?
void ProcessData(int[] numbers)
{
// ✅ Local function - gdy:
// - Używana wielokrotnie w metodzie
// - Ma być nazwana (czytelność)
// - Może być rekurencyjna
bool IsEven(int n) => n % 2 == 0;
foreach (var num in numbers)
{
if (IsEven(num)) // wielokrotne użycie
{
Console.WriteLine(num);
}
}
// ✅ Lambda - gdy:
// - Przekazywana do LINQ/metod wyższego rzędu
// - Używana raz
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
}
Lambda improvements - ewolucja (C# 9-14)
C# 9 - Static Lambdas
🎉 C# 9 - Static Lambdas
Lambda może być static - nie przechwytuje zmiennych z otoczenia (closure).
void ProcessNumbers(int[] numbers)
{
int threshold = 10;
// Normalna lambda - przechwytuje 'threshold' (closure)
var filtered1 = numbers.Where(n => n > threshold).ToList();
// Alokuje pamięć dla closure (przechowuje threshold)
// C# 9 - Static lambda - NIE przechwytuje zmiennych
var filtered2 = numbers.Where(static n => n > 0).ToList();
// Brak alokacji! Szybsze!
// ❌ To nie zadziała - static nie może użyć threshold
// var filtered3 = numbers.Where(static n => n > threshold).ToList();
// Błąd kompilacji! ✅ (zapobiega przypadkowemu closure)
}
// Kiedy używać static lambdas?
// 1. Performance-critical code (unikanie alokacji)
// 2. Gdy NIE potrzebujesz zmiennych z zewnątrz
// 3. Jako "dokumentacja" - "ta lambda jest niezależna"
C# 9 - Lambda attributes
// C# 9 - możesz dodać atrybuty do lambd
var numbers = new[] { 1, 2, 3, 4, 5 };
// Lambda z atrybutem (rzadko używane)
var result = numbers.Select([MyCustomAttribute] (int x) => x * 2);
// Częściej w event handlers
button.Click += [ObservableProperty] (sender, args) => { };
C# 10 - Natural type inference
// ❌ Przed C# 10 - musisz określić typ
Func<int, int> square1 = x => x * x;
Func<string, bool> isLong1 = s => s.Length > 10;
// ✅ C# 10 - var z lambdami (natural type inference)
var square2 = (int x) => x * x; // kompilator wywnioskuje Func<int, int>
var isLong2 = (string s) => s.Length > 10; // Func<string, bool>
// MUSISZ określić typy parametrów!
// var invalid = x => x * x; // ❌ błąd - nie wiadomo jaki typ x
// Lambda z return type
var parse = int (string s) => int.Parse(s); // jawny return type: int
// Lambda z attributes i return type
var processor = [MyAttribute] string (int x) => x.ToString();
C# 10 - Lambda return type inference
// Lambda może zwrócić różne typy - kompilator wywnioskuje wspólny typ bazowy
var getValue = (bool flag) => flag ? 1 : 2.5;
// Typ: Func>bool, double> (wspólny typ: int i double → double)
var getObject = (bool flag) => flag ? "text" : 123;
// Typ: Func>bool, object> (wspólny typ: string i int → object)
// Możesz wymusić konkretny return type
var enforceInt = int (bool flag) => flag ? 1 : 2;
// Typ: Func>bool, int>
🔥 C# 14 - Parameter modifiers w lambdach
Największa nowość - ref, out, in, params w lambdach!
🎉 REWOLUCJA C# 14 - Lambda parameter modifiers
Po raz pierwszy w historii C# możesz używać ref, out, in, params w lambdach!
❌ Przed C# 14 - niemożliwe!
// ❌ To NIE działało w C# 6-13!
// Nie mogłeś użyć ref w lambdzie
// var swap = (ref int a, ref int b) => { };
// BŁĄD!
// Nie mogłeś użyć out w lambdzie
// var tryParse = (string s, out int result) => { };
// BŁĄD!
// Musiałeś tworzyć normalną metodę 😞
✅ C# 14 - działa!
// ✅ C# 14 - wszystko działa!
// ref w lambdzie
var swap = (ref int a, ref int b) =>
{
(a, b) = (b, a);
};
// out w lambdzie
var tryParse = (string s, out int result) =>
{
return int.TryParse(s, out result);
};
// Nareszcie! 🎉
ref parameters w lambdach
// ref - modyfikacja zmiennej przez referencję
var increment = (ref int value) => value++;
int x = 10;
increment(ref x);
Console.WriteLine(x); // 11 (zmodyfikowane!)
// Swap - klasyczny przykład ref
var swap = (ref int a, ref int b) =>
{
int temp = a;
a = b;
b = temp;
};
int first = 5, second = 10;
swap(ref first, ref second);
Console.WriteLine($"{first}, {second}"); // 10, 5
// ref z większymi strukturami - performance
readonly struct LargeStruct
{
public int Value1 { get; init; }
public int Value2 { get; init; }
// ... wiele pól
}
// Unikamy kopiowania dużej struktury
var processLarge = (ref LargeStruct data) =>
{
Console.WriteLine($"{data.Value1}, {data.Value2}");
};
out parameters w lambdach
// out - zwracanie wielu wartości
var divide = (int a, int b, out int remainder) =>
{
remainder = a % b;
return a / b;
};
int quotient = divide(17, 5, out int rem);
Console.WriteLine($"{quotient} remainder {rem}"); // 3 remainder 2
// TryParse pattern w lambdzie
var tryParseInt = (string input, out int result) =>
{
if (int.TryParse(input, out result))
{
return true;
}
result = 0;
return false;
};
if (tryParseInt("123", out int number))
{
Console.WriteLine($"Parsed: {number}");
}
// GetMinMax w lambdzie
var getMinMax = (int[] numbers, out int min, out int max) =>
{
min = numbers.Min();
max = numbers.Max();
};
int[] nums = { 5, 2, 8, 1, 9 };
getMinMax(nums, out int minimum, out int maximum);
Console.WriteLine($"Min: {minimum}, Max: {maximum}");
in parameters w lambdach
// in - readonly reference (dla performance)
readonly struct Point3D
{
public double X { get; init; }
public double Y { get; init; }
public double Z { get; init; }
}
// in - przekazanie przez referencję, ale read-only
var calculateDistance = (in Point3D p1, in Point3D p2) =>
{
double dx = p2.X - p1.X;
double dy = p2.Y - p1.Y;
double dz = p2.Z - p1.Z;
return Math.Sqrt(dx * dx + dy * dy + dz * dz);
// p1.X = 10; // ❌ BŁĄD! in jest read-only
};
var point1 = new Point3D { X = 0, Y = 0, Z = 0 };
var point2 = new Point3D { X = 3, Y = 4, Z = 0 };
double distance = calculateDistance(in point1, in point2);
Console.WriteLine(distance); // 5
params w lambdach
// params - zmienna liczba argumentów
var sum = (params int[] numbers) =>
{
int total = 0;
foreach (int num in numbers)
{
total += num;
}
return total;
};
// Wywołania z różną liczbą argumentów
Console.WriteLine(sum(1)); // 1
Console.WriteLine(sum(1, 2)); // 3
Console.WriteLine(sum(1, 2, 3, 4, 5)); // 15
// Concat strings
var concat = (string separator, params string[] parts) =>
{
return string.Join(separator, parts);
};
string path = concat("/", "home", "user", "documents");
Console.WriteLine(path); // home/user/documents
// Max z dowolnej liczby wartości
var max = (params int[] values) =>
{
if (values.Length == 0)
throw new ArgumentException("Need at least one value");
return values.Max();
};
Console.WriteLine(max(5, 2, 8, 1, 9)); // 9
💡 Praktyczne zastosowanie - lambda jako callback z ref/out
// Wcześniej musiałeś tworzyć named method
// Teraz możesz użyć lambdy inline!
class DataProcessor
{
public delegate void ProcessDelegate(ref int value, out bool success);
public void Process(int[] data, ProcessDelegate processor)
{
foreach (ref int item in data.AsSpan())
{
processor(ref item, out bool success);
if (!success)
Console.WriteLine("Processing failed");
}
}
}
var processor = new DataProcessor();
int[] data = { 1, 2, 3, 4, 5 };
// C# 14 - lambda inline z ref i out!
processor.Process(data, (ref int value, out bool success) =>
{
if (value < 0)
{
success = false;
return;
}
value *= 2; // modyfikuj przez ref
success = true;
});
Console.WriteLine(string.Join(", ", data)); // 2, 4, 6, 8, 10
⚠️ Kiedy używać parameter modifiers w lambdach?
ref/out/in/params w lambdach to zaawansowane ficzery! Używaj tylko gdy:
✅ Performance-critical code (in dla dużych struct)
✅ Callback API wymaga ref/out
✅ Inline processing z modyfikacją (ref)
❌ NIE nadużywaj - w większości przypadków normalne lambdy wystarczą
W 99% kodu nie potrzebujesz tego. Ale gdy potrzebujesz - masz tę możliwość w C# 14! 🎉
Async/Await - podstawy asynchroniczności
Synchroniczny vs Asynchroniczny kod
🔍 Czym jest kod asynchroniczny?
Synchroniczny - program czeka na zakończenie operacji (blokuje wątek)
Asynchroniczny - program kontynuuje działanie podczas oczekiwania
// Synchronicznie - czeka 5 sekund
Thread.Sleep(5000); // ❌ Blokuje wątek!
DoSomethingElse(); // Wykona się dopiero po 5 sekundach
// Asynchronicznie - nie blokuje
await Task.Delay(5000); // ✅ Nie blokuje wątku!
DoSomethingElse(); // Może się wykonać wcześniej (na innym wątku)
async/await - podstawowa składnia
// Synchroniczna metoda (blokująca)
string DownloadString(string url)
{
using var client = new HttpClient();
return client.GetStringAsync(url).Result; // ❌ BLOKUJE wątek!
}
// Asynchroniczna metoda z async/await
async Task<string> DownloadStringAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // ✅ NIE blokuje!
}
// Użycie
string data = await DownloadStringAsync("https://api.example.com");
Console.WriteLine(data);
🎉 async/await - zasady
Metoda z await MUSI być oznaczona async
Async metoda zwraca Task (void) lub Task<T> (zwraca T)
await czeka na zakończenie Task bez blokowania wątku
Nazwa async metody kończy się na Async (konwencja)
Task<T> vs Task vs void
// Task<T> - zwraca wartość typu T
async Task GetNumberAsync()
{
await Task.Delay(1000);
return 42;
}
// Task - nie zwraca wartości (jak void)
async Task ProcessDataAsync()
{
await Task.Delay(1000);
Console.WriteLine("Done");
}
// async void - TYLKO dla event handlers!
async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(1000);
Console.WriteLine("Clicked");
}
// ⚠️ NIE używaj async void poza event handlerami!
// Wywołania
int number = await GetNumberAsync(); // czeka i zwraca 42
await ProcessDataAsync(); // czeka na zakończenie
Praktyczne przykłady async/await
// Przykład 1: HTTP request
async Task<string> FetchUserDataAsync(int userId)
{
using var client = new HttpClient();
string url = $"https://api.example.com/users/{userId}";
string json = await client.GetStringAsync(url);
return json;
}
// Przykład 2: Czytanie pliku
async Task<string> ReadFileAsync(string path)
{
using var reader = new StreamReader(path);
string content = await reader.ReadToEndAsync();
return content;
}
// Przykład 3: Wiele async operacji równolegle
async Task<(string user, string posts, string comments)> FetchAllDataAsync(int userId)
{
var userTask = FetchUserDataAsync(userId);
var postsTask = FetchPostsAsync(userId);
var commentsTask = FetchCommentsAsync(userId);
// Czekaj na WSZYSTKIE równocześnie
await Task.WhenAll(userTask, postsTask, commentsTask);
return (
await userTask,
await postsTask,
await commentsTask
);
}
// Przykład 4: Retry pattern z async
async Task<T> RetryAsync<T>(Func<Task<T>> operation, int maxAttempts = 3)
{
for (int i = 0; i < maxAttempts; i++)
{
try
{
return await operation();
}
catch (Exception ex) when (i < maxAttempts - 1)
{
Console.WriteLine($"Attempt {i + 1} failed: {ex.Message}");
await Task.Delay(1000 * (i + 1)); // exponential backoff
}
}
return await operation(); // ostatnia próba - rzuć wyjątek jeśli fail
}
// Użycie
var data = await RetryAsync(async () =>
{
return await FetchUserDataAsync(123);
});
ConfigureAwait - zaawansowane
// ConfigureAwait(false) - nie wracaj do oryginalnego kontekstu
// W aplikacjach UI (WPF, WinForms, Blazor)
async Task UpdateUIAsync()
{
var data = await FetchDataAsync(); // domyślnie wraca do UI thread
UpdateTextBox(data); // działa - jesteśmy na UI thread
}
// W bibliotekach (library code) - użyj ConfigureAwait(false)
async Task<string> LibraryMethodAsync()
{
var data = await FetchDataAsync().ConfigureAwait(false);
// NIE wraca do oryginalnego kontekstu - szybsze!
return ProcessData(data);
}
// Zasada:
// - Aplikacje UI: NIE używaj ConfigureAwait(false)
// - Biblioteki: ZAWSZE używaj ConfigureAwait(false)
⚠️ Async/Await - częste błędy
// ❌ BŁĄD 1: async void (poza event handlers)
async void ProcessAsync() // ❌ NIE!
{
await Task.Delay(1000);
}
// Wyjątki są trudne do złapania, nie możesz await
// ✅ Poprawnie
async Task ProcessAsync() // ✅ Task zamiast void
{
await Task.Delay(1000);
}
// ❌ BŁĄD 2: .Result lub .Wait() - deadlock!
async Task GetNumberAsync()
{
await Task.Delay(1000);
return 42;
}
int number = GetNumberAsync().Result; // ❌ DEADLOCK w UI apps!
// ✅ Poprawnie
int number = await GetNumberAsync(); // ✅ użyj await
// ❌ BŁĄD 3: async bez await
async Task DoSomethingAsync() // ⚠️ warning: async bez await
{
Console.WriteLine("Hello");
// brak await - async jest niepotrzebne!
}
// ✅ Poprawnie - usuń async
Task DoSomethingAsync() // lub zmień na void jeśli nie zwraca Task
{
Console.WriteLine("Hello");
return Task.CompletedTask;
}
Podsumowanie
Mega wpis o ewolucji metod i funkcji! Zobaczłeś jak C# zmienił się od 2015 do 2026:
✅ Expression-bodied members (C# 6-7) – => dla metod, properties, constructors
✅ 🔥 Local functions (C# 7+) – funkcje wewnątrz metod, static local functions
// Przykładowe użycie:
var processor = new OrderProcessor();
// Przetworz wiele zamówień równolegle
var orderIds = new[] { 1, 2, 3, 4, 5 };
var tasks = orderIds.Select(id => processor.ProcessOrderAsync(id));
var results = await Task.WhenAll(tasks);
Console.WriteLine($"Processed {results.Length} orders");
🎯 BONUS: Async Image Downloader z C# 14 features
Stwórz async image downloader używając WSZYSTKICH nowych ficzerów!
Wymagania:
Async methods:
Task<byte[]> DownloadImageAsync(string url)
Task SaveImageAsync(byte[] data, string path)
Task<bool> ValidateImageAsync(byte[] data)
Local functions:
Walidacja URL wewnątrz DownloadImageAsync
Retry logic jako local function
C# 14 Lambda z ref/out:
Lambda z out bool success do walidacji
Lambda z ref int retryCount dla retry logic
Expression-bodied members:
Properties dla statistics (ile pobranych, ile failed)
Parallel download:
Pobierz 10 obrazków równolegle
Progress reporting
Error handling (continue on error)
Przykład użycia:
var downloader = new ImageDownloader();
var urls = new[]
{
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
// ... 10 URLs
};
var results = await downloader.DownloadAllAsync(urls,
progress: (current, total) =>
{
Console.WriteLine($"Progress: {current}/{total}");
});
Console.WriteLine($"Downloaded: {downloader.SuccessCount}");
Console.WriteLine($"Failed: {downloader.FailedCount}");
Ten projekt łączy async/await, local functions, static lambdas, C# 14 parameter modifiers i expression-bodied members. Pokazujesz pełne opanowanie nowoczesnego C#! 🚀✨