Delegata

Co to jest delegata? Dobre pytanie na rozmowę kwalifikacyjną.

Delegata jest wskaźnikiem do danej metody bądź metod. Delegata przetrzymuje więc referencję do danej funkcji.

Istnieje jeden wymóg.

Aby delegata wskazywała do danej metody – delegata i metoda muszą mieć taką samą sygnaturę. Czyli przyjmować takie same parametry i zwracać taki sam typ.

Na moim blogu wpis o delegatach jest jednym z najpopularniejszych. Dużo osób wpisuje do wyszukiwarki : “C# delegata”. Mój wpis jednak powstał w 4 godziny i próbował za bardzo zobrazować odseparowanie zależności. Zanim włączyłem komentarze disques wiele osób poprosiło mnie o rozwinięcie tego tematu.

W tym wpisie stworze prostą aplikację WPF, która będzie wykorzystywać funkcje delegaty.

Przygotowanie aplikacji WPF

Sama aplikacja WPF nie jest istotna, ale potrzebujemy jakiegoś interfejsu.

WPF

Aplikacja będzie zawierać 4 przyciski. Każdy z przycisków będzie dodawać odpowiednią operację, która wyświetli się na liście.

Apliacja WPF

Przykładowo po takim zapisie oczekuję, że liczba początkowa równa zero zostanie podana następującym operacjom.

Interfejs nie jest szczególnie piękny. Oto kod XAML aplikacji WPF.

<Window x:Class="PowerOfDelegateWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="339" Width="525">
    <Grid Height="319" VerticalAlignment="Top">
        <StackPanel Margin="10,10,328,129" >
            <Button Tag="Plus" Content="Dodaj operacje dodawania"
                    Margin="0,0,0,10" Height="30" Width="165" 
                    Click="Button_AddOperaction_Click" />
            <Button Tag="Minus" Content="Dodaj operacje odejmowania"
                    Margin="0,0,0,10" Height="30" Width="165" 
                    Click="Button_AddOperaction_Click" />
            <Button Tag="Mno" Content="Dodaj operacje mnożenia"
                    Margin="0,0,0,10" Height="30" Width="165" 
                    Click="Button_AddOperaction_Click" />
            <Button Tag="Dzie" Content="Dodaj operacje dzielenia"
                    Margin="0,0,0,10" Height="30" Width="165" 
                    Click="Button_AddOperaction_Click" />
        </StackPanel>
        <ListBox Name="lbOperations" HorizontalAlignment="Left"
                 Height="115" Margin="194,44,0,0" VerticalAlignment="Top" Width="313"/>
        <Label Content="Lista operacji:" HorizontalAlignment="Left"
               Margin="194,10,0,0" VerticalAlignment="Top" 
               RenderTransformOrigin="0.342,0.077" Width="313"/>
        <TextBox Name="txtNumber" HorizontalAlignment="Left" Height="26"
               Margin="375,164,0,0" TextWrapping="Wrap" Text="100"
                 VerticalAlignment="Top" Width="132"/>
        <Label Content="Zmienna modyfikująca" HorizontalAlignment="Left"
               Margin="194,164,0,0" VerticalAlignment="Top" Width="181"/>
        <Label Name="lblResult" Content="Wynik:" HorizontalAlignment="Left"
               Margin="10,235,0,0" VerticalAlignment="Top" FontSize="16" Width="497"/>
        <Button Content="Wykonaj" HorizontalAlignment="Left"
                Margin="194,195,0,0" VerticalAlignment="Top" Width="313"
                Click="Button_Run_Click" Height="35"/>
        <Label x:Name="lblResult_2" Content="Wynik:" 
               HorizontalAlignment="Left" Margin="10,271,0,0"
               VerticalAlignment="Top" FontSize="16" Width="497"/>
    </Grid>
</Window>

Każdy z przycisków operacji wykonuje zdarzenie “Button_AddOperaction_Click”.

