MSDN Magazin > Home > Ausgaben > 2007 > September >  Basic Instincts: Lambda-Ausdrücke
Basic Instincts
Lambda-Ausdrücke
Timothy Ng

Lambda-Ausdrücke, die in Visual Basic® 2008 erstmals verfügbar sind, stellen eine praktische Ergänzung für die Arbeit jedes Programmierers dar. Dabei handelt es sich um aufrufbare Entitäten, die innerhalb einer Funktion definiert und als wesentliche Bestandteile behandelt werden. Sie können einen Lambda-Ausdruck aus einer Funktion zurückgeben und Lambda-Ausdrücke an andere Funktionen übergeben. Lambda-Ausdrücke wurden in Visual Basic 2008 (früher unter dem Codenamen „Orcas“ bekannt) zur Unterstützung von LINQ (Language Integrated Query) hinzugefügt, wodurch Visual Basic um Datenprogrammierbarkeit erweitert wird (hierauf wird an späterer Stelle ausführlicher eingegangen). Wenn Sie Lambda-Ausdrücke verwenden, können Sie sich von der von ihnen gebotenen Leistung und Flexibilität überzeugen. Ich lade Sie hiermit ein, die grundlegenden Konzepte von Lambda-Ausdrücken und ihre Vorteile kennenzulernen und zu erfahren, wie Sie mit ihnen ausdrucksvollere Programme schreiben können.

Was sind Lambda-Ausdrücke?
Im Folgenden finden Sie ein Beispiel für die Definition eines einfachen Lambda-Ausdrucks. Dabei wird doubleIt als Lambda-Ausdruck definiert, der eine ganze Zahl annimmt und eine ganze Zahl zurückgibt. Der Lambda-Ausdruck multipliziert im Grunde die Eingabe mit 2 und gibt dann das Ergebnis zurück.
Dim doubleIt As Func(Of Integer, Integer) = _
    Function(x As Integer) x * 2
Der Func-Typ wird ebenfalls mit Visual Basic 2008 neu eingeführt. Dabei handelt es sich im Wesentlichen um einen Delegaten, bei dem der Rückgabetyp als letzter generischer Parameter angegeben wird und bis zu vier Argumente als vorangehende generische Parameter angegeben werden können. (Es gibt mehrere Func-Delegaten, von denen jeder eine bestimmte Anzahl von Parametern akzeptiert.) Der Func-Delegattyp wird in der System.Core.dll-Assembly im System-Namespace definiert. Alle neuen Projekte, die mit Visual Basic erstellt werden, verfügen automatisch über einen Verweis auf System.Core.dll, sodass Sie den Func-Typ sofort nutzen können.
Der folgende Code zeigt die verschiedenen Überladungen von Func:
Dim f0 As Func(Of Boolean)
Dim f1 As Func(Of Integer, Boolean)
Dim f4 As Func(Of Integer, Integer, Integer, Integer, Boolean)
Hier ist f0 ein Delegat, der einen booleschen Wert zurückgibt, f1 ist ein Delegat, der eine ganze Zahl annimmt und einen booleschen Wert zurückgibt, und f4 ist ein Delegat, der vier Ganzzahlargumente annimmt und einen booleschen Wert zurückgibt. Besonders zu beachten ist, dass ein Lambda-Ausdruck als Delegat typisiert ist, d. h. er ist genau wie die Delegaten in Visual Basic 2005 eine aufrufbare Entität.
Auf der rechten Seite der Zuweisung im ersten Codeausschnitt sehen Sie die neue Syntax für Lambda-Ausdrücke. Sie beginnt mit dem Schlüsselwort „Function“, auf das dann eine Argumentliste und ein einzelner Ausdruck folgen.
Im vorstehenden Beispiel akzeptiert der Lambda-Ausdruck ein Argument (x), bei dem es sich um eine ganze Zahl handelt. Beachten Sie jedoch, dass es keine Rückgabeanweisung gibt. Dies liegt daran, dass dem Visual Basic-Compiler der Typ bereits auf Grundlage des Ausdrucks bekannt ist, sodass eine Rückgabeanweisung überflüssig ist. Im vorliegenden Fall gilt: Da x eine ganze Zahl ist, ist 2 * x ebenfalls eine ganze Zahl. Daher ist das Ergebnis des Lambda-Ausdrucks eine ganze Zahl.
Das Schöne an einem Lambda-Ausdruck besteht darin, dass Sie ihn einfach wie einen normalen Delegaten aufrufen können, wie Sie hier sehen:
Dim doubleIt As Func(Of Integer, Integer) = _
    Function(x As Integer) x * 2
