Func ActionSmak NR.1

Dlaczego znowu piszę o delegatach. Otóż widzę, że wiele osób wchodzi na mojego bloga, by się dowiedzieć “co to jest i jak to działa”. W 2011 roku, na szybko napisałem pewien wpis, który swoim przykładem miał zobrazować zależności wynikające z mechanizmu delegaty. W wielkim skrócie wpis mijał się z celem.

Dlatego postanowiłem napisać kolejny wpis o delegatach bez żadnych udziwnień.

Postanowiłem też w tym wpisie poruszyć ważne zagadnienie, które jest reprezentowane przez gotowe delegaty jak “Action<T>” i Func<T,T>.

Obecnie myśląc o delegatach mam na myśli te gotowe. Bez tworzenia swojego mambojambo.

Te gotowe delegaty najczęściej się stosuje. Dlatego też nie dziwi mnie fakt, że nie pamiętam jak tworzy się definicję swojej delegaty, ponieważ obecnie w .NET nie ma takiej potrzeby.

W sumie, to definiowałem delegaty tylko w przykładach edukacyjnych na blogu. Jeśli studiujesz, to zapewne też będziesz pisał taki kod, by zaliczyć przedmiot.

Cytując pewnego komentatora na blogu, mój wykładowca wymaga ode mnie znajomości delegat jakby to był .NET 2.0.

LINQ to kur###. Przejdźmy do rzeczy.

Gotowe delegaty ułatwiają życie. Przychodzą one w 5 smakach:

  • Func
  • Action
  • Predicate
  • Convert
  • Comparsion

Dzisiaj skoncentrujemy się na Action<T> i Func<TParameter, TOutput> .

Wcześniej jednak omówmy jak delegata działa i co ona robi.

Co to jest delegata. Raz jeszcze

Delegata najprościej mówiąc jest wskaźnikiem do metody. Delegata może być przekazywana do metody jako parametr.

Delegata pozwala nam na zmianę implementacji danej funkcji w programie. Wystarczy, że podmienimy metodę w delegacie. Oczywiście jeśli chcemy to zrobić musimy pilnować, by ciało metody, parametry i typ zwracany zgadzały się z delegatą.

W przykładzie posiadam klasę Game.

public class Game
{
    public string Name { get; set; }

    public int Year { get; set; }
}

Mam też klasę z metodami. Metoda “CalculateAgeEra” wyświetli mi napis, w zależności od różnicy obecnego roku z rokiem wydania gry. Wiem ta metoda jest głupia, ale to przykład. Metoda ta przyjmuje parametr int i zwraca napis string.

Mamy metody “UpperCaseName” i “LowerCaseName”. Przyjmują one napis i go odpowiednio modyfikują i zwracają.

public class Methods
{
    public string CalculateAgeEra(int year)
    {
        int currentyear = DateTime.Now.Year - year;

        if (currentyear < 2)
            return "PS4 XBOXONE Era";
        else if (currentyear < 10)
            return "PS3 XBOX Era";
        else if (currentyear < 17)
            return "PS2 GameCube Era";
        else if (currentyear < 21)
            return "PS1 Nintentdo 64 Era";
        else if (currentyear < 31)
            return "Nes Amiga Era";
        else
            return "Pegasus Era";
    }

    public string UpperCaseName(string s)
    {
        return s.ToUpperInvariant();
    }

    public string LowerCaseName(string s)
    {
        return s.ToLowerInvariant();
    }

    public void Show(string p1, string p2)
    {
        Console.ForegroundColor = ConsoleColor.White;
        Console.Write(p1);
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.Write(" : ");
        Console.ForegroundColor = ConsoleColor.Green;
        Console.Write(p2);
        Console.ForegroundColor = ConsoleColor.Gray;

    }

    public void Show2(string p1, string p2)
    {
        Console.ForegroundColor = ConsoleColor.Yellow;
        Console.Write(p1);
        Console.ForegroundColor = ConsoleColor.DarkGreen;
        Console.Write(" : ");
        Console.ForegroundColor = ConsoleColor.Magenta;
        Console.Write(p2);
        Console.ForegroundColor = ConsoleColor.Gray;
    }
}

Metody “Show” i “Show2” wyświetlą dwa napisy w efektowny sposób. Metody te nie zwracają nic.

Przejdźmy więc do gwiazd wpisu, czyli delegat. W programie konsolowym zdefiniowałem trzy delegaty. Będą one wskazywały na odpowiednie metody.