private void Button_AddOperaction_Click(object sender, RoutedEventArgs e)
{
    Button b = (Button)e.Source;
    string s = "";
    if (b.Tag.ToString() == "Plus")
    {
            s = "Dodawanie :" + txtNumber.Text;
    }
    if (b.Tag.ToString() == "Minus")
    {
        s = "Odejmowanie :" + txtNumber.Text;
    }
    if (b.Tag.ToString() == "Dzie")
    {
        s = "Dzielenie :" + txtNumber.Text;
    }
    if (b.Tag.ToString() == "Mno")
    {
        s = "Mnożenie :" + txtNumber.Text;
    }

    AddToListBox(s);
}

public void AddToListBox(string text)
{
    lbOperations.Items.Add(text);
}

Zdarzenie to uzupełnia listę operacji, która później będzie odczytywana przy użyciu delegat.

Delegaty

W aplikacji będę wykorzystywał dwie delegaty. Jedna będzie przeznaczona do wykonywania jednej operacji i będzie zwracać wyniki. Druga będzie wykonywać wiele operacji i nie będzie zwracała żadnego wyniku.

public delegate int DelegateOneOperationRunner(int a, int b);
public delegate void DelegateOperationsRunner(int a, int b);

Do delegaty możemy przypisywać metody tylko o podobnej sygnaturze.


public enum Operation
{
    Add,
    Substrack,
    Mutyply,
    Division
}

Typ wyliczeniowy “Operation” określa liczbę operacji, które są możliwe do przypisywania w aplikacji.

Oto definicja klasy “OneOperationRunner”, która wykorzystuje delegatę “DelegateOneOperationRunner”.

public class OneOperationRunner
{
    public DelegateOneOperationRunner Run(Operation operation)
    {
        DelegateOneOperationRunner runner = null;

        if (operation == Operation.Add)
            runner += Add;

        if (operation == Operation.Substrack)
            runner += Substract;

        if (operation == Operation.Mutyply)
            runner += Multyply;

        if (operation == Operation.Division)
            runner += Division;

        return runner;
    }

    private int Add(int a, int b) { return a + b; }
    private int Substract(int a, int b) { return a - b; }
    private int Multyply(int a, int b) { return a * b; }
    private int Division(int a, int b) { return a / b; }
}

Istnieją trzy kroki w definiowaniu i używaniu delegat

  • Deklaracja
  • Inicjalizowanie
  • Wywołanie (uruchomienie)

W metodzie klasy “Run” widzisz, że tworzę deklarację delegaty, która jest pusta. Później w zależności od typu wyliczeniowego do delegaty zostanie odpowiednia metoda. Jest to krok inicjalizowania, gdyż teraz delegata przechowuje jedną metodę.

Metoda ta zwraca delegatę. Zostanie ona później uruchomiona wraz z przypisaną metodą. Delegaty uruchamiamy metodą “Invoke()”.

Wiele metod w delegacie. Fachowa nazwa MultiCastingDelegate

Delegata może przetrzymywać listę metod. By to pokazać stworzyłem drugą klasę, która tym razem będzie wykorzystywać delegatę DelegateOperationsRunner.

Delegata taka w swojej metodzie “Invoke” uruchomi przekazaną listę metod. Do delegaty dodajemy metody używając wyrażenia “+=”. Możemy także usuwać dodane wcześniej metody używając wyrażenia “-=”.

Klasa OperationsRunner przetrzymuje delegatę. Do tej delegaty będą dodawane funkcje przy użyciu metody “AddOperation”. Wszystkie dodane wcześniej operacje zostaną wykonane przy wywołaniu metody “Run”.

public class OperationsRunner
{
    private DelegateOperationsRunner runner = null;

    public void AddOperation(Operation operation)
    {
        if (operation == Operation.Add)
            runner += Add;

        if (operation == Operation.Substrack)
            runner += Substract;

        if (operation == Operation.Mutyply)
            runner += Multyply;

        if (operation == Operation.Division)
            runner += Division;
    }

