Delegat jest typem reprezentującym metody o danej sygnaturze. Najprościej wyjaśnić to na przykładzie.
public delegate void TextHandler(string input);
W powyższej linijce definiujemy nowy typ - podobnie jak definiowalibyśmy typ za pomocą słowa class czy struct. Obiekty należące do typu TextHandler będą przechowywały w sobie referencję do dowolnej metody zwracającej void, a przyjmującej string jako parametr. Przyjrzyjmy się poniższemu programowi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public delegate void TextHandler(string input); class Printer { public void PrintToConsole(string input) { Console.WriteLine(input); } public void PrintToConsoleInBlue(string input) { Console.BackgroundColor = ConsoleColor.Blue; Console.WriteLine(input); Console.BackgroundColor = ConsoleColor.Black; } } class Program { static void Main(string[] args) { var printer = new Printer(); TextHandler textHandler = new TextHandler(printer.PrintToConsoleInBlue); textHandler("siemano na niebiesko"); textHandler = new TextHandler(printer.PrintToConsole); textHandler("siemano"); Console.ReadKey(); } } |
W linijce 1 definiujemy delegat TextHandler. Klasa Printer posiada dwie metody zgodne z sygnaturą delegatu - PrintToConsole i PrintToConsoleInBlue. A zatem obiekt typu TextHandler może w sobie przechowywać zarówno referencję do PrintToConsole, jak i do PrintToConsoleInBlue. Gdyby któraś z tych metod przyjmowała inne parametry niż string, albo zwracała inny typ niż void, nie byłaby już zgodna z typem TextHandler.
W linijce 23 tworzymy obiekt delegatu. Konstruktor delegatu przyjmuje jako parametr metodę, której sygnatura musi być zgodna z tym, co określiliśmy definiując delegat - dlatego możemy jako parametr przekazać printer.PrintToConsoleInBlue. W linijce 25 podmieniamy wartość obiektu textHandler na nowy obiekt delegatu - tym razem do konstruktora przekazując printer.PrintToConsole. Wynikiem działania programu jest:
siemano na niebiesko siemano
Ważną cechą delegatów jest to, że możemy do nich "podpiąć" więcej niż jedną metodę. Służy do tego wygodny i intuicyjny operator +=. Oczywiście możemy też usuwać metody za pomocą operatora -=. Kiedy do delegatu "podpięta" jest więcej niż jedna metoda, w momencie zawołania go po prostu wywołają się wszystkie metody, jedna po drugiej.
public delegate void TextHandler(string input); class Printer { public void PrintToConsole(string input) { Console.WriteLine(input); } public void PrintToConsoleInBlue(string input) { Console.BackgroundColor = ConsoleColor.Blue; Console.WriteLine(input); Console.BackgroundColor = ConsoleColor.Black; } } class Program { static void Main(string[] args) { var printer = new Printer(); TextHandler textHandler = new TextHandler(printer.PrintToConsoleInBlue); textHandler += printer.PrintToConsole; textHandler("siemano"); Console.WriteLine(); textHandler -= printer.PrintToConsoleInBlue; textHandler("siemano po raz drugi"); Console.ReadKey(); } }
Zastanówmy się jaki będzie wynik tego programu. Za pierwszym zawołaniem textHandlera mamy do niego podpięte dwie metody: PrintToConsoleInBlue i PrintToConsole. Po tym zawołaniu odpinamy metodę PrintToConsoleInBlue. Zostaje tylko metoda PrintToConsole. Dlatego za drugim razem tekst wypisuje się tylko raz:
siemano siemano siemano po raz drugi
Przy okazji pragnę zwrócić uwagę na pewien drobiazg. Ten zapis:
var textHandler = new TextHandler(printer.PrintToConsole);
Jest jednoznaczny z:
var textHandler = printer.PrintToConsole;
ReSharper zasugeruje nam skrócenie zapisu, jeśli wprost użyjemy konstruktora delegatu. Jest to oczywiście dobry pomysł, dlatego w dalszej części posta będę posługiwać się krótszym zapisem.
Chcę jeszcze wspomnieć, że delegat - jak każdy typ referencyjny w .NET - dziedziczy z klasy bazowej Object. A wchodząc głębiej w szczegóły: delegat dziedziczy z klasy MulticastDelegate, która dziedziczy z Delegate, która zaś dziedziczy z Object. W czasie kompilacji do Common Intermediate Language (CIL) kompilator przekształca nasz delegat w klasę. Znajdują się w niej między innymi metody
- Invoke - wywołująca metodę do której referencję posiada obiekt delegatu. Metoda Invoke przyjmuje takie same parametry jak delegat. Metoda ta działa synchronicznie względem głównego wątku programu
- BeginInvoke - analogiczna do Invoke, działa jednek w sposób asynchroniczny
Praktyczny przykład
Jeśli zdecydowalibyśmy się napisać ten program bez użycia delegat, wyglądałby on na przykład tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | public class Mountain { public string Name { get; set; } public int Height { get; set; } } class Program { static void Main(string[] args) { List<Mountain> mountains = new List<Mountain>() { new Mountain {Name = "Rysy", Height = 2499}, new Mountain {Name = "K2", Height = 8611}, new Mountain {Name = "Kilimandżaro", Height = 5895}, new Mountain {Name = "Ben Nevis", Height = 1344}, new Mountain {Name = "Nanga Parbat", Height = 8126}, }; DisplayMountains("Małe:", mountains.Where(m=> IsSmall(m))); DisplayMountains("Średnie:", mountains.Where(m=> IsMedium(m))); DisplayMountains("Duże:", mountains.Where(m=> IsBig(m))); Console.Read(); } static void DisplayMountains(string title, IEnumerable<Mountain> mountains) { Console.WriteLine(title); foreach (var m in mountains) { Console.WriteLine($"{m.Name} ma {m.Height} metrów"); } Console.WriteLine(); } static bool IsSmall(Mountain m) { return m.Height < 3000; } static bool IsMedium(Mountain m) { return m.Height >= 3000 && m.Height < 6000; } static bool IsBig(Mountain m) { return m.Height >= 6000; } } |
Program ten działa poprawnie i wypisuje taki wynik:
Małe: Rysy ma 2499 metrów Ben Nevis ma 1344 metrów Średnie: Kilimandżaro ma 5895 metrów Duże: K2 ma 8611 metrów Nanga Parbat ma 8126 metrów
Jeśli idzie o estetykę i jakość kodu, moje wątpliwości budzą linijki 20-22, gdzie trzy razy używamy instrukcji "Where".Wyobraźmy sobie, że chcemy odwrócić działanie programu - przekształcić go tak, by pokazywał te góry, które nie spełniają filtra (niemałe, nieśrednie, nieduże). Wtedy będziemy musieli dokonać zmiany w trzech linijkach:
DisplayMountains("Niemałe:", mountains.Where(m => !IsSmall(m))); DisplayMountains("Nieśrednie:", mountains.Where(m => !IsMedium(m))); DisplayMountains("Nieduże:", mountains.Where(m => !IsBig(m)));
Spróbujmy przekształcić ten program tak, by korzystał z delegat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | public class Mountain { public string Name { get; set; } public int Height { get; set; } } class Program { public delegate bool FilterDelegate(Mountain p); static void Main(string[] args) { List<Mountain> mountains = new List<Mountain>() { new Mountain {Name = "Rysy", Height = 2499}, new Mountain {Name = "K2", Height = 8611}, new Mountain {Name = "Kilimandżaro", Height = 5895}, new Mountain {Name = "Ben Nevis", Height = 1344}, new Mountain {Name = "Nanga Parbat", Height = 8126}, }; DisplayMountains("Małe:", mountains, IsSmall); DisplayMountains("Średie:", mountains, IsMedium); DisplayMountains("Duże:", mountains, IsBig); Console.Read(); } static void DisplayMountains(string title, List<Mountain> mountains, FilterDelegate filter) { Console.WriteLine(title); foreach (var m in mountains.Where(mountain => filter(mountain))) { Console.WriteLine($"{m.Name} ma {m.Height} metrów"); } Console.WriteLine(); } static bool IsSmall(Mountain m) { return m.Height < 3000; } static bool IsMedium(Mountain m) { return m.Height >= 3000 && m.Height < 6000; } static bool IsBig(Mountain m) { return m.Height >= 6000; } } |
Widzimy, że zniknęło brzydkie powtarzanie instrukcji "Where". Gdybyśmy postanowili odwrócić działanie programu, tym razem wystarczy poprawka w jednym miejscu, a nie w trzech:
foreach (var m in mountains.Where(mountain => !filter(mountain)))
Delegaty anonimowe
Rozważmy program, który dokonuje transformacji tekstu. W programie tym istnieje klasa TextTransformer, która posiada dwie metody: ToUpperCase, która przerabia litery tekstu na wielkie, oraz ToSpacious, która wstawia spacje między poszczególnymi literami. O tym, jaka transformacja ma zostać dokonana, decyduje metoda GetTextTransformation. Możliwa jest także opcja niewybrania żadnej transformacji - tekst ma wtedy pozostać niezmieniony.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | using System; namespace CSharpPractices { public class TextTransformer { public static string ToUpperCase(string input) { return input.ToUpper(); } public static string ToSpacious(string input) { var chars = input.ToCharArray(); return string.Join(" ", chars); } } public enum TransformType { ToUpper, ToSpacious, None } class Program { public delegate string TextTransformation(string text); public static TextTransformation GetTextTransformation(TransformType transformType) { switch (transformType) { case TransformType.ToUpper: return TextTransformer.ToUpperCase; case TransformType.ToSpacious: return TextTransformer.ToSpacious; case TransformType.None: return delegate (string input) { return input; }; default: throw new ArgumentException("Invalid transform type!"); } } static void Main(string[] args) { var toUpperCaseTransform = GetTextTransformation(TransformType.ToUpper); Console.WriteLine(toUpperCaseTransform("pieseł")); var toSpaciousTransform = GetTextTransformation(TransformType.ToSpacious); Console.WriteLine(toSpaciousTransform("koteł")); var noTransform = GetTextTransformation(TransformType.None); Console.WriteLine(noTransform("wyżeł")); Console.ReadKey(); } } } |
Wynikiem działania tego programu jest oczywiście:
PIESEŁ k o t e ł wyżeł
Przyjrzyjmy się bliżej linijce 38. Kiedy wybrano typ transformacji "None" chcemy zwrócić delegat do funkcji, która nie przekształci tekstu w żaden sposób. Jednak pisanie takiej funkcji wyglądałoby raczej dziwnie i mogłoby sprawić, że czytelnik kodu podniósł by nieco brwi do góry:
public class TextTransformer { public static string NoChange(string input) { return input; }
W tym przypadku lepiej jest użyć delegaty anonimowej. Nie posiada ona nazwy ani nie zaśmieca swoją obecnością klasy TextTransformer.
case TransformType.None: return delegate (string input) { return input; };
Delegaty są także kluczowym elementem mechanizmu eventów. Więcej o eventach można poczytać tutaj.
Komentarze
Prześlij komentarz