Delegat zwracający wartość w C# – pułapka bez ostrzeżenia

private static string GetMorningGreeting()
{
return "Dzień dobry! Gotowy na nowy dzień?";
}
private static string GetEveningGreeting()
{
return "Dobry wieczór! Czas odpocząć od kodu.";
}
Console.WriteLine($"[7:00 - UI]: {GetMorningGreeting()}");
Console.WriteLine($"[20:00 - UI]: {GetEveningGreeting()}");Nagłówek aplikacji ma pokazywać inne powitanie rano, inne wieczorem. Ten kod działa ale jest źle zaprojektowany. Komponent nagłówka musi znać wszystkie źródła tekstu. Dodanie powitania świątecznego to edycja w miejscu, które w ogóle nie powinno o takich rzeczach wiedzieć.
W dwóch poprzednich wpisach delegat był rozkazem, wydawałeś polecenie, nic nie wracało. Tym razem delegat staje się pytaniem, a odpowiedź po raz pierwszy wraca do Twojego kodu.
Złota Zasada 3 Kroków, tym razem z wynikiem
Złota Zasada 3 Kroków — teraz z wynikiem
Powstaje typ. Kontraktem jest string () — nic nie przyjmuje, coś zwraca.
Bez nawiasów, jak zawsze. Jeszcze nic się nie wykonało.
Z nawiasami — i po raz pierwszy wynik wraca do zmiennej.
Zwróć uwagę: linijka message = provider(); w krokach 3 i 5
wygląda tak samo, ale za każdym razem inna metoda stoi pod zmienną — inny tekst wraca.
Te same trzy kroki co zawsze: definiujesz, przypisujesz, wywołujesz, tylko krok trzeci ma teraz dodatkowy element: coś odbierasz.
using System;
namespace BeginnerDelegateReturnValue;
public delegate string GreetingProvider();
public class Program
{
public static void Main()
{
Console.WriteLine("=== ASYSTENT NAGŁÓWKA APLIKACJI ===\n");
GreetingProvider provider = GetMorningGreeting;
string message = provider();
Console.WriteLine($"[7:00 - UI]: {message}");
provider = GetEveningGreeting;
message = provider();
Console.WriteLine($"[20:00 - UI]: {message}");
provider = () => "Sto lat! Masz dziś urodziny!";
Console.WriteLine($"[00:01 - UI]: {provider()}");
}
private static string GetMorningGreeting()
{
return "Dzień dobry! Gotowy na nowy dzień?";
}
private static string GetEveningGreeting()
{
return "Dobry wieczór! Czas odpocząć od kodu.";
}
}Definicję czytamy jak zawsze od prawej do lewej: () - nie przyjmuje żadnych argumentów;string - zwraca tekst; GreetingProvider - tak nazywa się nowy typ.
Zwróć uwagę na nazwę: nie GreetingAction, tylko Provider - dostawca. W .NET nazwa delegata zwykle mówi, jaką rolę pełni: Action wykonuje, Provider albo Factory dostarcza.
Trzeci krok zawiera lambdę () => "Sto lat!..." - skrótowy zapis funkcji bez osobnej metody. To nadal ten sam delegat i ten sam kontrakt string (), zmienił się tylko sposób zapisu. Pełne rozłożenie lambd to temat na osobny wpis.
Zasada:
delegat zwracający wartość to nadal
DEFINIUJESZ → PRZYPISUJESZ → WYWOŁUJESZ.
Różnica jest wyłącznie w trzecim kroku - po prawej stronie stoi teraz= provider(), nie samprovider();.
Delegat jako pytanie, nie rozkaz
provider nie wie, która metoda jest pod niego akurat podpięta. Wie tylko, że po wywołaniu dostanie stringa - kontrakt gwarantuje typ wyniku, nie jego treść.
To zmiana kategorii narzędzia. Delegat z poprzednich dwóch wpisów wykonywał efekt uboczny, coś się działo na ekranie albo w grze. Ten delegat oddaje dane i to wywołujący decyduje, co z nimi zrobić: wypisać, zapisać do pliku, wysłać jako odpowiedź HTTP, sprawdzić w teście.
Pułapka #1: wynik, który znika bez żadnego ostrzeżenia
Gdzie ląduje wynik provider()?
Zwróć uwagę: obie karty pokażą zielony badge „✅ kompiluje się”. Kompilator nie ma zdania na temat tego, co robisz z wynikiem — a jednak tylko jedna linijka faktycznie coś pokazuje na ekranie. Ta pułapka nie zostawia po sobie żadnego śladu w edytorze.
To najgroźniejsza pułapka w całej serii, bo kompilator milczy.
GreetingProvider provider = GetMorningGreeting;
provider(); // ✅ kompiluje się, ale tekst przepadaDelegat zwracający wartość można wywołać jak zwykłą instrukcję, bez odbierania wyniku. Metoda wykona się poprawnie, zwróci tekst i ten tekst poleci do kosza. Zero błędu, zero ostrzeżenia, zero śladu w logach kompilatora.
Zauważ symetrię z pierwszym wpisem tej serii:
tam problemem były nawiasy, których nie powinno być. Tutaj problemem jest brak odbiorcy wyniku. Pierwsze kompilator wyłapie. Drugiego nigdy.
string message = provider(); // ✅ wynik trafia do zmiennej
Console.WriteLine(message);Zasada:
przy każdym delegacie zwracającym wartość zadaj sobie pytanie:
gdzie ląduje wynik?
Jeśli nie potrafisz wskazać zmiennej, prawdopodobnie właśnie go zgubiłeś.
Pułapka #2: typ zmiennej musi zgadzać się z typem zwracanym
int number = provider();
// ❌ CS0029: Cannot implicitly convert type 'string' to 'int'Nic zaskakującego, ale warto to zestawić z pierwszym wpisem tej serii, bo numer błędu jest ten sam, treść inna. Tam CS0029 mówił „cannot convert void", tu mówi „cannot convert string". Ta sama rodzina błędu, inna przyczyna, komunikat trzeba czytać, nie tylko rozpoznawać po numerze.
Pułapka #3: „wypisuje" to nie to samo co „zwraca"
private static void PrintGreeting()
{
Console.WriteLine("Dzień dobry! Gotowy na nowy dzień?");
}
GreetingProvider bad = PrintGreeting;
// ❌ CS0407: 'void Pulapki.PrintGreeting()' has the wrong return typePrintGreeting pokazuje powitanie, sama decyduje, że trafi do konsoli. Delegat GreetingProvider oczekuje metody, która tekst oddaje i pozwala decydować wywołującemu, co z nim zrobić. Z punktu widzenia użytkownika efekt na ekranie wygląda identycznie. Z punktu widzenia architektury to dwie różne rzeczy i dlatego typ zwracany jest częścią kontraktu, a nie szczegółem implementacji.
To też powód, dla którego delegaty zwracające wartość są łatwiejsze w testowaniu: sprawdzasz, co metoda zwróciła, zamiast przechwytywać konsolę.
Ten sam mechanizm dopasowania działa też w drugą stronę, brak parametru albo zły typ parametru odrzuca kandydata równie bezwzględnie:
private static string GetGreetingFor(string name)
{
return $"Dzień dobry, {name}!";
}
GreetingProvider bad2 = GetGreetingFor;
// ❌ CS0123: No overload for 'GetGreetingFor' matches delegate 'GreetingProvider'
// (przyjmuje parametr, a GreetingProvider nie deklaruje żadnego)Zasada:
typ zwracany to pełnoprawna część sygnatury delegata, tak samo jak liczba i typy parametrów. Metoda musi pasować całością, nie jednym elementem.
Ściągawka na code review
| Zapis | Wynik | Dlaczego |
|---|---|---|
GreetingProvider p = GetMorningGreeting; | ✅ | string () - kształt się zgadza |
string m = p(); | ✅ | wynik trafia do zmiennej |
p(); | ⚠️ kompiluje się | wynik zwrócony i natychmiast zgubiony |
int n = p(); | ❌ CS0029 | delegat zwraca string, nie int |
GreetingProvider p = PrintGreeting; | ❌ CS0407 | PrintGreeting zwraca void, nie string |
GreetingProvider p = GetGreetingFor; | ❌ CS0123 | metoda wymaga parametru, delegat żadnego nie deklaruje |
Po co to, skoro mogę po prostu wywołać metodę?
Uczciwe pytanie, tak samo jak przy poprzednich dwóch wpisach. W przykładzie z nagłówkiem faktycznie nic nie zyskujesz, dopóki wszystko siedzi w jednej metodzie Main.
Wartość pojawia się, gdy źródło danych i kod, który ich potrzebuje, przestają być tym samym miejscem w kodzie. Nagłówek pyta „daj mi tekst" i nie musi wiedzieć, czy odpowiedź przyjdzie z bazy danych, z pliku konfiguracyjnego czy z prostego return.
To dokładnie ten sam mechanizm, na którym stoi każda metoda LINQ: .Where(), .Select(), .OrderBy()
wywołują Twój delegat i oczekują wyniku z powrotem, żeby zdecydować, co zrobić z kolejnym elementem kolekcji. W nowoczesnym C# rzadko piszesz własny delegate
zwracający wartość, bo wystarcza gotowy Func<string> - ale mechanizm pod spodem jest identyczny z tym, co właśnie napisałeś.
Podsumowanie
- Delegat może zwracać wartość - wtedy przestaje być rozkazem, a staje się dostawcą danych.
- Wywołanie wygląda tak samo jak wcześniej, ale wynik trzeba mieć gdzie przyjąć. Zignorowanie go kompiluje się bez ostrzeżenia, to najgroźniejsza pułapka tego wpisu.
- Typ zwracany jest częścią kontraktu, tak samo jak liczba i typy parametrów z poprzedniego wpisu. Metoda, która tekst wypisuje, nie pasuje do delegata, który tekst zwraca.
- Ten sam numer błędu może znaczyć co innego.
CS0029w tym wpisie i w pierwszym wpisie serii ma inną treść, liczy się komunikat, nie sam numer. - Delegaty zwracające wartość łatwiej testować - sprawdzasz zwrócony wynik zamiast przechwytywać efekty uboczne.
📂 Kod źródłowy: github.com - projekt DevHobby.L03-GreetingProvider
📘 Darmowa roadmapa Junior .NET Developer: dev-hobby.pl/lista-vip
Zadanie na 5 minut:
zdefiniuj delegate string MessageProvider();,
napisz trzy metody — poranną, roboczą i wieczorną - zwracające tekst przez return. Podmieniaj przypisanie i za każdym razem zapisz wynik do zmiennej string przed wypisaniem.
Na końcu napisz obok siebie provider = GetMorning; oraz provider = GetMorning(); - jaki komunikat dostajesz dla drugiej linijki i czym różni się od CS0029 z pierwszego wpisu tej serii?
Zobacz
- Delegaty w C# — dlaczego przypisujesz bez nawiasów - pierwszy wpis, delegat jako rozkaz
- Delegat z parametrem w C# - typ jako kontrakt, dopasowanie sygnatury
- Func i Action w C# - gotowe delegaty zamiast własnych
- LINQ w C# — kompletny przewodnik - delegaty zwracające wartość w akcji
📦 Kod źródłowy tego artykułu: zobacz na GitHub
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →