- Delegat (.NET)
-
Dieser Artikel wurde aufgrund von inhaltlichen Mängeln auf der Qualitätssicherungsseite der Redaktion Informatik eingetragen. Dies geschieht, um die Qualität der Artikel aus dem Themengebiet Informatik auf ein akzeptables Niveau zu bringen. Hilf mit, die inhaltlichen Mängel dieses Artikels zu beseitigen und beteilige dich an der Diskussion! (+)
Begründung: Der Artikel besteht zu über 90% aus HowTo und erklärt nicht wirklich warum das nun wichtig sei. Die Quellenlage ist auch nicht wirklich gut. --WB 12:05, 21. Okt. 2010 (CEST)Als Delegat bezeichnet man in .NET ein Objekt, welches eine Referenz auf eine statische oder nicht-statische Funktion enthält. Delegaten sind mit den aus C und C++ bekannten Funktionszeigern mit einer Objektreferenz vergleichbar, bieten aber einen größeren Funktionsumfang. Delegate haben eine Signatur, ähnlich der von Funktionen und Methoden, und alle Funktionen, auf die ein Delegat zeigen soll, müssen einer im Delegat festgelegten Signatur entsprechen. Sie bilden die Grundlage des Event Handling unter .NET.
Inhaltsverzeichnis
Verwendung von Delegaten
Delegaten können für Instanz- und Klassenmethoden erstellt werden. Ein Instanzmethoden-Delegat speichert zusätzlich eine Referenz auf das Objekt, mit dem er erstellt wurde. Im Gegensatz zu Funktionszeigern in C++ kann ein einmal erstellter .NET-Delegat nicht für Methodenaufrufe auf verschiedenen Objekten verwendet werden. Dafür muss man Reflection und MethodInfo-Objekte benutzen. Wegen dieser Objektreferenz hindert ein existierender Delegat das referenzierte Objekt daran, vom Garbage Collector freigegeben zu werden.
Das folgende Codebeispiel veranschaulicht die Verwendung eines Delegaten. Nach dessen Deklaration wurden zwei Instanzen des Delegaten erstellt, mit jeweils einem anderen Ziel. Die Funktion DoSomething bekommt einen Delegaten als Parameter überreicht und kann nun die Funktion ausführen, auf welche der Delegat zeigt, um die Eingabe-Zeichenkette von einer für DoSomething unbekannten Funktion überarbeiten zu lassen. In diesem Beispiel wird DoSomething zweimal aufgerufen und gibt den Text einmal in Groß- und dann in Kleinbuchstaben aus.
Das C#-Beispiel zeigt auch die Verwendung von Delegaten mit Klassen- und Instanzmethoden.
Beispiel in Visual Basic
' Deklaration eines Delegaten "MyStringDelegate", der Referenzen auf Methoden mit Rückgabewert "string" ' und einem string-Parameter aufnehmen kann Public Delegate Function MyStringDelegate(ByVal input As String) As String Public Class MainClass Public Shared Sub Main(ByVal args As String()) ' Setzen des "Ziels" des dlgtUpper-Delegaten auf Klassenmethode "UpperFunction" Dim dlgtUpper As MyStringDelegate = AddressOf UpperFunction ' Setzen des "Ziels" des dlgtLower-Delegaten auf Instanzmethode "LowerFunction" Dim dlgtLower As MyStringDelegate = AddressOf LowerFunction Dim strHallo As String = "Hallo" DoSomething(dlgtUpper, strHallo) DoSomething(dlgtLower, strHallo) End Sub Public Shared Sub DoSomething(ByVal dlgt As MyStringDelegate, ByVal input As String) Dim output As String = dlgt(input) Console.WriteLine(output) End Sub ' Klassenmethode welche vom 1. Delegaten aufgerufen wird. Public Shared Function UpperFunction(ByVal inputStr As String) As String Return inputStr.ToUpper End Function ' Instanzmethode welche vom 2. Delegaten aufgerufen wird. Public Function LowerFunction(ByVal inputStr As String) As String Return inputStr.ToLower End Function End Class
Beispiel in Visual C#
using System; namespace Wikipedia.DelegateSample { // Deklaration eines Delegaten "MyStringDelegate", der Referenzen auf Methoden mit Rückgabewert "string" // und einem string-Parameter aufnehmen kann public delegate string MyStringDelegate(string input); public class MainClass { public static void Main(string[] args) { MainClass mc = new MainClass(); // Setzen des "Ziels" des dlgtUpper-Delegaten auf Klassenmethode "UpperFunction" MyStringDelegate dlgtUpper = new MyStringDelegate(mc.UpperFunction); // Setzen des "Ziels" des dlgtLower-Delegaten auf Instanzmethode "LowerFunction" MyStringDelegate dlgtLower = new MyStringDelegate(mc.LowerFunction); DoSomething(dlgtUpper, "Hallo Welt"); // Wird "HALLO WELT" auf der Console ausgeben. DoSomething(dlgtLower, "Hallo Welt"); // Wird "hallo welt" auf der Console ausgeben. } public static void DoSomething(MyStringDelegate dlgt, string input) { string output = dlgt(input); Console.WriteLine(output); } // Klassenmethode, die vom 1. Delegaten aufgerufen wird. public string UpperFunction(string inputStr) { return inputStr.ToUpper(); } // Instanzmethode, die vom 2. Delegaten aufgerufen wird. public string LowerFunction(string inputStr) { return inputStr.ToLower(); } } }
Delegaten und Events
Delegaten bilden die Grundlage des Event Handling unter .NET. Das delegate-Schlüsselwort veranlasst den Compiler eine Klasse zu erzeugen, die von System.MulticastDelegate abgeleitet ist. MulticastDelegate ist wiederum von System.Delegate abgeleitet und um die Fähigkeit, eine ganze Aufrufkette zu verwalten, erweitert worden. Bei einem Auslösen des MulticastDelegate werden der Reihe nach alle Methoden in der Aufrufkette ausgeführt.
Ein Event ist nun ein weiteres Sprachkonzept von .NET, das im Grunde nichts anderes bewirkt als eine Zugangsbeschränkung für den zugrundeliegenden Delegaten zu erreichen. Beispielsweise ist es unmöglich, die gesamte Aufrufkette eines Events zu überschreiben, weil auf einem Event statt dem Zuweisungsoperator nur noch die Operatoren += und -= erlaubt sind, um weitere Methoden an die Aufrufkette anzuhängen. Ein Event benötigt jedoch immer einen zugrundeliegenden Delegaten, der die vom Event übergebenen Parameter näher spezifiziert. Beispielsweise arbeiten die in System.Windows.Forms definierten Klassen zur Oberflächendarstellung intensiv mit dem .NET-Event-System, um u.a. das Neuzeichnen von Elementen zu ermöglichen oder Buttons auf einen Klick des Benutzers reagieren zu lassen.
Beispiel in Visual Basic
Public Delegate Sub MyEventHandler(ByVal sender As Object, ByVal message As String) Public Class MyClass Public Event MyEvent As MyEventHandler Public Shared Sub Main(ByVal args As String()) Dim myInstance As MyClass = New MyClass Dim dlgt1 As MyEventHandler = AddressOf MyEventSink1 Dim dlgt2 As MyEventHandler = AddressOf MyEventSink2 AddHandler myInstance.MyEvent, dlgt1 AddHandler myInstance.MyEvent, dlgt2 myInstance.FireMyEvent("Hello") RemoveHandler myInstance.MyEvent, dlgt1 myInstance.FireMyEvent("Hello Again") End Sub Public Sub FireMyEvent(ByVal message As String) RaiseEvent MyEvent(Me, message) End Sub Public Sub MyEventSink1(ByVal sender As Object, ByVal message As String) Console.WriteLine("Sink1: " + message) End Sub Public Sub MyEventSink2(ByVal sender As Object, ByVal message As String) Console.WriteLine("Sink2: " + message) End Sub End Class End Namespace
Beispiel in Visual C#
namespace Wikipedia.DelegateSample { public delegate void MyEventHandler(object sender, string message); public class MyClass { public event MyEventHandler MyEvent; public static void Main(string[] args) { MyClass myInstance = new MyClass(); MyEventHandler dlgt1 = new MyEventHandler(myInstance.MyEventSink1); MyEventHandler dlgt2 = new MyEventHandler(myInstance.MyEventSink2); // Eintragen der Delegaten in die Aufruf-Liste von MyEvent. myInstance.MyEvent += dlgt1; myInstance.MyEvent += dlgt2; // Feuern des Events. myInstance.FireMyEvent("Hello"); // Entfernen eines Delegaten von der Aufruf-Liste. myInstance.MyEvent -= dlgt1; // Erneutes Feuern des Events. myInstance.FireMyEvent("Hello Again"); } public void FireMyEvent(string message) { // Hier wird überprüft, ob ein Eintrag in der Aufruf-Liste vorhanden ist. if(this.MyEvent != null) { // Hier wird jeder Delegat, der sich für den Event registriert hat, aufgerufen. this.MyEvent(this, message); } } public void MyEventSink1(object sender, string message) { Console.WriteLine("Sink1: " + message); } public void MyEventSink2(object sender, string message) { Console.WriteLine("Sink2: " + message); } } }
Delegaten und Threading
Bei Anwendungen mit mehreren Threads finden Delegaten besondere Bedeutung, da sie verwendet werden können, um eine Aufgabe an einen anderen Thread zu delegieren. Beispielsweise ist es in der Windows.Forms-Programmierung erforderlich, so gut wie jede Interaktion mit Oberflächenelementen unter der Kontrolle desjenigen Threads ausführen zu lassen, der diese Elemente auch erzeugt hat (mithin für das GUI verantwortlich ist), weil es andernfalls zu konkurrierenden Zugriffen auf die Oberfläche und damit zu Fehlern kommen kann. Zu diesem Zweck definieren alle Oberflächenelemente eine Methode Invoke(), die einen parameterlosen Delegaten entgegennimmt und ihn im Kontext des GUI-Thread ausführt, sobald dieser mit einer eventuell noch laufenden Aktion fertig wird. Hierdurch findet eine Serialisierung der GUI-Zugriffe statt, der Invoke()-Aufruf unterbricht eine eventuell laufende Operation im GUI-Thread nicht. Das würde der notwendigen Serialisierung von GUI-Zugriffen zuwiderlaufen. Der Hintergrund-Thread, der Invoke() verwendet hat, wartet so lange, bis der übergebene Delegat vollständig ausgeführt wurde. Sofern das nicht notwendig ist, kann alternativ BeginInvoke() zum Aufruf verwendet werden und EndInvoke(), um – falls nötig – auf den Abschluss der delegierten GUI-Operation zu warten.
Gleich aussehend, aber ganz anders implementiert, sind die Methoden BeginInvoke() und EndInvoke(), die ein .NET-Kompiler automatisch für Delegaten generiert. Mittels dieser Methoden kann man die vom Delegaten referenzierte(n) Methode(n) asynchron auf einem eigenen Thread (Begin/EndInvoke()) aufrufen, der nur für die Laufzeit der Delegatenausführung bereitsteht (und aus Performancegründen aus einem Thread-Pool genommen wird).
Delegaten verbessern die Codequalität
Delegaten lassen sich auch verwenden, um Redundanz oder unhandliche, wartungsfeindliche Strukturen im Code zu vermeiden und den Code dadurch leichter lesbar und wartbarer (maintainable) zu gestalten.
Zum einen können switch-Anweisungen, die für recht unschönen Code sorgen können[1], durch ein Mapping der fallweise auszuführenden Methoden mittels Delegaten ersetzt werden. Zum anderen können Teile verschiedener Methoden, die innerhalb von in diesen Methoden jeweils gleichlautendem Code eingebettet sind, nun selbst in eigene Methoden ausgelagert werden (Methodenextraktion). Dadurch lässt sich - aus bisher vielen Methoden mit teilweise redundantem Code - eine einzige Methode erzeugen, die die jeweils benötigte Funktionalität als Delegat übergeben bekommt. Auf diese Weise lassen sich Methoden erzeugen, deren (allgemeine) Funktionalität sich durch den verwendeten Delegaten näher bestimmen sowie einfach erweitern oder ergänzen lässt, die aber dennoch nur eine einzige Implementierung benötigen.
Dies kann an einfachen Beispielen demonstriert werden (die sich auf das Wesentliche beschränken und der Übersichtlichkeit wegen keine Kommentare oder Fehlerbehandlung enthalten)
Switch-Anweisung (ohne Delegat)
... int summe = getErgebnis(operanden, 0, '+'); int produkt = getErgebnis(operanden, 1, '*'); ... private int getErgebnis(int[] operanden, int neutralesElement, char operat) { int ergebnis = neutralesElement; foreach(int operand in operanden) { switch (operat) { case '+': ergebnis += operand; break; case '*': ergebnis *= operand; break; } } return ergebnis; }
Switch-Anweisung ersetzt durch Delegat
... private Dictionary<char, Func<int, int, int>> m_Operationen = new Dictionary<char,Func<int, int, int>>(); createMapping(); ... int summe = getErgebnis(operanden, 0, '+'); int produkt = getErgebnis(operanden, 1, '*'); ... private int getErgebnis(int[] operanden, int neutralesElement, char operat) { int ergebnis = neutralesElement; Func<int, int, int> operation = null; if (m_Operationen.TryGetValue(operat, out operation)) { foreach(int operand in operanden) { ergebnis = operation(ergebnis, operand); } } return ergebnis; } private void createMapping() { m_Operationen.Add('+', addiere); m_Operationen.Add('*', multipliziere); } private int addiere(int operand1, int operand2) { return operand1 + operand2; } private int multipliziere(int operand1, int operand2) { return operand1 * operand2; }
Soll der Code um weitere Rechenarten erweitert werden, wird die switch-Anweisung selbst in diesem einfachen Beispiel schnell unhandlich und lang. Im zweiten Beispiel hingegen muss nur eine entsprechende, leicht überschaubare Funktion für jede Rechenart hinzugefügt und in createMapping aufgenommen werden, ohne dass die berechnende Funktion geändert werden muss. Außerdem kann der Code in beispielsweise 2 Klassen aufgeteilt werden, deren eine die Methode getErgebnis enthält. Obwohl diese Klasse dann nicht mehr geändert werden muss, kann dennoch über Änderungen in der zweiten Klasse die Funktionalität der ersten erweitert werden (Open-Closed-Principle).
Beispiel mit Lambda-Ausdrücken
Ein weiterer Schritt wäre die Ersetzung der Erweiterungsmethoden durch Lambda-Ausdrücke:
... private Dictionary<char, Func<int, int, int>> m_Operationen = new Dictionary<char,Func<int, int, int>>(); createMapping() ... int summe = getErgebnis(operanden, 0, '+'); int produkt = getErgebnis(operanden, 1, '*'); ... private int getErgebnis(int[] operanden, int neutralesElement, char operat) { int ergebnis = neutralesElement; Func<int, int, int> operation = null; if (m_Operationen.TryGetValue(operat, out operation)) { foreach(int operand in operanden) { ergebnis = operation(ergebnis, operand); } } return ergebnis; } private void createMapping() { m_Operationen.Add('+', (x, y) => { return x + y; }); m_Operationen.Add('*', (x, y) => { return x * y; }); }
Hier muss nur noch die Mappingmethode geeignet erweitert werden.
Auch zur Methodenextraktion drei Code-Beispiele:
Methodenextraktion: ursprünglicher Code ohne Delegat
... int summe = getSumme(operanden); int produkt = getProdukt(operanden); ... private int getSumme(int[] operanden) { int ergebnis = 0; foreach (int operand in operanden) { ergebnis += operand; } return ergebnis; } private int getProdukt(int[] operanden) { int ergebnis = 1; foreach (int operand in operanden) { ergebnis *= operand; } return ergebnis; }
Methodenextraktion durchgeführt mit Delegat
... int summe = getErgebnis(operanden, 0, addiere); int produkt = getErgebnis(operanden, 1, multipliziere); ... private int getErgebnis(int[] operanden, int neutralesElement, Func<int, int, int> operation) { int ergebnis = neutralesElement; foreach(int operand in operanden) { ergebnis = operation(ergebnis, operand); } return ergebnis; } private int addiere(int operand1, int operand2) { return operand1 + operand2; } private int multipliziere(int operand1, int operand2) { return operand1 * operand2; }
Das letzte Beispiel lässt sich nun sehr einfach um weitere Rechenarten ergänzen, ohne dass jedes Mal der gesamte Code von getSumme/getProdukt kopiert und angepasst werden muss.
Methodenextraktion mit Lambda-Ausdrücken
Noch lesbarer wird der Code, wenn man anstatt der explizit ausformulierten Erweiterungsmethoden addiere und multipliziere sogenannte Lambda-Expressions verwendet. Dies macht vor allem dann Sinn, wenn in den Erweiterungsmethoden nicht viel Code steht.
private void test() { int[] operanden = new int[] { 1, 2, 3, 4 }; int summe = getErgebnis(operanden, 0, (x, y) => { return x + y; }); int produkt = getErgebnis(operanden, 1, (x, y) => { return x * y; }); } private int getErgebnis(int[] operanden, int neutralesElement, Func<int, int, int> operation) { int ergebnis = neutralesElement; foreach (int operand in operanden) { ergebnis = operation(ergebnis, operand); } return ergebnis; }
Einzelnachweise
- ↑ Robert C. Martin, CleanCode
Siehe auch
Weblinks
Wikimedia Foundation.