Przed Tobą następny artykuł o języku C# 🔔! Pokażę czym w języku C# jest słowo kluczowe "Func", jak je stosować w praktyce i w jaki sposób może nam pomóc w znacznym podwyższeniu jakości kodu źródłowego 📈! Jak zaciekawiłem, to zapraszam do środka!

WYJAŚNIAM DLACZEGO WARTO STOSOWAĆ "FUNC" W JĘZYKU C#!

Uwaga ⚠️! Zagadnienie związane z tytułowym hasłem wymaga rozumienia delegatów, jak również typów generycznych, więc w pierwszym punkcie sugeruję najpierw zapoznać się z załączonymi materiałami, aby później wrócić tutaj i mieć większe szanse na opanowanie tematu.

Na początek podam definicję hasła "Func", aby móc potem rozwinąć treść o dalsze szczegóły.

Czym jest "Func" w języku C#?

Gdy natrafisz na słowo "Func", to w języku C# oznacza innymi słowy alias delegata metody zwracającej wartość jakiegoś typu (metoda typu różnego od "void"). W praktyce otwiera to możliwość dosłownego umieszczenia instrukcji do wykonania podczas działania metody, w której te instrukcje są zawarte. Przykładem takiej sytuacji jest wykonanie wskazanych instrukcji po wykonaniu jakiegoś pierwszorzędnego zadania przez daną metodę. Jeszcze większe możliwości daje fakt, iż ta metoda "wewnętrzna" również może przyjmować własne parametry, a "Func" udostępnia aż 17 różnych postaci, które różnią się liczbą akceptowanych typów generycznych (czyli "miejsc" na parametry wskazanego typu) od 0 do 16 🧨!

"Func" najczęściej występuje jako parametr innej metody i to pozwala na swobodne określenie co ma się wydarzyć w trakcie wywoływania metody, którą "Func" reprezentuje ❤️. Co więcej, przy każdym osobnym wywołaniu tej samej metody, możesz wstawić zupełnie inne instrukcje do wykonania, co przypomina bardzo elastyczny "element wymienny" 🤩! To sprawia, że słowo "Func" jest jedną z tych konstrukcji, które wymieniłbym jako warte jak najszybszego opanowania i wdrożenia w praktykę 🔥! Nie tylko zwiększa elastyczność metody i eliminuje duplikację kodu, lecz także dobrze wygląda w portfolio i dowodzi Twojej profesjonalności 🌟!

"Func" jest wygodnym skrótem następującego zapisu delegata 👇:

delegate T MyDelegate([opcjonalne parametry]);

Należy pamiętać o jednej ważnej rzeczy: "Func" dotyczy tylko tych funkcji, które mają na wyjściu zwrócić wartość jakiegoś typu ↩️. Dla funkcji typu "void" (niezwracających żadnej wartości) przeznaczone jest hasło "stojące obok" wyżej wymienionego: "Action", które także warto znać, a o nim napisałem dużo więcej w odrębnym materiale.

To wszystko z części teoretycznej, więc czas na kod źródłowy!

Jak skorzystać z "Func" w języku C#?

Odpowiedź podzielę na 2 przypadki, ponieważ oba są związane z tematem, a podejście do nich jest zupełnie inne 👇:

  1. definicja metody przyjmującej typ "Func",
  2. wywoływanie metody przyjmującej typ "Func".

Wyjaśnię najpierw jedno, a potem drugie.

Jak skorzystać z "Func" w języku C# podczas definiowania metody?

Definiowanie metody, która ma przyjmować typ "Func" w miejscu parametru, po prostu piszemy wspomniane hasło i nazwę tego parametru. Tylko tym razem uwaga! Musimy też określić typ wartości zwracanej przez tę metodę, który wstawiamy wewnątrz nawiasów kątowych 💥! To oznacza typ generyczny.

To jest przykład zdefiniowania miejsca dla metody zwracającej wartość typu całkowitoliczbowego 👇:

void DoSomething(Func<int> function)
{
	// instrukcje
}

To jednak okaże się niewystarczające ⛔. Aby móc tę funkcję "uruchomić", należy wśród innych instrukcji metody napisać wywołanie tejże funkcji ⏩! Ono powinno Ci już być znane 👇:

void DoSomething(Func<int> function)
{
	// instrukcje
	
	int result = function();

	// instrukcje
}

Dopiero gdy przejdziemy do tego wywołania, wtedy ta funkcja przekazywana jako parametr, faktycznie będzie wykonywana 🚀!

W tym przypadku jest przypisanie wyniku działania do zmiennej, ponieważ przypominam, że rozmawiamy cały czas o funkcji zwracającej wartość, bo tylko w przypadku takich funkcji użycie "Func" będzie miało ręce i nogi 😊.

Zauważ, że mimo tego, iż funkcja występująca w parametrze musi zwracać wartość, to cała funkcja posiadająca ten parametr nie musi się trzymać tej reguły! Ona sama może być typu "void" (tak jak na powyższym przykładzie) lub jakiegokolwiek innego i to jest jak najbardziej poprawny zabieg ✅. Wartość jaka zostanie zwrócona i tak będzie znajdować się tylko w obrębie bloku całej metody.

Gdy określimy sobie instrukcje właśnie w takiej formie, to to będzie oznaczać następujący przebieg zdarzeń 👇:

  1. instrukcje wykonują się jedna po drugiej,
  2. następuje wywołanie metody z parametru typu "Func" (wywoływanie metody zawierającej parametr "Func" jest chwilowo wstrzymane),
  3. po zakończeniu wszystkich instrukcji metody z parametru typu "Func", jest wznawiana obecna metoda i wykonywanie pozostałych instrukcji (jeśli jeszcze jakieś występują).

"Func" zawsze wymaga wstawienia wewnątrz nawiasów kątowych co najmniej jednego parametru, który oznacza typ zwracanej wartości. Natomiast możemy także określić parametry wejściowe jakie ma przyjmować ta metody i umieszczamy je dokładnie w tym samym miejscu, tylko trzeba wiedzieć jak poprawnie interpretować kolejność tych typów 🧩.

Jak określić parametry wejściowe dla funkcji umieszczonych w miejsce "Func" w języku C#?

Załóżmy, że funkcja zwracająca wartość typu "int" ma przyjmować jeden parametr w postaci łańcucha znaków ("string"), tak aby było łatwiej dostrzec kontrast 😊. Wówczas, "Func" powinniśmy napisać w taki sposób 👇:

Func<string, int>

Wtedy zapis interpretujemy tak 👇:

  1. pierwszy typ generyczny = pierwszy parametr wejściowy,
  2. drugi typ generyczny = typ wartości zwracanej na wyjściu.

Czyli zobacz, że typy parametrów wejściowych pojawiają się jako pierwsze! Dokładnie w taki sam sposób odczytujemy zapisy "Func" dla jeszcze większej liczby parametrów 👇:

Func<string, float, int, int>

To będzie oznaczać 👇:

  1. pierwszy typ generyczny = pierwszy parametr wejściowy,
  2. drugi typ generyczny = drugi parametr wejściowy,
  3. trzeci typ generyczny = trzeci parametr wejściowy,
  4. ostatni typ generyczny = typ wartości zwracanej na wyjściu.

Ujmując krótko, każdy n-ty typ generyczny umieszczony wewnątrz ostrych nawiasów z wyjątkiem ostatniego, to parametr wejściowy funkcji występujący w tej samej kolejności (zgodnie z ciągiem), zaś ostatni definiuje nam typ zwracanej wartości i to zawsze jest jedyny, który musi koniecznie się znaleźć w definicji sygnatury!

Trzeba pamiętać o tym, że standard C# udostępnia aż 17 różnych postaci takich form zapisu z nawiasami kątowymi, które wyróżniają się liczbą parametrów wejściowych od 0 do 16, a ta najprostsza (i każda inna) oczekuje określenia typu wartości zwracanej na wyjściu.

To by było na tyle odnośnie określania parametrów 🙂.

Kolejna rzecz o jakiej trzeba wiedzieć to to, że parametr typu "Func", tak jak każdy inny, może przyjąć wartość "null" ⚠️! A co za tym idzie, próba wywołania metody której nie ma, zakończy się zgłoszeniem wyjątku i przerwaniem programu, jeśli wyjątek nie jest obsłużony 💥!

Jak zabezpieczyć się przed wartością "null" dla typu "Func" w języku C#?

Wystarczy, że otoczysz wywołanie instrukcją warunkową sprawdzającą czy zmienna nie przyjmuje wartości "null" 👇:

if(function != null)
{
	int result = function();
}

Istnieje dużo prostszy i wygodniejszy zapis polegający na połączeniu operatora sprawdzającego wartość "null" (ang. null conditional operator) z wywołaniem metody "Invoke" 👇:

int result = function?.Invoke();

Podczas przypisywania wartości z operatorem sprawdzającym, trzeba pamiętać, że jeżeli się nie uda wywołać metody, to zmienna przyjmie wartość "null"!

Obojętnie jakiej metody użyjemy, tak skutecznie zapobiegniemy próbie wywołania metody, której nie ma 👍.

To wszystko z tematu definiowania metody z parametrem typu "Func", a więc teraz czas na wyjaśnienie użycia od drugiej strony!

Jak skorzystać z "Func" w języku C# podczas wywoływania metody?

Wywoływanie metody z parametrem typu "Func" 👇:

void DoSomething(Func<int> function)

możemy przeprowadzić używając 2 sposobów zapisu 👇:

  1. wyrażenie lambda (funkcja anonimowa),
  2. referencja do metody.

Wyrażenie lambda można by określić funkcją przekazywaną "na miejscu" (wstawioną bezpośrednio w miejsce parametru), która występuje tylko dla danego wywołania funkcji i nigdzie indziej 👇:

DoSomething(5, x => x*x);

Jeżeli interesuje Cię temat wyrażeń lambda, bardziej szczegółowo został opisany w odrębnym materiale.

Drugim sposobem jest referencja do metody, czyli zdefiniowanie metody w którymkolwiek innym miejscu i wskazanie jej poprzez umieszczenie samej jej nazwy (czyli de facto referencji, bo funkcja jest wskaźnikiem), o ile jest dostępu publicznego. Gdybyśmy przykładowo mieli definicję takiej metody 👇:

int GetSquareOf(int x)
{
	return x*x;
}

to wtedy możesz z niej skorzystać poprzez umieszczenie jej nazwy (referencji) w miejsce parametru takiej funkcji typu "Func" oczekiwanej przez inną (zakładając, że w pełni zgadza się zarówno typ zwracanej wartości, jak i lista parametrów wejściowych)! Piszemy wtedy tak 👇:

DoSomething(5, GetSquareOf);

Niezależnie od tego jaką drogą pójdziesz, pamiętaj o jednym: musi występować 100% zgodność z sygnaturą metody, a to oznacza identyczność w typie zwracanej wartości oraz liście parametrów (zarówno liczba, jak i typy) ⚠️!

Zakończyliśmy etap wyjaśniania samego podejścia do typu "Func" po stronie definiowania i wywoływania ✅, więc został nam krótki przykład na zakończenie 🚀!

Przykład użycia "Func" w języku C#

Przykład dotyczy zadania polegającego na utworzeniu sobie takiej metody, która ma przyjmować 2 parametry: liczbę zmiennoprzecinkową oraz metodę akceptującą jeden parametr typu zmiennoprzecinkowego i zwracającą wartość tego typu, tak abyśmy byli w stanie umieścić dwie różne metody w dwóch osobnych wywołaniach tej samej metody.

W C# wykonamy to zadanie bardzo prosto, stosując przykładowo takie podejście 👇:

public static void Main()
{
	double number = 16.0;

	InvokeFuncOnNumber(number, Math.Sqrt);
	InvokeFuncOnNumber(number, GetSquareOf);
}

private static void InvokeFuncOnNumber(double number, Func<double, double> function)
{
	Console.WriteLine("Wykonuję podaną metodę");
	Console.WriteLine($"Wynik metody: {function?.Invoke(number)}");
}

private static double GetSquareOf(double number) => number*number;

Wyjaśniam po kolei co się dzieje 💡!

Metoda uruchomieniowa "Main" posiada zmienną typu "double" oznaczającą liczbę jaka zostanie wprowadzona jako parametr dla dwóch osobnych wywołań tej samej metody, która pozwoli na umieszczenie dowolnych instrukcji do wykonania. Zaraz po definicji zmiennej, występują dwa osobne wywołania tej metody. Pierwsze z nich zawiera metodę do wyznaczania pierwiastka kwadratowego z podanej liczby. Drugie wywołanie korzysta z referencji do metody zdefiniowanej na samym dole, która wyznacza kwadrat podanej liczby.

Zwróć uwagę, że możemy skorzystać z referencji do metody również dla funkcji matematycznej, ponieważ zarówno typ zwracanej wartości ("double"), jak i lista parametrów (tylko jeden typu "double"), w całości pokrywają się z zapisem parametru "Func". Jeżeli typ oczekiwanego parametru byłby inny (np. "int"), wtedy musielibyśmy napisać w formie wyrażenia lambda (trochę dookoła).

Dalej mamy metodę pozwalającą na wywołanie wskazanej innej funkcji, która ma zwrócić wartość typu "double" 🔥. Przyjmuje 2 parametry jako liczbę i funkcję do wykonania zwracającą wartość, czyli to będzie typ "Func" ✔️. W nawiasach kątowych umieszczamy 2 typy generyczne oznaczające kolejno typ pierwszego parametru i typ wartości na wyjściu. W środku metody jest wypisanie komunikatu o wejściu do niej, a następnie wypisanie drugiego komunikatu na temat wyniku podanej metody na wyjściu (użyłem zapisu interpolacji łańcucha, która pozwala na bardziej zwięzłą formę przekazywania wyników wyrażeń do łańcucha bez konkatenacji 👍). Korzystamy z zabezpieczenia przed wartością "null", więc w przypadku braku jakiejkolwiek funkcji, po prostu nic nie wypisze i program jest bezpieczny również od tej strony 🔒.

Ostatnia metoda występująca na samym dole zwraca kwadrat podanej liczby typu zmiennoprzecinkowego ("double"), która jest użyta w drugim wywołaniu metody akceptującej parametr typu "Func". Ponieważ składa się tylko z jednej instrukcji, idealne jest skorzystanie ze skróconego zapisu jednowierszowego, w którym tylko wstawiamy wyrażenie jakie ma być zwrócone na wyjściu i to wszystko ✅.

Kompilacja tego programu sprawi, że na wyjściu ujrzysz po 2 komunikaty z dwóch wywołań jednej metody. Najpierw wiadomości dotyczyć będą wywołania metody wyznaczającej pierwiastek kwadratowy z liczby podanej na wejściu, a następnie metody wyznaczającej kwadrat tej liczby. Tak oto jedna ta sama metoda, była w stanie wywołać dwa różne bloki instrukcji 😄!

"Func" w języku C# jako parametr metody

"Func" w języku C# jest aliasem delegata przeznaczonego dla metod zwracających wartość jakiegokolwiek typu (inny niż "void").


Koniec tematu. Myślę, że udało się w sposób wyczerpujący wytłumaczyć hasło "Func" 🙂.

PODOBNE ARTYKUŁY