Nasza strona używa cookies. Dowiedz się więcej o celu ich używania i zmianie ustawień w przeglądarce. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Action i Func w C#

Matt Eland Software Development Manager / MoveHQ
Sprawdź, jak stosować Action i Func, aby jeszcze lepiej rozwijać swój kod w C#.
Action i Func w C#

Pięć lat temu dopadł mnie zastój. Mój kod osiągnął już pewien poziom i ciężko było mi go dalej rozwijać. Oto, jak wykorzystałem aspekty programowania funkcyjnego, by kontynuować efektywne doskonalenie mojego kodu.

Mój kod był dość SOLID-ny, ale nadal istniało w nim wiele bardzo podobnych linijek, pomimo usilnej próby usunięcia duplikatów, gdzie tylko było to możliwe. Nie było to dokładne powielanie, ale powtarzające się wzorce w całym kodzie sprawiły, że utrzymywanie go sprawiało więcej problemów niż powinno.

Potem odkryłem, jak stosować Action i Func, aby jeszcze bardziej ulepszyć mój kod.

Spójrzmy na hipotetyczny przykład:

// Associates Degree
if (resume.HasAssociatesDegree)
{
    try
    {
        resume.AssociatesDegreeScore = CalculateAssociatesDegreeScore();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Could not calculate associates degree score: {ex.Message}");
        resume.AssociatesDegreeScore = 0;
    }
}
else
{
    resume.AssociatesDegreeScore = 0;
}

// Bachelors Degree
if (resume.HasBachelorsDegree)
{
    try
    {
        resume.BachelorsDegreeScore = CalculateBachelorsDegreeScore();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Could not calculate bachelors degree score: {ex.Message}");
        resume.BachelorsDegreeScore = 0;
    }
}
else
{
    resume.BachelorsDegreeScore = 0;
}

// Masters Degree
if (resume.HasMastersDegree)
{
    try
    {
        resume.MastersDegreeScore = CalculateMastersDegreeScore();
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"Could not calculate masters degree score: {ex.Message}");
        resume.MastersDegreeScore = 0;
    }
}
else
{
    resume.MastersDegreeScore = 0;
}


Chociaż jest to wymyślony przykład, ilustruje problem. W tym scenariuszu mamy powtarzalny wzorzec sprawdzania właściwości, a następnie wykonywania niestandardowej logiki, której nie należy wywoływać, jeśli właściwość była fałszywa, a następnie zapisywania wyniku w niestandardowej właściwości na obiekcie.

Jest to prosta procedura, ale duplikacja jest oczywista i wyciągnięcie metod jest utrudnione, ponieważ nie można wywołać CalculateX, jeżeli CV nie ma odpowiedniego stopnia naukowego.

Dodatkowo załóżmy, że wystąpił błąd i konieczna była zmiana kodu. Musisz teraz dokonać tej samej zmiany w trzech miejscach. Jeśli przegapisz jedną, prawdopodobnie spowoduje to błędy. Dodatkowo podobieństwa kuszą Cię do tworzenia programów opartych na kopiowaniu i wklejaniu, co jest antywzorcem i potencjalnym źródłem błędów, jeśli nie wszystkie linie wymagające modyfikacji, zostały zmodyfikowane.

Nauczyłem się, że przekazywanie typów Action i Func pozwala uzyskać o wiele większą elastyczność metod, zapewniając im konfigurowalne zachowanie.

Action to generyczna sygnatura metody, która nie zwraca wartości.

Na przykład sygnatura czegoś, co przyjmuje parametr boolean i liczbę całkowitą, wyglądałby tak: Action<bool, int> myAction;

Func jest bardzo podobny do Action, z tą różnicą, że zwraca wartość. Typ zwróconej wartości jest określony przez ostatni argument. Na przykład Func<bool, int> przyjmie wartość logiczną i zwróci liczbę całkowitą.

Tak więc możemy użyć Func w naszym przykładzie, aby wyodrębnić metodę z pewnymi konfigurowalnymi zachowaniami:

private decimal EvaluateEducation(
  bool hasAppropriateDegree, 
  string degreeName, 
  Func<decimal> calculateFunc)
{
    if (hasAppropriateDegree)
    {
        try
        {
            // Invoke the function provided and return its result
            return calculateFunc(); 
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine($"Could not calculate {degreeName} score: {ex.Message}");
        }
    }

    return 0;
}


Poprawia to nasz kod w następujący sposób:

resume.AssociatesDegreeScore = EvaluateEducation(resume.HasAssociatesDegree, 
  "associates", 
  () => CalculateAssociatesDegreeScore());

resume.BachelorsDegreeScore = EvaluateEducation(resume.HasBachelorsDegree, 
  "bachelors", 
  () => CalculateBachelorsDegreeScore());

resume.MastersDegreeScore = EvaluateEducation(resume.HasMastersDegree, 
  "masters", 
  () => CalculateMastersDegreeScore());


Jest to o wiele prostsze, choć składnia jest nieco trudniejsza do odczytania. W trzecim parametrze tej metody deklarujemy wyrażenie lambda podobne do tych, których używamy do zapytań LINQ.

Jeśli musisz pracować z Func, który używa parametrów (powiedzmy, że masz sygnaturę Func<int, decimal>, możesz zmienić logikę w ten sposób:

(myInt) => myInt + CalculateMastersDegreeScore();


Podsumowanie

Chociaż był to wymyślony przykład, mam nadzieję, że ilustruje on siłę przekazywania Func i Action do różnych metod. Jest to podstawa, na której zbudowane jest programowanie funkcyjne, ale w małych dawkach, może to być niezwykle pomocne również w programowaniu obiektowym.

Chociaż ta składnia sprawia, że kod jest nieco trudniejszy do odczytania, korzyści w utrzymywaniu są znaczące, a prawdopodobieństwo wprowadzania defektów związanych z duplikacją, jest znacznie niższe.


Oryginał tekstu w języku angielskim przeczytasz tutaj.

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 120 tysiącami naszych czytelników

Dowiedz się więcej