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
| Typ | Parametry | Zwraca |
|---|---|---|
Action | brak | void |
Action<T> | T | void |
Action<T1, T2> ... do 16 | T1, T2, ... | void |
Func<TResult> | brak | TResult |
Func<T, TResult> | T | TResult |
Func<T1, T2, TResult> ... do 16 | T1, T2, ... | TResult |
Predicate<T> | T | bool |
Comparison<T> | T, T | int (ujemny, zero, dodatni) |
Converter<TIn, TOut> | TIn | TOut |
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,outlubin.Funcnie potrafi ich wyrazić, więc funkcja w styluTryParsepotrzebujedelegate 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);.