Dim z = doubleIt(20)
Wenn Sie diesen Code ausführen, werden Sie feststellen, dass der in z gespeicherte Wert 40 lautet. Sie haben im Grunde einen Lambda-Ausdruck erstellt, der jeden ganzzahligen Wert verdoppelt, den Sie an ihn übergeben.
Betrachten wir ein etwas komplexeres Beispiel – eine Lambda-Ausdrucks-Factory:
Dim mult As Func(Of Integer, Func(Of Integer, Integer)) = _
    Function(x As Integer) Function(y As Integer) x * y
Mult ist ein recht komplizierter Lambda-Ausdruck. Er nimmt ein Ganzzahlargument als Eingabe an und gibt einen anderen Lambda-Ausdruck zurück, der ein Ganzzahlargument als Eingabe annimmt und eine ganze Zahl zurückgibt. Die Syntax ist etwas kompliziert, die Verwendung der folgenden Zeilenfortsetzungen und Formatierungen kann jedoch bei der Angabe der genauen Verschachtelungsstruktur helfen:
Dim mult As Func(Of Integer, Func(Of Integer, Integer)) = _
    Function(x As Integer) _
        Function(y As Integer) x * y
Dies sollte etwas klarer sein: Der äußere Lambda-Ausdruck enthält einen weiteren Lambda-Ausdruck, der vom Compiler als Rückgabeanweisung verwendet wird. Die Signatur des inneren Lambda-Ausdrucks stimmt mit der Func(Of Integer, Integer)-Delegatsignatur im Rückgabeargument des äußeren Lambda-Ausdrucks überein. Daher wird die Anweisung vom Compiler fehlerfrei kompiliert.
Wie wird ein derartiger Lambda-Ausdruck verwendet?
Dim mult_10 = mult(10)
Dim r = mult_10(4)
Die erste Zeile definiert hier mult_10 als mult(10). Da Mult(10) einen Lambda-Ausdruck zurückgibt, der ein Argument annimmt und es mit 10 multipliziert, ist der Typ von mult_10 Func(Of Integer, Integer). Die zweite Zeile ruft mult_10 mit dem Wert 4 auf, sodass das Ergebnis von r 40 und der Typ von r „Integer“ ist.
Im Grunde ist mult eine Lambda-Ausdrucks-Factory. Sie gibt Lambda-Ausdrücke zurück, die durch das erste Argument angepasst werden. Möglicherweise haben Sie bemerkt, dass der innere Lambda-Ausdruck auf den Parameter des äußeren Lambda-Ausdrucks verweist, die Lebensdauer des inneren Lambda-Ausdrucks jedoch die Lebensdauer des äußeren Ausdrucks überschreitet. Ich werde an späterer Stelle auf diese so genannte „Variablenanhebung“ eingehen.

Lambda-Ausdrücke als Rückrufe
Da Lambda-Ausdrücke einfach Delegaten sind, können Sie sie an denselben Stellen wie Delegaten verwenden. Betrachten Sie die folgende Methode, die einen Delegaten annimmt und den Delegaten für jedes Element in einer Liste aufruft:
Delegate Function ShouldProcess(Of T)(element As T) As Boolean

Sub ProcessList(Of T)( _
        elements As List(Of T), shouldProcess As ShouldProcess(Of T))
    For Each elem in elements
        If shouldProcess(elem) Then
            ' Do some processing here
        End If
    Next
End Sub
Hierbei handelt es sich um eine recht übliche Anwendung von Delegaten. ProcessList der Methode durchläuft jedes der Elemente in der Liste, prüft, ob das Element verarbeitet werden sollte, und führt dann eine Standardverarbeitung durch.
Um dieses Verfahren in Visual Basic 2005 zu verwenden, müssten Sie eine Funktion in der Klasse oder dem Modul definieren, die bzw. das dieselbe Signatur wie der Delegat aufweist, und dann die Adresse der Funktion an die ProcessList-Prozedur übergeben, beispielsweise wie folgt (beachten Sie den rot dargestellten Code):
Class Person
    Public age As Integer
End Class

Function _PrivateShouldProcess(person As Person) As Boolean
    Return person.age > 50
End Function

Sub DoIt()
    Dim list As New List(Of Person)
    ' Obtain list of Person from a database, for example
    ProcessList(list, AddressOf _PrivateShouldProcess)
