3.9 Dynamisches Verhalten: Delegate und Function
»Drum prüfe, wer sich ewig bindet, ob sich nicht etwas Bess‘res findet.«
Bisher haben wir das Verhalten eines Objekts im Voraus festgelegt, indem wir Methoden in der zum Objekt gehörigen Klasse definiert haben. Dies erlaubt dem Compiler, die Konsistenz von Methodendefinition und -gebrauch zu prüfen. So komfortabel es ist, sich vom Compiler bei der Fehlersuche helfen zu lassen, manchmal ist das Korsett einfach zu eng. In solchen Situationen ist es wünschenswert, erst zur Laufzeit zu entscheiden, wie sich das Objekt verhält. Zum Beispiel wird bei der Anweisung, sich nach Amerika zu begeben, die Wahl nicht primär auf das Fahrrad fallen. Manchmal steuert auch der Zustand eines Objekts das Verhalten; zum Beispiel fällt das Auto bei Fahruntüchtigkeit des Fahrers aus.
Eine Möglichkeit, das Verhalten erst während der Laufzeit festzulegen, besteht in der Verwendung geeigneter Kontrollstrukturen (If, Select, …). Diese Vorgehensweise hat entscheidende Nachteile. Wenn eine Verhaltensweise dazukommt, muss der Quelltext angepasst werden. Außerdem wird durch die vielen Anweisungen der Code recht »sperrig«. Daher bietet Visual Basic die Möglichkeit, Variablen zu definieren, in denen die zu verwendende Funktionalität gespeichert ist. Dadurch kann mit einer einfachen Variablenzuweisung das Verhalten geändert werden. Visual Basic bietet zwei Möglichkeiten:
- Funktionszeiger: Referenz auf eine Methode
- Funktionsobjekt: Anweisungen werden in der Variablen gespeichert.
3.9.1 Funktionszeiger: Delegates
Wenden wir uns nun den Funktionszeigern zu. Da sie nur eine Referenz darstellen und die eigentliche Aufgabe an die Funktion weiterreichen, auf die sie zeigen, werden sie auch Delegates genannt. Ihr Einsatz besteht aus vier Teilen:
- 1 Deklaration des Typs der zu verwendenden Funktion
- 2 Deklaration der Variablen, über die die Funktion angesprochen wird
- 3 Wertbelegung der Variablen, d. h. Verbindung zur konkreten Funktion
- 4 Aufruf der Funktion über die Variable
Der erste Punkt ist einer Methodendeklaration sehr ähnlich. In der Syntax macht das Wort Delegate aus einer Methodendeklaration einen Funktionszeiger. Der Name in der Deklaration ist nur formell ein Methodenname und vereinbart in Wirklichkeit einen neuen Datentyp, der den Typ der zu verwendenden Funktion vollständig beschreibt. Analog zu Methoden kann mit Sub eine Prozedur ohne Rückgabewert und mit Function eine Funktion mit Rückgabewert vereinbart werden. Optionale Teile sind in eckige Klammern gesetzt, kursive Teile müssen Sie Ihren Bedürfnissen anpassen.
[<Sichtbarkeit>] Delegate Sub Name([<Parameter>]) 1
[<Sichtbarkeit>] Delegate Function Name([<Parameter>]) As Typ |
Hinweis |
Da ein Delegate implizit einen neuen Datentyp vereinbart, kann die Deklaration auch außerhalb einer Klasse stehen, direkt innerhalb eines Namensraums. |
Tabelle 3.13 zeigt die erlaubten Modifikatoren.
Art | Beschreibung |
Sichtbarkeit |
Grad der Öffentlichkeit (siehe Abschnitt 3.2, »Kapselung«) |
Redefinition |
Ersatzes mit Shadows (siehe Abschnitt 3.13, »Vererbung«) |
Hinweis |
Optionale Parameter (Optional oder ParamArray) sind nicht erlaubt, und sowohl die Parametertypen als auch ein eventueller Rückgabetyp müssen mindestens den gleichen Grad der Öffentlichkeit haben wie der Funktionszeiger selbst. |
Der zweite Punkt unterscheidet sich nicht von einer normalen Variablendeklaration. Als Typ wird Name aus der Deklaration des Delegates verwendet. Die Modifikatoren für lokale Variablen sind in Abschnitt 2.5.3, »Variablendeklaration«, beschrieben, die Modifikatoren für Variablen auf Klassenebene in Abschnitt 3.6.1, »Deklaration und Initialisierung«.
[<Modifikatoren>] var As Name 2 |
Als dritter Punkt muss der Variablen vor ihrer Verwendung noch ein Wert zugewiesen werden. Dieser besteht aus der Adresse der Funktion, die von der Variablen repräsentiert wird, oder aus einem expliziten Funktionsobjekt. Auf Funktionsobjekte gehen wir in Abschnitt 3.9.5, »Funktionsobjekte: Function (λ-Ausdrücke)«, ein. Die Modifikatoren der Methode haben keinerlei Einfluss auf die spätere Verwendung des Funktionszeigers. Zum Beispiel kann die Methode durch den Modifikator Private nur in der Klasse nutzbar sein und dennoch durch einen öffentlichen Funktionszeiger von außen (indirekt) benutzbar sein. Zur Zuweisung stehen die vier folgenden Syntaxvarianten zur Verfügung (optionale Teile stehen in eckigen Klammern und kursive Teile müssen Sie Ihren Bedürfnissen anpassen):
var = New Name(AddressOf methode) 3 var = New Name(Function([<Parameter>]) Anweisung) var = AddressOf methode var = Function([<Parameter>]) Anweisung |
Hinweis |
Oft ist eine Qualifizierung der Methode durch ein Objekt oder eine Klasse notwendig (objekt.methode oder klasse.methode). |
Schließlich wird die Variable verwendet, um den eigentlichen Aufruf durchzuführen. Auch hier gibt es mehrere Varianten (die kursiven Teile müssen Sie Ihren Bedürfnissen anpassen). Die zweite ist die Langform der ersten. Auf die dritte gehen wir in Abschnitt 3.9.4, »Asynchrone Aufrufe«, ein.
var([<Parameter>]) 4 var.Invoke([<Parameter>]) var.BeginInvoke([<Parameter>]) |
Prinzip
Schauen wir uns zuerst ein Beispiel an, das alle vier Teile des Einsatzes eines Funktionszeigers enthält. Der direkte Aufruf der Funktion wäre sicherlich einfacher; es geht hier nur um die Demonstration der Syntax.
'...\Klassendesign\Funktionen\Delegate.vb |
Option Explicit On Namespace Klassendesign Module Einfach Delegate Function Aktion(ByVal x As Short) As Short '1 Function Quadrat(ByVal x As Short) As Short Return x * x End Function Sub Test() Dim akt As Aktion '2 akt = AddressOf Quadrat '3 Console.WriteLine("{0}^2 = {1}", 4, akt(4)) '4 Console.ReadLine() End Sub End Module End Namespace
Die vier Teile sind gekennzeichnet. Erstens wird mit Delegate ein Funktionszeiger Aktion deklariert. Als Zweites wird die Variable akt vom Typ Aktion deklariert. Im dritten Schritt wird die Variable mit der Funktion Quadrat verbunden. Schließlich erfolgt im vierten Schritt der Aufruf von Quadrat(4) mittels akt(4). Die Ausgabe bestätigt den korrekten Ablauf des Programms:
4^2 = 16
Listen
Das vorige Beispiel hat noch nicht demonstriert, worin der Vorteil des Einsatzes von Funktionszeigern besteht. Demgegenüber nutzt das folgende Codefragment einen Funktionszeiger als Parameter der Funktion Scan, um den Code, der zum Durchlaufen der Liste nötig ist, nur einmal schreiben zu müssen. Denn egal, welche Funktion auf die Elemente der Liste angewendet wird, die Write-Methoden zum Ausdruck und ein Schleifenkonstrukt zur Erfassung aller Listenelemente werden immer gebraucht. An die Stelle der expliziten Wertzuweisung der Variablen tritt die Parameterübergabe an Scan, die die Zuweisung implizit vornimmt. Der Vollständigkeit halber sind alle vier Varianten der Belegung gezeigt; auf Function gehen wir in Kürze ein (siehe Abschnitt 3.9.5, »Funktionsobjekte: Function (λ-Ausdrücke)«).
‘ ...\Klassendesign\Funktionen\DelegateListen.vb |
Option Explicit On Namespace Klassendesign Module Listen Delegate Function Aktion(ByVal x As Double) As Double Sub Scan(ByVal name As String, ByVal fun As Aktion, ByVal x() As Single) Console.Write(name & " ") For Each v As Single In x Console.Write("{0,5}", fun(v).ToString("#0.0")) Next Console.WriteLine() End Sub Sub Test() Dim v(4) As Single For no As Int32 = 0 To v.Length-1 : v(no)=no+1 : Next Scan("Sinus ", New Aktion(AddressOf Math.Sin), v) Scan("Identität", New Aktion(Function(x) x), v) Scan("Wurzel ", AddressOf Math.Sqrt, v) Scan("Quadrat ", Function(x) x ^ 2, v) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe zeigt, dass alle Funktionen korrekt angesprochen wurden:
Sinus 0.8 0.9 0.1 –0.8 –1.0 Identität 1.0 2.0 3.0 4.0 5.0 Wurzel 1.0 1.4 1.7 2.0 2.2 Quadrat 1.0 4.0 9.0 16.0 25.0
Damit dies möglich ist, werden die Single-Elemente der Variablen v implizit in einen Double umgewandelt, also den Typ, den das Delegate laut Deklaration erwartet.
3.9.2 Automatisch generierter Code
Durch die Deklaration eines Delegates wird vom Compiler implizit eine neue Klasse gleichen Namens definiert, die auf der Klasse MultiCastDelegate basiert. Das folgende Listing enthält nur die für Ihren Code zugänglichen Klassenmitglieder, ohne Modifikatoren anzugeben. Um das Listing weiter zu kürzen, sind einige Methodensignaturen mit optionalen Parametern (auch solchen, die nicht am Ende stehen) und Alternativen mit einem vertikalen Strich | formuliert. Redundante Signaturen sind auch weggelassen worden (sie existieren zur Beschleunigung). Der Wert params steht für die Parameter, die Sie dem Delegate im Quelltext gegeben haben. Alle nicht gekennzeichneten Parameter werden als Wert übergeben (ByVal).
Class klasse Class delegat Function BeginInvoke(<params (alle ByVal)>, _ rückruf As AsyncCallback, status As Object) _ As IAsyncResult Function EndInvoke(<nur params mit ByRef>, _ DelegateAsyncResult As IAsyncResult) As Double Function Invoke(<params>) As Double 'Class MulticastDelegate Sub GetObjectData(info As SerializationInfo, cont As StreamingContext) Function Equals(obj As Object) As Boolean Function GetInvocationList() As Delegate() Operator =|<>(d1 As MulticastDelegate, d2 As MulticastDelegate) _ As Boolean Function GetHashCode() As Integer 'Class Delegate Function Clone() As Object Function DynamicInvoke(ParamArray argumente As Object()) As Object Shared Function Combine(ParamArray delegaten As Delegate()) As Delegate Shared Function Remove|RemoveAll(quelle As Delegate, wert As Delegate) _ As Delegate Shared Function CreateDelegate( _ typ As Type, ziel As Object|Type, methode As String, _ Optional grossSchreibungIgnorieren As Boolean, _ Optional throwBindungsfehler As Boolean) As Delegate Shared Function CreateDelegate(typ As Type, _ Optional erstesArg As Object, methode As MethodInfo, _ Optional throwBindungsfehler As Boolean) As Delegate Operator =|<>(d1 As Delegate, d2 As Delegate) As Boolean ReadOnly Property Method() As MethodInfo ReadOnly Property Target() As Object 'Class Object End Class End Class
3.9.3 Mehrere Aktionen gleichzeitig
Es gibt Situationen, in denen sich eine Aktion aus mehreren Teilen zusammensetzt, die zusammen ausgeführt werden müssen. Zum Beispiel muss bei einer Kontobuchung gewährleistet sein, dass die Summe aller Buchungen null ist, da sonst entweder Geld aus dem Nichts geschaffen würde oder Geld verschwinden würde. Das folgende Codefragment zeigt eine Überweisung von einem Konto auf ein anderes. Damit alle Kontobewegungen gemeinsam erfolgen, werden sie in einem einzelnen Delegate durch die Methode Combine in der Variablen um gebündelt. Innerhalb von Buchung() wird durch die Referenzübergabe der summe eine Anpassung der Überweisungsrichtung möglich.
'...\Klassendesign\Funktionen\MehrfachAktionen.vb |
Option Explicit On Namespace Klassendesign Class Konto Shared nr As Integer Private no As Integer Private euro As Double Sub New(ByVal stand As Double) nr += 1 : no = nr : euro = stand End Sub Sub Buchen(ByRef summe As Double) euro += summe : summe *= –1 'ab jetzt abziehen Console.Write("Konto {0}: {1} Euro ", no, euro) End Sub End Class Module Bank Delegate Sub Buchung(ByRef summe As Double) Sub Test() Dim k1 As Konto = New Konto(1000) Dim k2 As Konto = New Konto(1000) Dim b1 As Buchung = AddressOf k1.Buchen Dim b2 As Buchung = AddressOf k2.Buchen Dim um As Buchung = CType([Delegate].Combine(b1, b2), Buchung) um(300) : Console.WriteLine() 'alles inklusive um(-100) : Console.WriteLine() 'alles inklusive Console.ReadLine() End Sub End Module End Namespace
Hinweis |
Combine ignoriert Nullreferenzen (Nothing). |
Die Ausgabe zeigt, wie sich die Buchungen jeweils auf beide Konten auswirken:
Konto 1: 1300 Euro Konto 2: 700 Euro Konto 1: 1200 Euro Konto 2: 800 Euro
Hinweis |
Durch die Bündelung von Aktionen kann kein Teil einer zusammengesetzten Aktion vergessen werden. Die Reihenfolge der Aktionen bleibt auch in der Kombination gewahrt. |
3.9.4 Asynchrone Aufrufe
Normalerweise werden die Anweisungen in einer Methode in der Reihenfolge ausgeführt, die der Quelltext vorgibt. Dies ist nicht immer erwünscht. Es gibt Situationen, in denen Teile eines Programms unabhängig agieren sollen. Ein solches Beispiel ist die Interaktion zweier Programmteile, deren Programmflüsse unabhängig voneinander sind. Bitte beachten Sie, dass nur die Abläufe der Teile nichts miteinander zu tun haben. Es ist ohne Weiteres möglich, aber nicht zwingend, dass die Teile mit denselben Daten arbeiten und dadurch miteinander kommunizieren. Denn ändert ein Teil eine gemeinsam genutzte Variable, ist dies im anderen Teil natürlich sichtbar (er benutzt identisch dieselbe Variable). Der logische »Kleber« der Teile ist also der Satz an Daten, auf die beide Teile Zugriff haben.
Ein Funktionszeiger bietet die Möglichkeit, die mit ihm verbundene Funktion so aufzurufen, dass sie unabhängig von der Methode ist, von der sie aufgerufen wurde. Die aufgerufene Methode ist damit von den Anweisungen der aufrufenden Methode entkoppelt. Solche unabhängigen Programmfäden werden im Englischen Threads genannt. Die Unabhängigkeit der Threads wird durch das (Betriebs)System gewährleistet. Für die Programmlogik spielt es keine Rolle, ob die verschiedenen Teile auf physisch unabhängiger Hardware (Prozessorkernen) laufen oder ob das System die Teile logisch dadurch entkoppelt, dass ihnen abwechselnd kleine Zeiteinheiten auf einem einzelnen Prozessor zur Verfügung gestellt werden.
Hinweis |
In diesem Abschnitt erfolgt nur ein kurzer Einstieg in die Thematik, der mit Delegates zusammenhängt. Das Kapitel 5, »Multithreading«, befasst sich ausführlich mit dem Themenkreis. |
Im folgenden Codefragment wird in der Methode Los() so lange hochgezählt und die Zahlen ausgegeben, bis weiter von außen auf False gesetzt wird. Damit diese Einflussnahme von außen möglich ist, muss dieses »Außen« dadurch geschaffen werden, dass die Methode Los() unabhängig vom anderen Programmfluss läuft. Dies wird durch den Start über BeginInvoke() erreicht.
Die Reihenfolge des Programmflusses ist durch Zahlen kenntlich gemacht, die beiden unabhängigen Programmteile (Threads) durch die Buchstaben A und B. Die logische Reihenfolge des Programmablaufs lautet:
1A Zuerst wird die Delegate-Variable zähl mit der Methode Los() verbunden. | |
2A Diese wird in Test() durch BeginInvoke() unabhängig von der Anweisungsreihenfolge in Test() gestartet. | Die ersten beiden Parameter werden an Los() als Werte übergeben, als seien alle Parameter mit ByVal deklariert worden. Die Berücksichtigung von ByRef erfolgt im Schritt 3B. Der vorletzte Parameter ist ein Funktionszeiger auf die nach Beendigung von Los() aufzurufende Methode. Der letzte Parameter nimmt Zusatzinformationen für eben jene Routine auf. |
3A Dann wird ReadLine() aufgerufen, und die Anweisungsfolge in Test() blockiert, bis ein Zeilenvorschub eingegeben wird. | |
1B In der Zwischenzeit wird die While-Schleife in Los() ausgeführt. | Der Sleep-Befehl sorgt dafür, dass die Zahlen in einer Geschwindigkeit erscheinen, in der Menschen sie lesen können. |
4A Nach Eingabe des Zeilenvorschubs wird in Test() die Variable weiter gesetzt, und mit einem erneuten ReadLine() wird der weitere Fortschritt blockiert. | |
2B Im parallel laufenden Los() wird daraufhin die While-Schleife verlassen und die Methode beendet. | |
3B Danach wird automatisch die in BeginInvoke() als vorletzter Parameter spezifizierte Funktion Ende() aufgerufen. | In dieser wird mit EndInvoke() das Ergebnis abgeholt. Außerdem werden alle ByRef-Parameter in der Reihenfolge an EndInvoke() übergeben, die im Delegate deklariert ist. EndInvoke() belegt sie mit den Werten, die bei Beendigung von Los() gültig waren. Schließlich wird mit AsyncState auf den letzten Parameter von BeginInvoke() zugegriffen. |
5A Zuletzt beendet ein Zeilenvorschub Test() und damit das ganze Programm. | |
Die Formulierung ohne unabhängige Teile ist hier nicht möglich, da jeder Versuch, mit ReadLine() eine Benutzereingabe zu lesen, den Programmablauf blockiert.
'...\Klassendesign\Funktionen\Asynchron.vb |
Option Explicit On Namespace Klassendesign Module Auswahl Function Los(ByVal delta As Int32, ByRef anz As Int32) As Int32 '1B Dim no As Integer While weiter anz += 1 : no += delta : Console.Write("{0} ", no) Threading.Thread.Sleep(500) '=> Zeit zum Lesen End While Return no '2B End Function Delegate Function ZählFun( _ ByVal delta As Int32, ByRef anläufe As Int32) As Int32 Public zähl As ZählFun = AddressOf Los '1A Public weiter As Boolean = True 'gemeinsam (!) genutzt Sub Test() Dim res As IAsyncResult = _ zähl.BeginInvoke(2, 0, AddressOf Ende, "Test") '2A Console.ReadLine() 'blockieren 3A weiter = False '=> Ende von While in Los 4A Console.ReadLine() '5A End Sub Sub Ende(ByVal ar As IAsyncResult) '3B Dim versuche As Int32 Dim z As Int32 = zähl.EndInvoke(versuche, ar) Console.WriteLine("{0} nach {1} Zahlen", z, versuche) Console.WriteLine("Start in {0}", ar.AsyncState) End Sub End Module End Namespace
Eine typische Ausgabe zeigt einige durch Los() ausgegebene Zahlen. Die beiden letzten Zeilen werden in Ende() erzeugt.
2 4 6 8 10 12 14 16 18 20 20 nach 10 Zahlen Start in Test
Hinweis |
Fehler in unabhängig laufenden Threads zu finden, kann recht schwierig sein. Lassen Sie sich davon nicht entmutigen. Das Konzept bietet viele Möglichkeiten, unter anderem bezüglich der Beschleunigung eines Programms durch die Ausnutzung mehrerer Prozessorkerne gleichzeitig. Praktisch jede grafische Benutzeroberfläche arbeitet mit verschiedenen Threads. |
3.9.5 Funktionsobjekte: Function (λ-Ausdrücke)
Visual Basic hat zwei Arten von Routinen, die etwas zurückgeben. Auf Modulebene werden sie Funktionen genannt und mit Function deklariert (siehe Abschnitt 3.3.5, »Rückgabewert«), als Wert einer Variablen nennt man sie λ-Ausdruck (Funktionsobjekt).
Dieser in einer Variablen gespeicherte λ-Ausdruck führt die funktionale Programmierung in .NET ein. Die Erklärung, was dies genau bedeutet, würde ein ganzes Buch füllen (für einen ersten Einstieg siehe http://de.wikipedia.org/wiki/Funktionale_Programmierung). In diesem Buch wird der praktische Nutzwert solcher Variablen exemplarisch gezeigt.
Die Syntax eines Function-Ausdrucks ist gegeben durch (optionale Teile stehen in eckigen Klammern und kursive Teile müssen Sie Ihren Bedürfnissen anpassen):
Function([<Parameter>]) <Ausdruck> |
Der Ausdruck muss einen Wert zurückliefern. Ein Aufruf einer mit Sub deklarierten Prozedur, zum Beispiel von WriteLine, ist nicht erlaubt. Außerdem ist nur ein einzelner Ausdruck erlaubt. Der kann aber auch eine Funktion aufrufen.
Im folgenden Codefragment wird der Variablen sec ein Funktionsobjekt zugewiesen und in WriteLine als Funktion verwendet:
'...\Klassendesign\Funktionen\Lambda.vb |
Option Strict On
Namespace Klassendesign
Module Lambda
Sub Parameterlos()
Dim sec = Function() Now.Second
Console.WriteLine("Minutenanteil: {0}", sec() / 60)
Console.ReadLine()
End Sub
...
End Module
End Namespace
Im Gegensatz zu bisherigen Variablendeklarationen fällt auf, dass die As-Klausel fehlt. Dies liegt daran, dass der Compiler den Typ aus dem Funktionsobjekt automatisch ermitteln kann. Er tut dies, da standardmäßig Option Infer On gesetzt ist. In wenigen Fällen scheitert der Automatismus, und der Typ muss explizit angegeben werden. Bei den Delegates haben Sie bereits gesehen, wie ein solcher Typ angegeben wird. Beim folgenden Codefragment scheitert der Compiler bei der automatischen Typermittlung, und Sie müssen den Typ explizit spezifizieren.
Delegate Function IIFun(ByVal x As Int32) As Int32 Dim fak As IIFun = Function(x As Int32) If(x > 1, x * fak(x – 1), 1)
Das folgende Codefragment zeigt, dass die Parametertypen wie gewohnt mit einer As-Klausel deklariert werden. Die Ausgaben der WriteLine-Befehle sind der Zeile als Kommentar angehängt.
'...\Klassendesign\Funktionen\Lambda.vb |
Option Strict On
Namespace Klassendesign
Module Lambda
...
Sub Parameter()
Dim mult = Function(z1 As Double, z2 As Double) z1*z2
Console.WriteLine("4*3: {0}", mult(4, 3)) '4*3: 12
Console.WriteLine("5*7: {0}", mult(5, 7)) '5*7: 35
Console.ReadLine()
End Sub
End Module
End Namespace
Hinweis |
Optionale Parameter (Optional und ParamArray) sind nicht erlaubt, aber Referenzen jeder Art. |
Durch ByRef gekennzeichnete Parameter sind auch erlaubt. Sie sind nur dann nötig, wenn im Ausdruck des Funktionsobjekts eine Methode mit diesem Parameter aufgerufen wird, denn eine direkte Zuweisung eines Wertes im Funktionsobjekt ist nicht möglich, da eine Zuweisung keinen Wert an sich hat, sondern »nur« den Seiteneffekt einer Variablenänderung.
Funktionsobjekt als Parameter
Wie jeder andere Datentyp kann auch ein Funktionsobjekt als Parametertyp einer anderen Funktion dienen. Der Typ eines Funktionsobjekts wird in einer Delegate-Deklaration festgelegt. Im folgenden Codefragment werden auf dieselbe Zahl verschiedene Arten der Rundung angewandt. Die entsprechenden Funktionsobjekte a1 und a2 werden an eine allgemeine Rundungsfunktion etwa() als Parameter übergeben.
'...\Klassendesign\Funktionen\Parameter.vb |
Option Strict On Namespace Klassendesign Module Parameter Delegate Function EtwaFun(ByVal v As Double) As Double Sub Funktionsobjekt() Dim etwa = Function(f As EtwaFun, x As Double) f(x) Dim a1 As EtwaFun = AddressOf Math.Floor Dim a2 As EtwaFun = AddressOf Math.Round Console.WriteLine("Floor(7.5): {0}", etwa(a1, 7.5)) '7 Console.WriteLine("Round(7.5): {0}", etwa(a2, 7.5)) '8 Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe bestätigt die korrekte Arbeitsweise:
Floor(7.5): 7 Round(7.5): 8
Funktionsobjekt als Rückgabewert
Ein Funktionsobjekt stellt implizit einen Datentyp dar, der auch als Rückgabetyp einer Funktion dienen kann. Damit dieser Typ benannt werden kann, muss ein Delegate deklariert worden sein, das in den Parametertypen und dem Typ des Rückgabewerts mit der Funktionsobjektvariablen übereinstimmt. Das folgende Codefragment verwendet ein Delegate mit einer ganzen Zahl als Parameter und einer Fließkommazahl als Rückgabewert. Die Methode Anteil() gibt ein Funktionsobjekt mit passender Signatur zurück, das in Typ() der Variablen ant zugewiesen wird. Von nun an kann ant wie die Funktion AntFun benutzt werden.
'...\Klassendesign\Funktionen\Rueckgabewert.vb |
Option Strict On Namespace Klassendesign Module Rückgabewert Delegate Function AntFun(ByVal secs As Int32) As Double Function Anteil() As AntFun Return Function(secs As Int32) Now.Second / secs End Function Sub Typ() Dim ant As AntFun = Anteil() Console.WriteLine("Minutenanteil: {0}", ant(60)) Console.WriteLine("Stundenanteil: {0}", ant(3600)) Console.ReadLine() End Sub End Module End Namespace
Sichtbarkeit und Lebensdauer von Werten
Ein Funktionsobjekt hat Zugriff auf all die Variablen, die während seiner Definition erreichbar sind. Im folgenden Codefragment wird innerhalb des Funktionsobjekts fun sowohl auf eine Modulvariable a als auch auf eine lokale Variable b zugegriffen. Die Ausgabe ist der Zeile als Kommentar angehängt.
'...\Klassendesign\Funktionen\Sichtbarkeit.vb |
Option Strict On Namespace Klassendesign Module Sichtbarkeit Dim a As Integer = 10 Sub Test() Dim b As Integer = 100 Dim fun = Function() a + b Console.WriteLine("a+b: {0}", fun()) '110 Console.ReadLine() End Sub End Module End Namespace
Was passiert, wenn zwischen der Definition eines Funktionsobjekts und seiner Verwendung Variablen geändert werden, auf die es zugreift? Zwei Fälle des Zugriffs sind dabei zu unterscheiden:
- Der Zugriff erfolgt noch im selben Bereich.
- Der Zugriff erfolgt in einem neuen Bereich.
Der erste Fall liegt vor, wenn das Funktionsobjekt in der gleichen Funktion benutzt wird, in der es definiert wurde. Dann verhält sich das Funktionsobjekt wie jede andere Funktion und nimmt die gerade gültigen Werte. Im zweiten Fall wird der Wert genommen, der gültig war, als die Funktion beendet wurde, die das Funktionsobjekt definiert hat. Ob eine Variable mit Dim oder Static deklariert wird, hat auf diesen Mechanismus keinen Einfluss. Das folgende Codefragment zeigt beide Fälle:
'...\Klassendesign\Funktionen\Abschluss.vb |
Option Strict On Namespace Klassendesign Module Abschluss Delegate Function IntFun() As Integer Sub Lebensdauer() Console.WriteLine(„Definition Funktion“) Dim lesen = Funktion() Console.WriteLine("Lesen: {0}", lesen()) '3 Console.WriteLine("Lesen: {0}", lesen()) '3 Console.WriteLine("Definition Funktion") lesen = Funktion() Console.WriteLine("Lesen: {0}", lesen()) '6 Console.ReadLine() End Sub Function Funktion() As IntFun Static loc As Integer Dim var = Function() loc loc = loc + 1 Console.WriteLine("Lesen Definition: {0}", var()) '1,4 loc = loc + 2 Return var End Function End Module End Namespace
Die Ausgabe von Lebensdauer() lautet:
Definition Funktion Lesen Definition: 1 Lesen: 3 Lesen: 3 Definition Funktion Lesen Definition: 4 Lesen: 6
In der Prozedur Lebensdauer() wird durch den Aufruf Funktion() das erste Funktionsobjekt definiert. In Funktion() startet die lokale Variable loc mit dem Wert 0. Danach wird sie um 1 erhöht und wird das erste Funktionsobjekt definiert. Es greift nun auf den aktuellen Wert 1 zu. Danach wird loc um 2 erhöht und die Funktion verlassen. In der Prozedur Lebensdauer() wird das Funktionsobjekt nun zweimal verwendet, jedes Mal greift es auf den Wert zu, den loc hatte als Funktion() verlassen wurde, hier 3. Der nächste Aufruf von Funktion() erhöht loc um 1 und definiert das zweite Funktionsobjekt. Das verwendet beim Aufruf innerhalb von Funktion() den gerade aktuellen loc-Wert 4. Danach wird loc um 2 erhöht und Funktion() verlassen. In der Prozedur Lebensdauer() greift das zweite geschaffene Funktionsobjekt auf den Wert von loc zu, den die Variable beim letzten Verlassen von Funktion() hatte, nämlich 6.
Hinweis |
Das Zur-Verfügung-Stellen des richtigen (Variablen-)Kontexts für den Funktionsaufruf wird Funktionsabschluss (engl. Closure) genannt. |
3.9.6 Umwandlungen
Erwartet der Delegate-Typ eine Prozedur, kann als konkreter Wert der Delegate-Variablen auch eine Funktion verwendet werden. Der Rückgabewert wird dann schlicht ignoriert. Umgekehrt ist dies nicht möglich. Ein Delegate-Typ mit Rückgabewert kann auch nur mit einer Funktion mit Rückgabewert verwendet werden. Dabei muss der Wert der konkreten Funktion dem des Delegates entsprechen oder sich implizit in diesen umwandeln lassen.
Das Konzept greift auch bezüglich der Parameter. Jeder Typ eines Parameters der der Delegate-Variablen zugewiesenen konkreten Funktion oder Prozedur muss mit dem Typ des korrespondierenden Parameters im Delegate übereinstimmen oder sich in diesen implizit umwandeln lassen. Das folgende Codefragment zeigt, wie durch implizite Umwandlung ein Integer-Parameter anstatt eines Short-Parameters im Delegate verwendet werden kann. Analog wird der Integer-Rückgabewert implizit in den Double-Rückgabewert des Delegates umgewandelt.
Delegate Function Oper(ByVal v As Short, ByVal v As Short) As Double Dim op As Oper = Function(v1 As Integer, v2 As Short) v1 \ v2
Bei Funktionsobjekten gibt es eine »unglückliche« Stolperfalle. Wird ein Delegate mit Parametern vereinbart, akzeptiert eine Delegate-Variable dieses Typs ein Funktionsobjekt ohne Parameter. Funktionsobjekte mit mindestens einem Parameter und nicht passender Anzahl oder falschen Typen werden zurückgewiesen. Der Aufruf muss mit korrekter Parameterzahl und Parametertypen erfolgen. Da das Funktionsobjekt keine Parameter hat, werden die übergebenen Parameter schlicht ignoriert. Das folgende Codefragment zeigt eine solche verwirrende Situation:
'...\Klassendesign\Funktionen\DelegateParameter.vb |
Option Strict On Namespace Klassendesign Module DelegateParameter Delegate Function Oper(ByVal v As Short,ByVal v As Short) As Double Sub Test() Dim d As Oper = Function() –1 Console.WriteLine("15/3 {0}", d(15, 3)) d = Function(v1 As Double, v2 As Double) v1 / v2 Console.WriteLine("15/3 {0}", d(15, 3)) Console.ReadLine() End Sub End Module End Namespace
Die Ausgabe komplettiert die Verwirrung:
15/3 –1 15/3 5
3.9.7 Grafikbibliothek: Vergleich von Rechtecken
In diesem Abschnitt erweitern wir die in Abschnitt 3.1.12, »Grafikbibliothek: Beispiel für Kapitel 3 und 4«, eingeführte und zuletzt in Abschnitt 3.8.3, »Grafikbibliothek: Position des Rechtecks«, erweiterte Grafikanwendung. Zuerst werden zwei Delegates (Funktionszeiger) hinzugefügt. Der erste dient zum Vergleichen von Rechtecken, der zweite zur Ermittlung einer Maßzahl zum Vergleich. Die allgemeine Methode Vgl() führt mittels der Funktion vom Typ Dimension den Vergleich aus und wird in VglFläche() und VglUmfang() mit passenden Funktionsobjekten aufgerufen. In der Testmethode Vergleichen() werden schließlich die übergebenen Rechtecke mit einigen Funktionen in einer For Each-Schleife verglichen.
'...\Klassendesign\Graphik\Delegate.vb |
Option Explicit On Namespace Klassendesign Partial Public Class Rechteck Delegate Sub Vergleich(ByVal rechteck As Rechteck) Delegate Function Dimension(ByVal rechteck As Rechteck) As Double Sub Vgl(ByVal r As Rechteck, ByVal f As Dimension, ByVal art As String) Console.WriteLine("{0}x{1} hat {2} {3} als {4}x{5}", r.a, r.b, _ If(f(r) >= f(Me), "mehr", "weniger"), art, a, b) End Sub Sub VglFläche(ByVal recht As Rechteck) Vgl(recht, Function(r As Rechteck) r.a * r.b, "Fläche") End Sub Sub VglUmfang(ByVal recht As Rechteck) Vgl(recht, Function(r As Rechteck) r.a + r.b, "Umfang") End Sub Sub Vergleichen(ByVal ParamArray rechtecke() As Rechteck) Dim vgl() As Vergleich = {AddressOf VglFläche, AddressOf VglUmfang} For Each v As Vergleich In vgl For Each r As Rechteck In rechtecke : v(r) Next r, v End Sub End Class End Namespace
Zum Test werden einige Rechtecke erzeugt und mit einem weiteren Rechteck verglichen.
'...\Klassendesign\Zeichner\Delegate.vb |
Option Explicit On Namespace Klassendesign Partial Class Zeichner Sub Delegates() Dim rechtecke() As Rechteck = _ {New Rechteck(6, 7), New Rechteck(12, 4), New Rechteck(7, 8)} Dim rechteck As New Rechteck(7, 7) rechteck.Vergleichen(rechtecke) End Sub End Class End Namespace
Zur Kontrolle sehen Sie hier die Ausgabe der Methode Delegates():
6x7 hat weniger Fläche als 7x7 12x4 hat weniger Fläche als 7x7 7x8 hat mehr Fläche als 7x7 6x7 hat weniger Umfang als 7x7 12x4 hat mehr Umfang als 7x7 7x8 hat mehr Umfang als 7x7
Die nächste Erweiterung erfolgt in Abschnitt 3.10.6, »Grafikbibliothek: Größenänderungen von Rechtecken überwachen«.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.