Menu

Func i Action w C#: generyczne delegaty, Predicate i przykłady

Jak działają Func i Action w C#: Func<T, TResult> dla funkcji zwracających wartość, Action<T> dla tych, które nic nie zwracają, Predicate i Comparison, przekazywanie i zwracanie funkcji, słownik poleceń, lambdy async i kiedy lepiej zadeklarować własny delegat.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Func i Action to generyczne typy delegatów, które .NET deklaruje za ciebie, więc możesz przechowywać i przekazywać funkcje bez wcześniejszego deklarowania typu delegatu. Func zwraca wartość; Action nic nie zwraca.

Wynik:

4
46.00
False
Hello!
Mia scored 87

Zasada do zapamiętania: w Func ostatni argument typu to typ zwracany. Func<int, int, bool> przyjmuje dwa int i zwraca bool. Func<int> nic nie przyjmuje i zwraca int. Wszystkie argumenty typu w Action to parametry.

Cała rodzina

TypParametryZwraca
Actionbrakvoid
Action<T>Tvoid
Action<T1, T2> ... do 16T1, T2, ...void
Func<TResult>brakTResult
Func<T, TResult>TTResult
Func<T1, T2, TResult> ... do 16T1, T2, ...TResult
Predicate<T>Tbool
Comparison<T>T, Tint (ujemny, zero, dodatni)
Converter<TIn, TOut>TInTOut

Predicate, Comparison i Converter są starsze i występują w metodach List<T> i Array: FindAll(Predicate<T>), Sort(Comparison<T>), ConvertAll(Converter<T, TOut>). LINQ wszędzie używa Func, dlatego list.Where(...) i list.FindAll(...) przyjmują lambdy o tym samym kształcie, ale różnych typach:

Wynik:

17, 15
34, 52, 28
52, 34, 28, 17, 15
52y 34y 28y 17y 15y

Przekazywanie funkcji do metod

Parametr Func albo Action pozwala kodowi wywołującemu dostarczyć tę część algorytmu, która się zmienia. Dwa częste kształty to "uruchom to i zmierz czas" oraz "próbuj, aż zadziała":

Wynik:

attempt 1 failed
attempt 2 failed
<html>ok</html>
clicks: 2

Retry jest generyczna, więc jej Func<int, T> może zwracać cokolwiek; kompilator wywnioskował T jako string z lambdy. Lambda przekazana do Twice zmienia lokalną zmienną clicks, co działa, bo lambda przechwytuje samą zmienną, a nie kopię jej wartości.

Grupy metod: przekazanie metody po nazwie

Do wypełnienia Func lub Action nie potrzebujesz lambdy. Nazwa istniejącej metody o pasującej sygnaturze (tzw. "method group") konwertuje się bezpośrednio:

Wynik:

parsed: 43
sat is weekend: True
total quantity: 20

Select(int.Parse) to to samo co Select(s => int.Parse(s)), tylko bez jednej warstwy wywołania. Kompilator wybiera przeciążenie pasujące do typu docelowego: int.Parse ma kilka przeciążeń, a do Func<string, int> pasuje tylko Parse(string). Gdy kompilator nie może zdecydować (kilka przeciążeń pasuje równie dobrze albo argumentów typu metody generycznej nie da się wywnioskować z grupy metod), wywołanie się nie kompiluje, a naprawia to lambda, która jawnie zapisuje wywołanie.

Grupa metod użyta jako Func nadal wykonuje się później, przy każdym wywołaniu delegatu, dokładnie jak lambda. Różnica dotyczy tylko zapisu.

Zwracanie funkcji

Metoda może zbudować i zwrócić funkcję. Zwrócony Func niesie ze sobą wartości, z których go zbudowano:

Wynik:

140
180
162

Twice(staff) buduje nową funkcję, która dwa razy stosuje zniżkę pracowniczą: 200, potem 180, potem 162.

Słownik funkcji

Przechowywanie funkcji w słowniku zamienia długi switch w tablicę wyszukiwania. To typowy kształt obsługi poleceń, kalkulatorów i akcji menu, a nowe wpisy można dodawać w trakcie działania programu:

Wynik:

12 + 30 = 42
7 * 6 = 42
9 / 0 = cannot divide by zero
2 ^ 3 = unknown operator
Hello, Sam
DONE

Dodanie operatora to jeden wpis więcej w słowniku, bez zmian w pętli. StringComparer.OrdinalIgnoreCase przekazany do drugiego słownika sprawia, że wielkość liter w nazwach poleceń nie ma znaczenia.

Funkcje async: Func<Task>, nie Action

W kodzie async typ delegatu decyduje o tym, czy kod wywołujący może poczekać na zakończenie pracy:

Func<Task> save = async () => await File.WriteAllTextAsync("a.txt", "data");
await save();                       // the caller can await it and see its exceptions

Func<int, Task<string>> load = async id => await FetchUserAsync(id);
string user = await load(42);

Action bad = async () => await File.WriteAllTextAsync("a.txt", "data");
bad();                              // async void: fire and forget, exceptions escape

Lambda async przypisana do Action staje się metodą async void: nic nie może na nią czekać, a wyjątek w jej wnętrzu nigdy nie dociera do kodu wywołującego. Jest rzucany ponownie w puli wątków (albo w wątku UI w aplikacji desktopowej), gdzie zwykle wywraca proces. Gdy piszesz metodę przyjmującą pracę asynchroniczną, przyjmuj Func<Task> (albo Func<Task<T>>).

Kiedy zadeklarować własny delegat

Func i Action pokrywają większość potrzeb. Zadeklaruj nazwany typ delegatu, gdy:

  • Sygnatura ma parametry ref, out lub in. Func nie potrafi ich wyrazić, więc funkcja w stylu TryParse potrzebuje delegate bool TryParser<T>(string text, out T value);.
  • Nazwa dokumentuje rolę w publicznym API. delegate bool Validator(string input) w sygnaturze metody mówi więcej niż Func<string, bool>, a nazwy jego parametrów widać w IntelliSense.
  • Potrzebujesz parametru params, którego generyczne delegaty też nie mogą mieć.

Zdarzenia to drugie miejsce, w którym nadal pojawiają się własne typy delegatów, choć większość przypadków pokrywa wbudowany EventHandler<TEventArgs>. Deklarowanie i łączenie typów delegatów omawia strona o delegatach.

Najczęściej zadawane pytania

Czym różni się Func od Action w C#?

Func zwraca wartość, a Action nie. W Func<int, string, bool> ostatni argument typu (bool) to typ zwracany, a pozostałe to parametry. Action<int, string> przyjmuje int i string i zwraca void. Oba występują w wersjach od 0 do 16 parametrów.

Jak przekazać funkcję jako parametr w C#?

Zadeklaruj parametr jako Func lub Action o odpowiednim kształcie i wywołaj go w metodzie: static decimal Apply(decimal price, Func<decimal, decimal> rule) => rule(price);. Kod wywołujący przekazuje lambdę (Apply(80m, p => p * 0.9m)) albo nazwę metody (Apply(80m, HalfPrice)).

Czym jest Predicate<T> w C#?

To typ delegatu, który przyjmuje T i zwraca bool, ma więc ten sam kształt co Func<T, bool>. Jest starszy niż Func i używają go List<T>.Find, FindAll, RemoveAll, Exists i Array.Find. Te dwa typy nie konwertują się nawzajem, ale lambda konwertuje się na każdy z nich.

Jak używać Func z kodem asynchronicznym?

Użyj Func<Task> dla funkcji async bez wyniku i Func<T, Task<TResult>> dla funkcji z wynikiem, aby kod wywołujący mógł na nią wykonać await. Nie przypisuj lambdy async do Action: staje się wtedy async void, na którą nie da się czekać i której wyjątków kod wywołujący nie może złapać.

Czy Func może mieć parametry out lub ref?

Nie. Parametry generyczne Func i Action to zwykłe parametry przekazywane przez wartość, więc nie da się zapisać Func<string, out int, bool>. Dla sygnatury w stylu TryParse zadeklaruj własny delegat: delegate bool TryParser<T>(string text, out T value);.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