End Sub
Dies ist im besten Fall mühsam, häufig müssen Sie jedoch die Codedokumentation durchsuchen, um herauszufinden, welche Signatur der Delegat verlangt, und dann eine genaue Übereinstimmung sicherstellen. Wenn Sie ProcessList mit vielen verschiedenen shouldProcess-Funktionen aufrufen müssen, verunreinigen Sie darüber hinaus Ihren Code mit vielen kleinen privaten Funktionen.
Sehen wir uns an, wie Sie diese Funktion mit Lambda-Ausdrücken aufrufen können:
Class Person
    Public age As Integer
End Class

Sub DoIt()
    Dim list As New List(Of Person)
    ' Obtain list of Person from a database, for example
    ProcessList(list, Function(person As Person) person.age > 50)
End Sub    
Ich liebe die Eleganz und Einfachheit von Lambda-Ausdrücken. Sie müssen keine eigene Funktion zur Ausführung der Verarbeitungslogik erstellen. Der Delegat wird an der Stelle definiert, an der er verwendet wird. Dies ist viel besser, als wenn er irgendwo in einer privaten Methode definiert ist und seine Lokalität mit der Methode verliert, die die private Methode verwendet.
Ich bin sicher, dass Sie erkennen, dass Lambda-Ausdrücke leistungsstark und praktisch sind und dazu beitragen können, dass Ihr Code leichter zu lesen und zu verwalten ist. Erweiterte Features wie z. B. Typrückschluss erhöhen die Leistungsfähigkeit noch weiter.
Eine Einschränkung, die Sie beachten sollten, besteht darin, dass ein Lambda-Ausdruck genau das ist, was der Name besagt – ein einzelner Ausdruck. In Visual Basic 2008 darf nur ein einziger Ausdruck in einem Lambda-Ausdruck enthalten sein. Im weiteren Verlauf dieses Artikels stelle ich einen neuen ternären Operator vor, der mit Visual Basic 2008 eingeführt und Ihnen die Erstellung bedingter Ausdrücke ermöglichen wird. Das aktuelle Feature unterstützt jedoch keine beliebigen Anweisungen in einem Lambda-Ausdruck.
Bevor ich jedoch die tieferen Konzepte erörtere, auf denen Lambda-Ausdrücke beruhen, möchte ich erklären, warum Lambda-Ausdrücke überhaupt eingeführt wurden.

Warum wurden Lambda-Ausdrücke hinzugefügt?
Zur Unterstützung von LINQ-Abfragen mussten einige Features hinzugefügt werden, darunter Visual Basic- und Lambda-Ausdrücke. Angenommen, Sie verfügen über die folgende Abfrageanweisung in Visual Basic:
Dim q = From p In Process.GetProcesses() _
        Where p.PriorityClass = ProcessPriorityClass.High _
        Select p
Zur Kompilierung dieser Anweisung sind zahlreiche Schritte hinter den Kulissen erforderlich. Grob gesagt, durchläuft der Compiler die Process.GetProcesses-Auflistung, wendet den Where-Filter darauf an und gibt eine Liste der Prozesse zurück, die dem Filter in der Where-Klausel entsprechen.
Beachten Sie, dass die Where-Klausel einen Visual Basic-Ausdruck enthält: p.PriorityClass = ProcessPriorityClass.High. Um diesen Filter auszuführen, erstellt der Compiler einen Lambda-Ausdruck für den Where-Filter und wendet ihn auf jedes Element in der Prozessliste an:
Dim q = Process.GetProcesses().Where( _
            Function(p) p.PriorityClass = ProcessPriorityClass.High)
Im Wesentlichen bietet der Lambda-Ausdruck dem Compiler eine Kurzform für das Ausgeben von Methoden und deren Zuweisung zu Delegaten. All diese Schritte werden Ihnen abgenommen. Ein Vorteil eines Lambda-Ausdrucks gegenüber einer Delegat/Funktion-Kombination besteht darin, dass der Compiler einen automatischen Typrückschluss für die Lambda-Argumente durchführt. Im Beispiel oben wird von der Verwendung auf den Typ des Arguments p zurückgeschlossen. In diesem Fall definiert das Where-Argument den Typ des Lambda-Ausdrucks, und vom Compiler wird auf den Typ des als Argument verwendeten Lambda-Ausdrucks zurückgeschlossen. Die vom Compiler unterstützten Typrückschlussfeatures stellen eine leistungsstarke Erweiterung von Visual Basic dar. Betrachten wir nun, welche Vorteile Ihnen diese Features bieten.

Typrückschluss
Die Einführung leistungsstarker Typrückschlussmethoden bedeutet, dass Sie sich keine Gedanken über die Ermittlung des Typs der einzelnen Variablen machen müssen. Darüber hinaus ermöglicht der Typrückschluss Szenarios, die ansonsten unmöglich wären. Betrachten wir nun drei verschiedene Methoden, mit denen bei der Verwendung von Lambda-Ausdrücken auf den Typ zurückgeschlossen wird.
Rückschluss auf den Typ von Lambda-Ausdrucks-Argumenten Dieses Szenario ist wirklich praktisch, wenn Sie einem Delegattyp einen Lambda-Ausdruck zuweisen und die Argumente nicht vollständig angeben möchten:
Dim lambda As Func(Of Integer, Integer) = Function(x) x * x
Im Beispiel ist die Lambda-Variable als Func(Of Integer, Integer) typisiert. Hierbei handelt es sich um einen Delegaten, der ein Ganzzahlargument annimmt und ein Ganzzahlargument zurückgibt. Daher schließt der Compiler automatisch, dass das Lambda-Argument x und der Rückgabewert des Lambda-Ausdrucks eine ganze Zahl sind.
Der Rückschluss auf den Typ von Lambda-Ausdrucks-Argumenten ist auch von Vorteil, wenn Sie eine Methode aufrufen, die einen Delegaten annimmt. Betrachten Sie diese abgeänderte Version eines früheren Beispiels:
Delegate Function ShouldProcess(Of T)(element As T) As Boolean

Sub ProcessList(Of T)( _
        elements As List(Of T), shouldProcess As ShouldProcess(Of T))
    ' Method body removed for brevity
End Sub
In diesem Fall nimmt die ProcessList-Funktion einen Lambda-Ausdruck an.
Sie können die Prozedur folgendermaßen aufrufen:
Sub DoIt()
    Dim list As New List(Of A)
    ' fill or obtain elements in list
    ProcessList(list, Function(a) a.x > 50)
End Sub
Beachten Sie, dass ich diesmal nicht den Typ des Lambda-Arguments angegeben habe. Dennoch schließt der Compiler auf den Typ „Person“. Wie kommt es dazu? Dieses Bespiel umfasst tatsächlich mehrere Ebenen des Typrückschlusses.
Zuerst erkennt der Compiler, dass ProcessList eine generische Prozedur ist, die List(Of T) und ShouldProcess(Of T) als Eingabe annimmt. Im Aufruf von ProcessList stellt der Compiler fest, dass „list“ das erste Argument ist und dass es sich dabei um eine Liste von Personen (List(Of Person)) handelt. Da das zweite Argument keine Hinweise darauf bietet, worum es sich beim Typ T handelt, entscheidet der Compiler, dass T zum Typ „Person“ gehört. Als Nächstes folgert er, dass das generische Argument für ShouldProcess(Of T) „Person“ ist, und schließt daraus, dass das zweite Argument ShouldProcess(Of Person) ist. Da im Lambda-Ausdruck kein Typ für sein Argument angegeben wurde, erkennt der Compiler dann den Argumenttyp auf Grundlage der Delegatsignatur von ShouldProcess(Of Person) und schließt darauf, dass der Typ des Parameters (a) „Person“ ist. Dies ist ein äußerst leistungsfähiges Modell für den Typrückschluss. Sie müssen den Typ der Delegatargumente nicht kennen, wenn Sie den Lambda-Ausdruck erstellen, und ich empfehle Ihnen tatsächlich, dass Sie dem Compiler den schwierigen Teil der Arbeit überlassen. Glücklicherweise erhalten Sie jedoch IntelliSense® und QuickInfos, die bei der Angabe des zurückgeschlossenen Typs helfen, sodass Sie genauso produktiv, wenn nicht gar produktiver arbeiten können.
Rückschluss auf den Ergebnistyp Dieses Szenario ist äußerst praktisch, wenn Sie keinen Delegattyp haben und möchten, dass der Compiler für Sie einen Delegattyp erstellt. Dieses Feature ist nur in Visual Basic verfügbar:
Dim lambda = Function(x As Integer) x * x
Im Beispiel ist der Lambda-Ausdruck vollständig typisiert (das Lambda-Argument x hat den Typ „Integer“, und der Compiler folgert, dass der Rückgabewert eine ganze Zahl ist, da ganze Zahl * ganze Zahl = ganze Zahl). Die Lambda-Variable hat jedoch keinen Typ. Daher erstellt der Compiler künstlich einen anonymen Delegaten, der der Form des Lambda-Ausdrucks entspricht, und weist dem Lambda-Ausdruck diesen Delegattyp zu.
Dies ist ein hervorragendes Feature, denn es bedeutet, dass Sie Lambda-Ausdrücke dynamisch erstellen können, ohne ihre Delegattypen statisch erstellen zu müssen. Wie oft waren Sie beispielsweise in der Situation, dass Sie eine Bedingung einem Satz von Variablen zuweisen mussten, und zwar an mehreren Stellen wie in Abbildung 1? Beim Programmieren sind mir derartige Situationen schon recht häufig begegnet. Normalerweise würde ich dafür sorgen, dass ich die Bedingungsüberprüfung nur an einer Stelle und nicht in der gesamten Funktion verteilt durchführen kann.
Class Motorcycle
    Public color As String
    Public CC As Integer
    Public weight As Integer
End Class

Sub PrintReport(motorcycle As New Motorcycle)
    If motorcycle.color = "Red" And motorcycle.CC = 600 And _
       motorcycle.weight > 300 And motorcycle.weight < 400 Then
       ' do something here
    End If

    ' do something here

    If motorcycle m.color = "Red" And motorcycle.CC = 600 And _
       motorcycle.weight > 300 And motorcycle.weight < 400 Then
       ' do something here
    End If
End Sub

Es gibt jedoch Situationen, in denen die Überprüfung nur in dieser Funktion und nirgendwo sonst verwendet wird. Ich streue nicht gern willkürliche Hilfsfunktionen in eine Klasse ein, die nur zur Unterstützung dieser Funktion verwendet werden, da sich dies negativ auf die Wartbarkeit auswirkt – was ist, wenn jemand anderes diese Funktion aufruft und ich eine Änderung vornehmen muss? Es kann auch zu einer Namensfülle kommen – es ist schwer, bei Klassen mit zahlreichen privaten Methoden den Überblick zu behalten. Zudem könnte der Nutzen von IntelliSense abnehmen, da die IntelliSense-Liste mehr und mehr Einträge enthält. Darüber hinaus wird die Lokalität der Logik beeinflusst. Wenn ich eine separate private Methode erstelle, möchte ich, dass sie sich physisch in der Nähe der Methode befindet, von der sie verwendet wird. Wenn viele Programmierer an derselben Codebasis arbeiten, kann es schwierig sein, diese Lokalität langfristig aufrechtzuerhalten. Wenn Sie Lambda-Ausdrücke verwenden und den Compiler automatisch Delegatklassen erstellen lassen, werden diese Probleme beseitigt, wie in Abbildung 2 dargestellt.
Sub PrintReport(motorcycle As New Motorcycle)
    Dim check = Function(m As Motorcycle) m.color = "Red" And _
                                          m.CC = 600 And _
                                          m.weight > 300 And _
                                          m.weight < 400
    If check(motorcycle) Then
        ' do something here
    End If

    ' do something here

    If check(motorcycle) Then
        ' do something here
    End If
End Sub

Ich habe die Logik zum Prüfen einiger Bedingungen für die Motorcycle-Klasse ausgelagert – nicht in eine private Methode, wo dies mit Nachteilen verbunden ist, sondern in einen Lambda-Ausdruck, für den der Compiler automatisch einen Delegattyp erstellt (der verborgen ist) und alle erforderlichen Schritte ausführt, sodass ich den Lambda-Ausdruck so aufrufen kann, als ob er eine Methode wäre.
Mir gefällt dieser Ansatz sehr, da die Logik in der Nähe der Implementierung angeordnet wird (innerhalb des Methodentexts), die Übersichtlichkeit gewährleistet ist (nur eine Kopie) und der Compiler einen Großteil der Wartung übernimmt. Dies funktioniert gut, weil Sie einen beliebig komplexen Ausdruck als Text für den Lambda-Ausdruck erstellen können.
Spät gebundene Objekte und Typrückschluss In diesem Szenario ist weder die Lambda-Variable, noch der Lambda-Ausdruck typisiert:
Dim lambda = Function(x) x * x
Hier wird ebenfalls ein anonymer Delegat vom Compiler für Sie generiert, die Typen des Lambda-Ausdrucks sind jedoch System.Object. Dies bedeutet, dass in diesem Szenario die späte Bindung aktiviert ist, wenn Option Strict auf „Off“ gesetzt ist.
Dies ist ein wirklich nützliches Szenario für Entwickler, die auf die späte Bindung angewiesen sind. Lambda-Ausdrücke unterstützen Vorgänge mit später Bindung vollständig. Solange daher im Beispiel oben der Operator „*“ für die an den Lambda-Ausdruck übergebenen Typen definiert wird, funktioniert der Code problemlos:
Dim a = lambda(10)
Dim b = lambda(CDec(10))
Dim c = lambda("This will throw an exception because " & _
               "strings don't support the * operator")
Wie Sie in den vorstehenden Beispielen sehen können, funktioniert alles einwandfrei, solange der Laufzeittyp den Operator „*“ aufweist. Diese Art von Lambda-Ausdrücken passt sehr gut zum Modell der späten Bindung in Visual Basic.

Codegenerierung im Detail
Nachdem ich nun das Konzept der Lambda-Ausdrücke erläutert habe, möchte ich darauf eingehen, welche Art von Code der Compiler generiert. Betrachten Sie das frühere Beispiel:
Sub TestLambda()
    Dim doubleIt As Func(Of Integer, Integer) = _
        Function(x As Integer) x * 2
    Console.WriteLine(doubleIt(10))
End Sub
Sie wissen, dass Func ein Delegat ist und Delegaten einfach nur Zeiger auf Funktionen sind. Wie funktioniert nun der Zaubertrick des Compilers? In diesem Fall gibt der Compiler eine neue Funktion für Sie aus und richtet den Delegaten so ein, dass er auf die neue Funktion zeigt:
Function $GeneratedFunction$(x As Integer) As Integer
    Return x * 2
End Sub

Sub TestLambda()
    Dim doubleIt As Func(Of Integer, Integer) = _
        AddressOf $GeneratedFunction$
    Console.WriteLine(doubleIt(10))
End Sub
Der Compiler erstellt im Wesentlichen eine neue Funktion mit dem Inhalt des Lambda-Ausdrucks und ändert die Zuweisungsanweisung so, dass der Lambda-Ausdruck die Adresse der generierten Funktion annimmt. In diesem Fall wird die Funktion im selben übergeordneten Element generiert, das die Methode enthält, von der der Lambda-Ausdruck verwendet wird. Wenn TestLambda für eine Klasse C definiert ist, ist auch die generierte Funktion für C definiert. Beachten Sie, dass die generierte Funktion nicht aufrufbar und als privat markiert ist.

Lambda-Ausdrücke und Variablenanhebung
In den bisherigen Beispielen verwiesen die Texte der Lambda-Ausdrücke auf Variable, die an den Lambda-Ausdruck übergeben wurden. Die Leistungsfähigkeit von Lambda-Ausdrücken entfaltet sich jedoch erst mit der Variablenanhebung vollständig. Wie ich bereits an früherer Stelle angedeutet habe, vollführt der Compiler in bestimmten Szenarios einen „Zaubertrick“. Bevor wir diese Szenarios genauer untersuchen, sollten wir jedoch einige grundlegende Konzepte aus dem als „Lambda-Kalkül“ bezeichneten Zweig der Mathematik betrachten, da Lambda-Ausdrücke auf einem ähnlichen Konzept basieren.
Das grundlegende Konzept beim Lambda-Kalkül besteht darin, dass eine Funktion freie Variable oder gebundene Variable aufweisen kann. Freie Variable sind Variable, die in den Variablen der enthaltenden Methode (lokale Variablen und Parameter) definiert sind. Gebundene Variable sind Variable, die in der Lambda-Signatur definiert oder Member der Klasse sind, die den Lambda-Ausdruck enthält, einschließlich Basisklassen.
Es ist sehr wichtig, in Ihren Lambda-Ausdrücken zwischen gebundenen und freien Variablen zu unterscheiden, da sie die Semantik des Lambda-Ausdrucks, den generierten Code und letztlich die Fehlerfreiheit Ihres Programms beeinflussen. Hier ist ein Beispiel für einen Lambda-Ausdruck, der gebundene und freie Variable enthält:
Dim y As Integer = 10
Dim addTen As Func(Of Integer, Integer) = Function(ByVal x) x + y
Die Variable x wird hier als gebundene Variable im Lambda-Ausdruck betrachtet, da es sich um einen formalen Parameter für den Lambda-Ausdruck handelt, und y wird als freie Variable betrachtet, da es sich um eine Variable handelt, die zur enthaltenden Methode des Lambda-Ausdrucks gehört.
Denken Sie daran, dass ein von Ihnen definierter Lambda-Ausdruck wie ein Delegattyp behandelt wird. (Sie können den Lambda-Ausdruck beispielsweise aus einer Methode zurückgeben.) Betrachten Sie folgendes Beispiel:
Function MakeLambda() As Func(Of Integer, Integer)
    Dim y As Integer = 10
    Dim addTen As Func(Of Integer, Integer) = Function(ByVal x) x + y
    Return addTen
End Function

Sub UseLambda()
    Dim addTen = MakeLambda()
    Console.WriteLine(addTen(5))
