Delegaty i lambdy to fundamenty programowania funkcyjnego w C#! W 2015 roku używałeś delegatów i prostych lambd. W 2026 roku masz ulepszone lambdy (C# 10-14), naturalne typy, lambdy z atrybutami, i drzewa wyrażeń!
W tym wpisie poznasz podstawy delegatów, Action/Func/Predicate, events i event handling, wszystkie formy lambda expressions, nowoczesne feature'y lambd, oraz zrobimy wprowadzenie do expression trees!
C# 10 (2021) - 🔥 Natural type dla lambd, explicit return types
C# 14 (2026) - 🔥 Lambdy z ref/out/in/params parameters!
Delegates - podstawy
Czym jest delegate?
🔍 Delegate to type-safe function pointer
Delegate = typ który reprezentuje referencję do metody z konkretną sygnaturą
Możesz przechowywać metody w zmiennych, przekazywać jako parametry, wywoływać dynamicznie
// Definicja delegate - type który reprezentuje metodę
public delegate int MathOperation(int a, int b);
// Metody które pasują do delegate (ta sama sygnatura)
public int Add(int a, int b)
{
return a + b;
}
public int Multiply(int a, int b)
{
return a * b;
}
// Używanie delegate
MathOperation operation;
operation = Add; // Przypisz metodę do delegate
int result1 = operation(5, 3); // Wywołaj przez delegate - 8
operation = Multiply; // Zmień na inną metodę
int result2 = operation(5, 3); // 15
// Delegate przechowuje referencję do metody!
Delegates jako parametry - callback pattern
// Delegate jako parametr - callback pattern
public delegate void ProgressCallback(int percentComplete);
public void DownloadFile(string url, ProgressCallback onProgress)
{
for (int i = 0; i <= 100; i += 10)
{
// Simulate download
Thread.Sleep(100);
// Call callback
onProgress(i); // Callback do callera!
}
}
// Użycie - przekaż metodę jako callback
void ReportProgress(int percent)
{
Console.WriteLine($"Downloaded: {percent}%");
}
DownloadFile("http://example.com/file.zip", ReportProgress);
// Output:
// Downloaded: 0%
// Downloaded: 10%
// Downloaded: 20%
// ...
// Downloaded: 100%
Multicast delegates - łańcuchowanie
// Multicast delegates - można dodawać wiele metod!
public delegate void Notify(string message);
void SendEmail(string message)
{
Console.WriteLine($"Email: {message}");
}
void SendSMS(string message)
{
Console.WriteLine($"SMS: {message}");
}
void LogToFile(string message)
{
Console.WriteLine($"Log: {message}");
}
// Multicast - combine multiple methods
Notify notifier = SendEmail;
notifier += SendSMS; // Add
notifier += LogToFile; // Add
// Wywołanie - WSZYSTKIE metody są wywołane!
notifier("System alert!");
// Output:
// Email: System alert!
// SMS: System alert!
// Log: System alert!
// Remove method
notifier -= SendSMS; // Remove SMS
notifier("Another alert");
// Output:
// Email: Another alert
// Log: Another alert
Action, Func, Predicate - built-in delegates
Action<T> - void methods
🎉 C# 3.0 - Action<T>
Action<T> - delegate dla metod które NIE zwracają wartości (void)
// Action - void method bez parametrów
Action greet = () => Console.WriteLine("Hello!");
greet(); // "Hello!"
// Action<T> - void method z 1 parametrem
Action<string> greetPerson = name => Console.WriteLine($"Hello, {name}!");
greetPerson("Jan"); // "Hello, Jan!"
// Action<T1, T2> - void method z 2 parametrami
Action<string, int> displayInfo = (name, age) =>
Console.WriteLine($"{name} is {age} years old");
displayInfo("Anna", 25); // "Anna is 25 years old"
// Action<T1, T2, ..., T16> - do 16 parametrów!
Action<int, int, int> printSum = (a, b, c) =>
Console.WriteLine($"Sum: {a + b + c}");
printSum(1, 2, 3); // "Sum: 6"
// Użycie jako parametr
void ProcessItems(List<string> items, Action<string> processor)
{
foreach (var item in items)
{
processor(item);
}
}
var names = new List<string> { "Jan", "Anna", "Piotr" };
ProcessItems(names, name => Console.WriteLine(name.ToUpper()));
// JAN
// ANNA
// PIOTR
Func<T, TResult> - methods z return value
🎉 C# 3.0 - Func<T, TResult>
Func<T, TResult> - delegate dla metod które ZWRACAJĄ wartość
// Func<TResult> - method bez parametrów, zwraca TResult
Func<int> getRandomNumber = () => Random.Shared.Next(1, 100);
int number = getRandomNumber();
// Func<T, TResult> - method z 1 parametrem, zwraca TResult
Func<int, int> square = x => x * x;
int result = square(5); // 25
// Func<T1, T2, TResult> - method z 2 parametrami, zwraca TResult
Func<int, int, int> add = (a, b) => a + b;
int sum = add(3, 4); // 7
// Func<T1, ..., T16, TResult> - do 16 parametrów + return!
Func<string, int, bool> isLongerThan = (text, length) => text.Length > length;
bool isLong = isLongerThan("Hello", 3); // true
// LINQ używa Func<T, bool> dla Where!
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0); // Func<int, bool>
// LINQ używa Func<T, TResult> dla Select!
var doubled = numbers.Select(n => n * 2); // Func<int, int>
// Użycie jako parametr - transform pattern
TResult Transform<T, TResult>(T input, Func<T, TResult> transformer)
{
return transformer(input);
}
string text = "hello";
string upper = Transform(text, s => s.ToUpper()); // "HELLO"
int length = Transform(text, s => s.Length); // 5
Predicate<T> - boolean check
// Predicate<T> - method która zwraca bool
// Predicate<T> == Func<T, bool>
Predicate<int> isEven = x => x % 2 == 0;
bool result = isEven(4); // true
Predicate<string> isNullOrEmpty = s => string.IsNullOrEmpty(s);
bool empty = isNullOrEmpty(""); // true
// List<T>.FindAll używa Predicate<T>
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
// [2, 4, 6]
// W nowoczesnym C# używa się Func<T, bool> zamiast Predicate<T>
// Predicate jest legacy, ale nadal działa
Delegate Type
Sygnatura
Przykład
Action
void Method()
Action greet = () => Console.WriteLine("Hi");
Action<T>
void Method(T)
Action<string> print = s => Console.WriteLine(s);
Action<T1, T2>
void Method(T1, T2)
Action<int, int> add = (a, b) => Console.WriteLine(a+b);
Func<TResult>
TResult Method()
Func<int> getRandom = () => Random.Shared.Next();
Func<T, TResult>
TResult Method(T)
Func<int, int> square = x => x * x;
Func<T1, T2, TResult>
TResult Method(T1, T2)
Func<int, int, int> add = (a, b) => a + b;
Predicate<T>
bool Method(T)
Predicate<int> isEven = x => x % 2 == 0;
Events i Event Handling
Events - publisher/subscriber pattern
// Event = specjalny delegate dla publisher/subscriber pattern
public class Button
{
// Delegate dla event
public delegate void ClickHandler(object sender, EventArgs e);
// Event - używa delegate
public event ClickHandler Click;
public void PerformClick()
{
// Wywołaj event (jeśli ktoś subskrybuje)
Click?.Invoke(this, EventArgs.Empty);
}
}
// Subscriber
void OnButtonClick(object sender, EventArgs e)
{
Console.WriteLine("Button was clicked!");
}
// Użycie
var button = new Button();
button.Click += OnButtonClick; // Subscribe
button.PerformClick(); // "Button was clicked!"
button.Click -= OnButtonClick; // Unsubscribe
EventHandler i EventHandler<T> - standard pattern
// .NET ma built-in EventHandler i EventHandler<T>
public class Button
{
// EventHandler - standard delegate (object sender, EventArgs e)
public event EventHandler Click;
public void PerformClick()
{
Click?.Invoke(this, EventArgs.Empty);
}
}
// EventHandler<T> - z custom event args
public class DownloadProgressEventArgs : EventArgs
{
public int PercentComplete { get; set; }
public long BytesDownloaded { get; set; }
}
public class Downloader
{
// EventHandler<T> gdzie T : EventArgs
public event EventHandler<DownloadProgressEventArgs> ProgressChanged;
public void Download(string url)
{
for (int i = 0; i <= 100; i += 10)
{
// Raise event
ProgressChanged?.Invoke(this, new DownloadProgressEventArgs
{
PercentComplete = i,
BytesDownloaded = i * 1024
});
Thread.Sleep(100);
}
}
}
// Subscriber
var downloader = new Downloader();
downloader.ProgressChanged += (sender, e) =>
{
Console.WriteLine($"Progress: {e.PercentComplete}% ({e.BytesDownloaded} bytes)");
};
downloader.Download("http://example.com/file.zip");
Praktyczne przykłady - events
// Przykład 1: PropertyChanged event (INotifyPropertyChanged)
public class Person : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged(nameof(Name)); // Raise event
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
// Subscriber
var person = new Person();
person.PropertyChanged += (sender, e) =>
{
Console.WriteLine($"Property {e.PropertyName} changed!");
};
person.Name = "Jan"; // "Property Name changed!"
// Przykład 2: Timer events
var timer = new System.Timers.Timer(1000); // 1 second
timer.Elapsed += (sender, e) =>
{
Console.WriteLine($"Tick at {e.SignalTime}");
};
timer.Start();
Thread.Sleep(5000);
timer.Stop();
// Przykład 3: Custom event system
public class OrderProcessor
{
public event EventHandler<OrderEventArgs> OrderPlaced;
public event EventHandler<OrderEventArgs> OrderProcessed;
public event EventHandler<OrderEventArgs> OrderShipped;
public void ProcessOrder(Order order)
{
OrderPlaced?.Invoke(this, new OrderEventArgs { Order = order });
// Process...
Thread.Sleep(1000);
OrderProcessed?.Invoke(this, new OrderEventArgs { Order = order });
// Ship...
Thread.Sleep(1000);
OrderShipped?.Invoke(this, new OrderEventArgs { Order = order });
}
}
public class OrderEventArgs : EventArgs
{
public Order Order { get; set; }
}
// Multiple subscribers
var processor = new OrderProcessor();
processor.OrderPlaced += (s, e) => Console.WriteLine("Email: Order placed");
processor.OrderPlaced += (s, e) => Console.WriteLine("SMS: Order placed");
processor.OrderPlaced += (s, e) => LogToDatabase(e.Order);
processor.ProcessOrder(new Order { Id = 123 });
Lambda Expressions - wszystkie formy
Lambda podstawy - od anonymous methods
C# 2.0 - Anonymous methods
// Anonymous method (verbose)
Func<int, int> square = delegate(int x)
{
return x * x;
};
List<int> numbers = new List<int> { 1, 2, 3 };
var even = numbers.Where(delegate(int n)
{
return n % 2 == 0;
});
C# 3.0+ - Lambda expressions
// Lambda (zwięźle! ✨)
Func<int, int> square = x => x * x;
List<int> numbers = new List<int> { 1, 2, 3 };
var even = numbers.Where(n => n % 2 == 0);
Lambda syntax - wszystkie formy
// Forma 1: Expression lambda - single expression
Func<int, int> square = x => x * x;
// Forma 2: Statement lambda - block z {}
Func<int, int> square2 = x =>
{
int result = x * x;
return result;
};
// Forma 3: Bez parametrów - ()
Func<string> getMessage = () => "Hello";
Action logMessage = () => Console.WriteLine("Log");
// Forma 4: Jeden parametr - bez nawiasów
Func<int, int> double1 = x => x * 2;
// Forma 5: Wiele parametrów - z nawiasami
Func<int, int, int> add = (a, b) => a + b;
// Forma 6: Explicit types (opcjonalne)
Func<int, int, int> multiply = (int a, int b) => a * b;
// Forma 7: Discard parameters (C# 9)
Func<int, int, int> returnFirst = (x, _) => x; // _ = discard
// Forma 8: Async lambda
Func<Task<string>> fetchData = async () =>
{
await Task.Delay(100);
return "Data";
};
Lambda closures - capturing variables
// Lambda może "capture" zmienne z outer scope
int factor = 10;
Func<int, int> multiply = x => x * factor; // Captures 'factor'
Console.WriteLine(multiply(5)); // 50
factor = 20; // Zmień factor
Console.WriteLine(multiply(5)); // 100 - lambda widzi nową wartość!
// Closure - lambda "zamyka" nad zmiennymi
List<Func<int>> functions = new List<Func<int>>();
for (int i = 0; i < 5; i++)
{
int captured = i; // Capture variable
functions.Add(() => captured);
}
foreach (var func in functions)
{
Console.WriteLine(func()); // 0, 1, 2, 3, 4
}
// ❌ Pułapka - capturing loop variable
List<Func<int>> badFunctions = new List<Func<int>>();
for (int i = 0; i < 5; i++)
{
badFunctions.Add(() => i); // Captures 'i' (reference!)
}
foreach (var func in badFunctions)
{
Console.WriteLine(func()); // 5, 5, 5, 5, 5 - wszystkie 5!
}
// i == 5 po pętli, wszystkie lambdy widzą tę samą zmienną!
🔥 Lambda Improvements (C# 10-14)
C# 9 - Static lambdas
// C# 9 - static lambda - NIE może capture zmiennych
int factor = 10;
// ❌ Normal lambda - może capture
Func<int, int> multiply1 = x => x * factor; // OK - captures factor
// ✅ Static lambda - NIE MOŻE capture (compile error jeśli próbujesz)
// Func<int, int> multiply2 = static x => x * factor; // ❌ BŁĄD!
// Static lambda - tylko dla performance (no allocation dla closure)
Func<int, int> double1 = static x => x * 2; // ✅ OK - no capture
// Use case: performance-critical code bez closures
var numbers = Enumerable.Range(1, 1000000);
var doubled = numbers.Select(static x => x * 2); // No closure allocations!
C# 10 - Natural type dla lambd
🎉 C# 10 - Natural type dla lambd
Lambda może być przypisana do var! Kompilator wywnioskuje typ!
// C# 10 - var z lambda! Natural type inference
var square = (int x) => x * x; // Type: Func<int, int>
var add = (int a, int b) => a + b; // Type: Func<int, int, int>
var greet = (string name) => Console.WriteLine($"Hello, {name}");
// Type: Action<string>
// Musisz podać typy parametrów dla var!
// var bad = x => x * 2; // ❌ BŁĄD - nie wie jaki typ x
// Przekazywanie jako argument - też działa!
void ProcessNumber(Func<int, int> processor)
{
Console.WriteLine(processor(5));
}
ProcessNumber((int x) => x * x); // Lambda bez explicit Func<...>!
// Array of lambdas z var
var operations = new[]
{
(int x) => x * 2,
(int x) => x * x,
(int x) => x + 10
};
// Type: Func<int, int>[]
C# 10 - Explicit return type
// C# 10 - możesz określić return type w lambda
var square = int (int x) => x * x; // Return type: int
var divide = double (int a, int b) => (double)a / b; // Return: double
// Użyteczne gdy return type nie jest oczywisty
var getValue = object (bool condition) => condition ? 42 : "text";
// Return type: object (bez explicit byłoby error - ambiguous)
// Z async
var fetchData = async Task<string> () =>
{
await Task.Delay(100);
return "Data";
};
C# 10 - Attributes na lambdach
// C# 10 - attributes na lambdach i ich parametrach
var validateInput =
([NotNull] string input) => input.Length > 0;
// Multiple attributes
var process =
[SomeAttribute]
[AnotherAttribute]
(string value) => value.ToUpper();
// Return type + attributes
var compute =
[Pure]
int (int x) => x * x;
🔥 C# 14 - ref/out/in/params w lambdach!
🎉 NOWOŚĆ C# 14 - ref/out/in/params w lambdach!
Lambdy mogą używać ref, out, in, params parameters! Poznane w wpisie #7!
// C# 14 - ref parameter w lambda
var swap = (ref int a, ref int b) =>
{
(a, b) = (b, a);
};
int x = 5, y = 10;
swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 10, 5
// C# 14 - out parameter w lambda
var tryParse = (string input, out int result) =>
{
return int.TryParse(input, out result);
};
if (tryParse("123", out int number))
{
Console.WriteLine(number); // 123
}
// C# 14 - in parameter w lambda (readonly ref)
var computeSquare = (in int value) => value * value;
int num = 5;
int squared = computeSquare(in num);
// C# 14 - params parameter w lambda
var sum = (params int[] numbers) =>
{
int total = 0;
foreach (int n in numbers)
total += n;
return total;
};
int result = sum(1, 2, 3, 4, 5); // 15
Expression Trees - wprowadzenie
Expression<T> - lambda jako data
// Normal lambda - compiled do kodu
Func<int, int> squareFunc = x => x * x;
int result = squareFunc(5); // 25 - wykonuje kod
// Expression tree - lambda jako DATA (structure)
Expression<Func<int, int>> squareExpr = x => x * x;
// squareExpr to drzewo reprezentujące wyrażenie!
// Możesz je analizować, modyfikować, translować do SQL, etc.
// Struktura expression tree:
// squareExpr.Body: BinaryExpression (Multiply)
// Left: ParameterExpression (x)
// Right: ParameterExpression (x)
Console.WriteLine(squareExpr.Body);
// Output: (x * x)
// Możesz też compile i execute
Func<int, int> compiled = squareExpr.Compile();
int result2 = compiled(5); // 25
Po co Expression Trees? LINQ to EF!
// LINQ to EF używa Expression Trees!
using var db = new MyDbContext();
// To jest Expression<Func<Person, bool>>, nie Func!
var adults = db.People.Where(p => p.Age >= 18);
// EF analizuje expression tree i generuje SQL!
// Expression tree:
// p => p.Age >= 18
// BinaryExpression (GreaterThanOrEqual)
// Left: MemberExpression (p.Age)
// Right: ConstantExpression (18)
// EF translates to SQL:
// SELECT * FROM People WHERE Age >= 18
// Bez Expression Trees, EF nie mógłby translate do SQL!
Building Expression Trees programmatically
// Możesz budować Expression Trees programmatically
// Zamiast: x => x * x
// Ręczne budowanie:
ParameterExpression param = Expression.Parameter(typeof(int), "x");
BinaryExpression multiply = Expression.Multiply(param, param);
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(multiply, param);
// lambda reprezentuje: x => x * x
// Compile i execute
Func<int, int> compiled = lambda.Compile();
int result = compiled(5); // 25
// Po co? Dynamic query building!
Expression<Func<Person, bool>> BuildFilter(string? city, int? minAge)
{
ParameterExpression param = Expression.Parameter(typeof(Person), "p");
Expression filter = Expression.Constant(true); // Start z true
if (city != null)
{
// p.City == city
var cityProp = Expression.Property(param, nameof(Person.City));
var cityConst = Expression.Constant(city);
var cityEquals = Expression.Equal(cityProp, cityConst);
filter = Expression.AndAlso(filter, cityEquals);
}
if (minAge != null)
{
// p.Age >= minAge
var ageProp = Expression.Property(param, nameof(Person.Age));
var ageConst = Expression.Constant(minAge.Value);
var ageCheck = Expression.GreaterThanOrEqual(ageProp, ageConst);
filter = Expression.AndAlso(filter, ageCheck);
}
return Expression.Lambda<Func<Person, bool>>(filter, param);
}
// Dynamic query!
var filterExpr = BuildFilter("Warsaw", 18);
var query = db.People.Where(filterExpr);
// Generated SQL będzie zawierać tylko używane filters!
Expression Trees - use cases
// Use case 1: LINQ providers (EF, MongoDB, etc.)
var query = db.Products
.Where(p => p.Price > 100 && p.Category == "Electronics")
.OrderBy(p => p.Name);
// Expression trees są translated do SQL/MongoDB query
// Use case 2: Dynamic filtering
Expression<Func<T, bool>> BuildPredicate<T>(string propertyName, object value)
{
var param = Expression.Parameter(typeof(T), "x");
var property = Expression.Property(param, propertyName);
var constant = Expression.Constant(value);
var equals = Expression.Equal(property, constant);
return Expression.Lambda<Func<T, bool>>(equals, param);
}
var predicate = BuildPredicate<Person>("Name", "Jan");
var people = db.People.Where(predicate); // WHERE Name = 'Jan'
// Use case 3: Validation frameworks
// FluentValidation używa Expression Trees do define rules
RuleFor(customer => customer.Email).NotEmpty();
// Expression tree pozwala FluentValidation analyze property access
// Use case 4: Mapping frameworks (AutoMapper)
// Expression trees do define mappings
CreateMap<Source, Dest>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => src.FirstName + " " + src.LastName));
Podsumowanie
Delegates, Events i Lambda - functional programming w C#!
✅ Delegates - type-safe function pointers, callbacks, multicast
var builder = new QueryBuilder<Person>();
// Build dynamic predicate
var predicate = builder
.Where(p => p.Age)
.GreaterThan(18)
.And(p => p.City)
.In("Warsaw", "Krakow", "Gdansk")
.And(p => p.Name)
.StartsWith("J")
.Build();
// Use with LINQ to Objects
var people = GetPeople().Where(predicate.Compile()).ToList();
// Use with LINQ to EF
var dbPeople = db.People.Where(predicate).ToList();
// Generated expression:
// p => (p.Age > 18) && (p.City in ["Warsaw", "Krakow", "Gdansk"]) && p.Name.StartsWith("J")
// Advanced: Dynamic field selection at runtime
string field = GetFieldFromUserInput(); // "Age", "City", etc.
var dynamicPredicate = builder
.Where(field)
.GreaterThan(18)
.Build();
// API endpoint example:
// GET /api/people?age.gt=18&city.in=Warsaw,Krakow&name.startsWith=J
var query = ParseQueryString(Request.QueryString);
var predicate = builder.BuildFromQuery(query);
var results = db.People.Where(predicate).ToList();
Ten projekt demonstruje advanced Expression Trees - dynamic query building, LINQ providers, type-safe API! 🚀🌳