class Program
    {
        //defincja delgaty
        public delegate string calculateAgeEraFunctionPointer(int year);
        //defincja delgaty
        public delegate string changeTitleFunctionPointer(string name);
        //defincja delgaty
        public delegate void showTwoStringsPointer(string p1, string p2);

Użycie delegaty jest proste. Tworzę zmienną typu delegaty, a później przyrównuje ją do odpowiedniej metody.

Ciało metody i delegaty musi się zgadzać.

static void Main(string[] args)
{
    Methods methods = new Methods();

    calculateAgeEraFunctionPointer funcPointer = methods.CalculateAgeEra;

W aplikacji konsolowej w metodzie Main poproszę użytkownika o wybranie metody wyświetlenia napisów.

W zależności od tego, czy użytkownik wcisnął 1 lub 2 do zmiennej “show”, zostanie przypisana inna metoda.

string f = "";

while (!(f == "1" || f == "2"))
{
    Console.WriteLine("Wybierz ukazanie tekstu");
    Console.WriteLine("1. Biało zielony");
    Console.WriteLine("2. Zółt różowy");
    f = Console.ReadKey().KeyChar.ToString();
}
Console.Clear();
showTwoStringsPointer show;

if (f == "1")
    show = methods.Show;
else
    show = methods.Show2;

Dla jedynki zmienna show będzie przechowywać metodę Show, a dla dwójki metodę Show2.

Analogicznie postępuję z kolejną zmienną “titlePointer”, która będzie przechowywać metodę w zależności od wyboru użytkownika.

Dla jedynki titlePointer będzie powiększać napis, dla dwójki będzie go zmniejszać.

f = "";
while (!(f == "1" || f == "2"))
{
    Console.WriteLine("Wybierz ukazanie wielkość tekstu");
    Console.WriteLine("1. Duże litery");
    Console.WriteLine("2. Małe litery");
    f = Console.ReadKey().KeyChar.ToString();
}

Console.Clear();
changeTitleFunctionPointer titlePointer; 

if (f == "1")
    titlePointer = methods.UpperCaseName;
else
    titlePointer = methods.LowerCaseName;

Metoda “RestOfTheProgram” przyjmuje w parametrach właśnie te trzy delegaty. Na poziomie metody RestOfTheProgram nie wiem, co te metody zrobią, wiem tylko, co mają przyjąć i co mają zwrócić.

RestOfTheProgram(funcPointer,titlePointer,show);

W metodzie obliczę erę w jakiej gra powstała oraz zmienię znaki w nazwie gry.

public static void RestOfTheProgram(calculateAgeEraFunctionPointer funcPointer,
    changeTitleFunctionPointer titlePointer,
    showTwoStringsPointer show)
{
    Game game = new Game() { Name = "Chrono Trigger", Year = 1995 };

    string era = funcPointer(game.Year);
    string name = titlePointer(game.Name);

    show(era, name);

    Console.ReadKey();
}

Na koniec wyświetlę erę i nazwę gry.

Program działa tak:

Untitled3_thumb2

Kiedy używać delegat, a nie interfejsów

Jest to bardzo dobre pytanie. Zwłaszcza jeśli myślisz nad przykładami na blogu, a potem się okazuje, że ten sam przykład można zrobić lepiej używając klas abstrakcyjnych lub interfejsów.

Dlaczego więc delegaty są nam potrzebne? Zawsze mogę przecież stworzyć interfejs z metodą.

Jeśli nie chcesz przekazywać interfejsu bądź klasy do innej powłoki aplikacji albo klasy, to delegaty są dla ciebie.

Jeśli kod odwołuje się tylko do metod, a nie do właściwości, czy pól, to delegaty są jak najbardziej na miejscu, bardziej niż interfejsy i klasy abstrakcyjne.

Istnieją też przypadki, w których zdarzeniowa implementacja jest najlepszym rozwiązaniem. Trzeba mieć jednak w takiej sytuacji obraz całego systemu.

Gotowe delegaty

W .NET już od jakiegoś czasu istnieją gotowe delegaty. Pamiętasz te trzy definicje delegat, które napisaliśmy wcześniej. Teraz je zastąpimy gotowymi delegatami Action<T> i Func<TParameter,TOutput>.

Jak widzisz są one typami generycznymi, a to oznacza, że możemy definiować ich ciała.

Przejdźmy więc do delegaty Func.

Func<TParameter, TOutput>

Func jest podobny do bazowej implementacji delegaty. Różnica polega jednak w deklaracji. W trakcie deklaracji ustalamy parametr przyjmowany oraz parametr zwracany.

Metoda kalkulująca erę przyjmuje liczbę całkowitą int i zwraca typ string.

Przy deklaracji Func ustalam wiec na początku typ int, a potem typ string.

static void Main(string[] args)
{
    Methods methods = new Methods();

    Func<int, string> funcPointer = methods.CalculateAgeEra;

Zmienna funcPointer przechowuje teraz metodę “CalculateAgeEra”.

Analogicznie mogę postąpić ze zmienną titlePointer. Przyjmuje ona napis string i ten sam typ zwraca.

Func<string, string> titlePointer; 

if (f == "1")
    titlePointer = methods.UpperCaseName;
else
    titlePointer = methods.LowerCaseName;

RestOfTheProgram(funcPointer,titlePointer,show);

Teraz moja metoda “RestOfTheProgram” w parametrach przyjmuje delegaty Func. Kod nagle stał się bardziej czytelny ponieważ wie, jakie dokładnie ciała metod przechowują te typy Func.

Jest to ogromna zaleta. Wcześniejsze mambojambo tego nie miało.

public static void RestOfTheProgram(Func<int,string> funcPointer,
    Func<string, string> titlePointer,
    showTwoStringsPointer show)
{
    Game game = new Game() { Name = "Chrono Trigger", Year = 1995 };

    string era = funcPointer(game.Year);
    string name = titlePointer(game.Name);

    show(era, name);

    Console.ReadKey();
}

A co jeśli moja metoda przyjmuje więcej parametrów niż jeden, albo wcale.

Nie ma się czego bać, otóż istnieje 17 wersji delegaty Func. Każda z nich przyjmuje więcej parametrów.

Ktoś mógłby powiedzieć, dlaczego tak mało. Jeśli nawet zdarzy ci się napisać metodę, która przyjmuje 17 parametrów, to zmierzysz się z tym, ale raczej jest coś nie tak z twoim kodem.

funfdnfjdnf_thumb2

Do przykładu dodałem kolejną metodę “CheckIfGameExist”. Przyjmuje ona dwa parametry typu string i typu int. Zwraca ona wartość logiczną.

public class Methods
{
    public bool CheckifGameExist(string name,int year)
    {
        return true;
    }

Jak zapisać tę metodę do delegaty Func. Nic trudnego. Piszę na początku typy dwóch parametrów, a potem na końcu typ zwracany.

Func<string, int, bool> gameExistPointer = methods.CheckifGameExist;
bool result = gameExistPointer("Kombat ", 1992);

A co jeśli moja metoda nic nie zwraca.

Nie zapiszemy takiej metody do delegaty Func. W delegacie Func zawsze musimy podać typ zwracany.

Spójrzmy na delegatę Action.

Action<TParameter>

Delegata Action jest używana, gdy chcemy zapisać metodę , która zwraca nic. Co jest określone poprzez sygnaturę void w metodzie.

Delegata Action ma wiele wersji. Wersje różnią się od tego ile parametrów ma przyjmować nasza delegata.

gggg[3]_thumb[2]

Zapiszmy metodę Show do delegaty Action.

Jak pamiętasz metoda Show przyjmuje dwa parametry. Dlatego typ Action przyjmuje dwa typy string.

while (!(f == "1" || f == "2"))
{
    Console.WriteLine("Wybierz ukazanie tekstu");
    Console.WriteLine("1. Biało zielony");
    Console.WriteLine("2. Zółt różowy");
    f = Console.ReadKey().KeyChar.ToString();
}
Console.Clear();
Action<string, string> show;

if (f == "1")
    show = methods.Show;
else
    show = methods.Show2;

Teraz metoda RestOfTheProgram korzysta tylko z gotowych delegat.

public static void RestOfTheProgram(Func<int,string> funcPointer,
    Func<string, string> titlePointer,
    Action<string,string> show)
{
    Game game = new Game() { Name = "Chrono Trigger", Year = 1995 };

    string era = funcPointer(game.Year);
    string name = titlePointer(game.Name);

    show(era, name);

    Console.ReadKey();
}

A co jeśli metoda nie przyjmuje parametrów.

public class Methods
{
    public void AddEnter()
    {
        Console.WriteLine();
    }

Wtedy korzystamy z niegenerycznej wersji metody Action.

Methods methods = new Methods();
Action action = methods.AddEnter;

Jeśli metoda przyjmuje tylko jeden parametr to…

public class Methods
{
    public void AddEnterAndSomethin(string som)
    {
        Console.WriteLine();
        Console.WriteLine(som);
    }

Delegata Action deklaruje w swoich strzałkach tylko jeden parametr.

Methods methods = new Methods();
Action action = methods.AddEnter;
Action<string> action2 = methods.AddEnterAndSomethin;

Od tej pory nie musisz deklarować swoich delegat.

W następnym wpisie opiszemy pozostałe trzy gotowe smaki delegat:

  • Predicate
  • Convert
  • Comparsion