End Sub
Durch diesen Code wird „15“ auf der Konsole ausgegeben, wenn UseLambda aufgerufen wird. Vielleicht fragen Sie sich, wie das funktioniert. Die Funktion „MakeLambda“ definiert y als eine lokale Variable, und y wird im Lambda-Ausdruck verwendet, der Lambda-Ausdruck wird jedoch aus der Funktion „MakeLambda“ zurückgegeben. Die Funktion „UseLambda“ ruft den Lambda-Ausdruck aus MakeLambda ab und führt ihn aus, und es scheint so, als ob die Variable y irgendwie im Lambda-Ausdruck gespeichert wird.
Handelt es sich dabei um eine Art internen Trick? Die Lebensdauer von y ist die Methode von MakeLambda. Wenn wir den aus MakeLambda zurückgegebenen Lambda-Ausdruck ausführen, liegt MakeLambda außerhalb des Gültigkeitsbereichs, und ihr Stapelspeicher sollte entfernt werden. Die Variable y wurde jedoch für den Stapel definiert und ist irgendwie beim Lambda-Ausdruck verblieben.
Dieses Verhalten ist das häufig als Variablenanhebung bezeichnete Phänomen. In diesem Fall wird die Variable y als angehobene Variable bezeichnet. Wie Sie sehen können, sind angehobene Variable sehr leistungsfähig: Der Compiler nimmt Ihnen viel Arbeit ab, um den Status der Variablen zu erfassen und sie über ihre normale Lebensdauer hinaus beizubehalten.
Formeller ausgedrückt: Wenn der Compiler auf einen Lambda-Ausdruck mit freien Variablen trifft, hebt er die freien Variablen in eine als Closure bezeichnete Klasse an. Die Lebensdauer der Closure geht über die Lebensdauer der in ihr angehobenen freien Variablen hinaus. Der Compiler schreibt den Variablenzugriff in der Methode um, um auf die Variable innerhalb der Closureinstanz zuzugreifen.
Betrachten wir zuerst noch einmal das MakeLambda-Beispiel:
Function MakeLambda() As Func(Of Integer, Integer)
    Dim y As Integer = 10
    Dim addTen As Func(Of Integer, Integer) = Function(ByVal x) x + y
    Return Lambda
End Function
Wie bereits analysiert, ist x an den Parameter des Lambda-Ausdrucks gebunden, y ist jedoch eine freie Variable. Der Compiler erkennt dies und erstellt eine Closureklasse, in der die freien Variablen sowie die Definition des Lambda-Ausdrucks erfasst werden:
Public Class _Closure$__1
    Public y As Integer
    Public Function _Lambda$__1(ByVal x As Integer) As Integer
        Return x + Me.y
    End Function
End Class
Sie sehen, dass die Closurevariable die Variable y erfasst und in der Closureklasse speichert. Die freie Variable wird dann in eine gebundene Variable für die Closureklasse konvertiert.
Der Compiler schreibt auch die Methode, die den Lambda-Ausdruck enthält, folgendermaßen um:
Function MakeLambda() As Func(Of Integer, Integer)
    Dim Closure As New _Closure$__1
    Closure.y = 10
    Return AddressOf Closure._Lambda$__1
End Function
Jetzt können Sie sehen, wie der Compiler die Closurevariable erstellt, die lokale Variable y umschreibt, die in die Closurevariable angehoben wird, die Variable initialisiert und einfach die Adresse des Lambda-Ausdrucks zurückgibt, der in die Closureklasse geschrieben wurde.
Beachten Sie, dass der Compiler nur freie Variable im Lambda-Ausdruck anhebt. Der Status der Variablen wird in der Closure erfasst, die so lange existiert, wie der Lambda-Ausdruck existiert.
Betrachten wir ein weiteres Beispiel:
Sub Test()
    Dim y As Integer = 10
    Dim Lambda As Func(Of Integer, Integer) = Function(ByVal x) x + y
    y = 20
    Console.WriteLine(Lambda(5))
End Sub
Welcher Wert wird angezeigt, wenn Sie diese Funktion ausführen? Wenn Sie auf 25 getippt haben, lagen Sie richtig. Weshalb 25? Nun, der Compiler erfasst alle freien Variablen y und schreibt sie folgendermaßen in die Kopie der Closure um:
Sub Test()
    Dim Closure As New $CLOSURE_Compiler_Generated_Name$
    Closure.y = 10
    Dim Lambda = AddressOf Closure.Lambda_1
    Closure.y = 20
    Console.WriteLine(Lambda(5))
End Function
Wie Sie sehen können, wurde der Wert von y vor der Ausführung des Lambda-Ausdrucks in 20 geändert. Daher wird 5 + 20 zurückgegeben, wenn der Lambda-Ausdruck ausgeführt wird.
Dies ist bei Schleifen besonders wichtig. Da die freien Variablen in einer einzigen Closure erfasst werden, kann ein unerwartetes Verhalten auftreten, wenn Sie beispielsweise einen Thread erzeugen, der den Lambda-Ausdruck mit einer erfassten Variablen verwendet, die sich ändert:
Sub Test()
    For I = 1 To 5
        StartThread(Function() I + 10)
    Next
End Function
Angenommen, in diesem Beispiel erstellt StartThread einen neuen Thread und gibt das Ergebnis des Lambda-Ausdrucks auf der Konsole aus. Da I in der Closure erfasst wird, ist es möglich, dass die For-Schleife bis zur Ausführung des Lambda-Ausdrucks durch den Thread den Wert von I geändert hat. In diesem Fall gibt das Programm möglicherweise nicht wie erwartet 11, 12, 13, 14 und 15 aus. Stattdessen müssen Sie den Bereich der erfassten Variablen innerhalb der For-Schleife angeben:
Sub Test()
    For I = 1 To 5
        Dim x = I
        StartThread(Function() x + 10)
    Next
End Function
Dieser Code erfasst jetzt den Wert von x in der Closure, und das Programm gibt wie erwartet 11, 12, 13, 14 und 15 aus. Es ist äußerst wichtig zu wissen, welche Variable angehoben werden, wann die Lambda-Ausdrücke ausgeführt werden und wann sich die angehobenen Variablen ändern können, damit Sie sicher sein können, dass Ihr Programm unter allen Umständen ordnungsgemäß ausgeführt wird.

Optimale Nutzung von Lambda-Ausdrücken
In Visual Basic 2008 können Sie nur einen Ausdruck innerhalb des Lambda-Ausdrucks angeben. Es wird jedoch auch ein neues ternäres Schlüsselwort eingeführt, das Ihnen die Angabe einfacher, vollständig typisierter bedingter Ausdrücke ermöglicht:
Dim x = If(condition, 10, 20)
Das Schlüsselwort „If“ ähnelt dem IIF-Funktionsaufruf, außer dass das Schlüsselwort vollständig typsicher ist. Das bedeutet, dass der Compiler im Beispiel oben gefolgert hat, dass beide Verzweigungen des If-Schlüsselworts eine ganze Zahl zurückgeben. Daher wendet er Typrückschlussregeln an und entscheidet, dass der Typ von x „Integer“ ist. Bei Verwendung von IIF hat x den Typ „Object“.
Sie können das If-Schlüsselwort in einem Lambda-Ausdruck verwenden:
Dim x = Function(c As Customer) _
    If(c.Age >= 18, c.Address, c.Parent.Address)
In diesem Beispiel wird angenommen, dass eine Customer-Klasse vorhanden ist, deren Definition die Address-Eigenschaft enthält, die erwartungsgemäß die aktuelle Adresse des Kunden darstellt. Im Lambda-Ausdruck wird der ternäre Ausdruck verwendet, um eine Bedingung auf das Eingabeargument anzuwenden. Wenn der Kunde mindestens 18 Jahre alt ist, wird seine Adresse zurückgegeben. Andernfalls wird die Adresse der Eltern zurückgegeben.
An dieser Stelle kommt der Typrückschluss ins Spiel, und der Compiler stellt fest, dass der Rückgabetyp des Lambda-Ausdrucks „Address“ ist. Er erstellt dann einen Delegattyp für x (wie bereits erläutert), wobei der Delegattyp „Customer“ als Eingabe annimmt und eine Adresse zurückgibt.
Wenn Sie mehr über Lambda-Ausdrücke erfahren möchten, können Sie meinen Blog (blogs.msdn.com/timng) abonnieren, in dem ich Lambda-Ausdrücke (und weitere Features der Visual Basic 2008-Sprache) ausführlicher behandeln werde.

Senden Sie Ihre Fragen und Kommentare (in englischer Sprache) an instinct@microsoft.com.


Timothy Ng ist Softwareentwickler im Visual Basic Compiler-Team von Microsoft. Er hat an mehreren Features für die bevorstehende Veröffentlichung von Visual Studio 2008 gearbeitet, einschließlich Typrückschluss, Friend-Assemblys, Ausdrucksstrukturen und anonymer Typen. Sie erreichen Tim Ng unter timng@microsoft.com.

Page view tracker