    public void Run(int a, int b)
    {
        if (runner == null)
            return;

        runner.Invoke(a, b);
    }

    public int Result { get; private set; }

    private void Add(int a, int b)
    {
        if (Result == 0) { Result = a + b; } 
        else { Result = Result + b; }
    }
    private void Substract(int a, int b)
    {
        if (Result == 0) { Result = a - b; } 
        else { Result = Result - b; }
    }
    private void Multyply(int a, int b)
    {
        if (Result == 0) { Result = a * b; } 
        else { Result = Result * b; }
    }
    private void Division(int a, int b)
    {
        if (Result == 0) { Result = a / b; } 
        else { Result = Result / b; }
    }
}

Właściwość “Result” będzie przetrzymywać wynik operacji matematycznych. Wynik zwracany przez obie klasy będzie inny i zaraz zobaczysz dlaczego.

Powrót do aplikacji WPF

Zdarzenie kliknięcia przycisku “Wykonaj” będzie spacerować po uzupełnionej liście operacji. Klasa “OneOperationRunner” będzie zwracać odpowiednią delegatę w zależności od wartości przesłanego typu wyliczeniowego. Delegata ta będzie wykonywana w każdej iteracji w pętli.

Klasa OperationRunner będzie uzupełniać swoją delegatę w pętli. Po skończeniu pętli wykonany listę metod.

Różnica tego rozwiązania polega na tym, że w drugim przypadku możemy przesłać tylko raz parametr do listy metod.

Co zatem idzie, obie metody nie będą działały tak samo. Wynik drugiej klasy będzie zależny od ostatniej wartości wpisanej w polu tekstowym.

private void Button_Run_Click(object sender, RoutedEventArgs e)
{
    OneOperationRunner runner = new OneOperationRunner();
    OperationsRunner r = new OperationsRunner();

    int a = 0; //Startowa wartość

    foreach (string item in lbOperations.Items)
    {
        var tab = item.Split(':');

        int b = int.Parse(tab[1]);

        DelegateOneOperationRunner del = runner.Run(Operation.Add);

        if (item.Contains("Dodawanie"))
        {
            del = runner.Run(Operation.Add);

            r.AddOperation(Operation.Add);
        }

        if (item.Contains("Odejmowanie"))
        {
            del = runner.Run(Operation.Substrack);

            r.AddOperation(Operation.Substrack);
        }

        if (item.Contains("Mnożenie"))
        {
            del = runner.Run(Operation.Mutyply);

            r.AddOperation(Operation.Mutyply);
        }

        if (item.Contains("Dzielenie"))
        {
            del = runner.Run(Operation.Division);

            r.AddOperation(Operation.Division);
        }

        //Wykoanie operacji pojedyńczo
        a = del.Invoke(a, b);
    }

    //Wykonanie operacji na raz
    r.Run(0, int.Parse(txtNumber.Text));
    int many = r.Result;
    //Wyświetlenie wyników
    lblResult.Content = "Po kolei: " + a.ToString();
    lblResult_2.Content = "Wszystkie operacje na raz na zmiennej w polu tekstowym: " 
    + many.ToString();
}

Działanie programu

Program nie jest odporny na błędy użytkownika jak “wpisywanie wartości nieliczbowych do textboxu”.

Program działają prawidłowo. Pierwsza klasa wykonuje wzór i zaokrągla wyniki do liczby całkowitej:

((0 + 100) * 100) – 5) / 5 = 1999

Druga klasa wykonuje listę operacji na parametrze 5:

(0 + 5) * 5) – 5) / 5 = 4

 

Delegaty

Jest to bardzo prosty przykład użycia delegat. Ich potęga polega na odseparowaniu zależności definicji danych metody. Ten prosty przykład nie ukazuje tego, ale ze względu na zainteresowanie delegatami na moim blogu być może przygotuję jeszcze lepszy wpis o tej tematyce.

Edit z 2022: Kod jest do pobrania na GitHubie PanNiebieski/PowerOfDelegateWPF (github.com)