Vorschau auf Visual Basic 9.0

Veröffentlicht: 16. Apr 2007

Von Erik Meijer, Amanda Silver, und Paul Vick

Diese Vorschau bietet einen Überblick über neue Features des Visual Basic 2005-Nachfolgers bei der Visual Basic-Sprache und neuen Spracherweiterungen, die datenintensives Programmieren unterstützen.

* * *

Auf dieser Seite

Einführung Einführung
Erste Schritte mit Visual Basic 9.0 Erste Schritte mit Visual Basic 9.0
Implizit typisierte lokale Variable Implizit typisierte lokale Variable
Objekt- und Arrayinitialisierer Objekt- und Arrayinitialisierer
Anonyme Typen Anonyme Typen
Tiefgreifende XML-Unterstützung Tiefgreifende XML-Unterstützung
Abfragebegriffe Abfragebegriffe
Erweiterungsmethoden und Lambda-Ausdrücke Erweiterungsmethoden und Lambda-Ausdrücke
Nullfähige Typen Nullfähige Typen
Lockere Delegaten Lockere Delegaten
Schlussbemerkung Schlussbemerkung

Einführung

Das Hauptaugenmerk von Visual Basic lag immer auf der Erstellung von pragmatischen, datenorientierten Geschäftsanwendungen. Die Hinwendung zu .NET hat für Anwendungsentwickler ein leistungsfähiges, einheitliches Framework und eine verwaltete Plattform mit sich gebracht.

Visual Basic 9.0, die voraussichtlich Ende 2007 mit Visual Studio 2008/9.0 erscheinende Nachfolgeversion von Visual Basic 2005, enthält nunmehr eine Reihe von Features, die sich maßgeblich auf die Produktivität von Entwicklern beim Erstellen datenorientierter Anwendungen auswirken. Diese Spracherweiterungen führen allgemeine Abfragefunktionen ein, die für alle Datenquellen anwendbar sind – unabhängig davon, ob es sich um relationale Quellen, hierarchische Objektdiagramme oder XML-Dokumente handelt.

Im vorliegenden Dokument erhalten Sie einen Überblick über diese neuen Features. Weitere Informationen sowie Aktualisierungen der Sprachdefinition von Visual Basic sowie Compilerpreviews finden Sie im englischsprachigen Visual Basic-Entwicklercenter (https://msdn2.microsoft.com/vbasic/default.aspx).

 

Erste Schritte mit Visual Basic 9.0

Wie leistungsfähig diese Sprachfeatures tatsächlich sind, lässt sich zunächst an einem Beispiel aus der Realität erkennen – der Datenbank des CIA World Factbook. Sie enthält vielerlei Informationen zu geografischen, wirtschaftlichen, gesellschaftlichen und politischen Aspekten über die Länder der Welt. Für unser Beispiel erstellen wir zunächst ein Schema für die Namen der einzelnen Länder und deren Hauptstadt, Gesamtfläche und Bevölkerung. Dieses Schema wird in Visual Basic 9.0 dargestellt, wobei folgende Klasse (ein aus Platzspargründen eingesetzter Pseudocode) verwendet wird:

Class Country
  Public Property Name As String
  Public Property Area As Long 
  Public Property Population As Integer
End Class

Im Folgenden ist eine kleine Teilmenge der Länderdatenbank angeführt, die wir als Beispiel verwenden:

Dim countries = { 
  New Country With { .Name = "Palau", .Area = 458, .Population = 16952 }, _
  New Country With { .Name = "Monaco", .Area = 1.9, .Population = 31719 }, _
  New Country With { .Name = "Belize", .Area = 22960, .Population = 219296 }, _
  New Country With { .Name = "Madagascar", .Area = 587040, .Population = 13670507}}

Basierend auf dieser Liste können mithilfe des folgenden Abfrageausdrucks alle Länder abgefragt werden, die eine Bevölkerung von weniger als einer Million aufweisen:

Dim smallCountries = From country In countries _
                     Where country.Population < 1000000 _
                     Select country

For Each country In SmallCountries
  Console.WriteLine(country.Name)
Next

Da in der Liste lediglich Madagaskar mehr als eine Million Einwohner hat, würde die Ausgabe der Ländernamen mit oben angeführtem Programm nach dem Kompilieren und Ausführen folgendermaßen aussehen:

Palau Monaco Belize

Sehen wir uns das Programm genauer an, um die Features von Visual Basic 9.0 zu verstehen, die diesen Prozess so vereinfachen. Die Deklaration der einzelnen Ausdrücke, die für die Countries stehen, verwendet zunächst die neue Objektinitialisierer-Syntax New Country With {..., .Area = 458, ...}, um über eine präzise, ausdrucksbasierte Syntax eine komplexe Objektinstanz, ähnlich der bestehenden With-Anweisung, zu erstellen.

Die Deklaration veranschaulicht auch das Deklarieren von implizit typisierten lokalen Variablen, bei denen die lokale Variable Countries durch den Compiler vom Initialisiererausdruck auf der rechten Seite der Deklaration abgeleitet wird. Obige Deklaration ist die genaue Entsprechung einer explizit typisierten Deklaration mit lokalen Variablen des Typs Country().

Dim countries As Country() = {...}

Wie bereits erwähnt, handelt es sich um eine stark typisierte Deklaration. Der Compiler hat den Typ automatisch per Rückschluss aus der rechten Seite der lokalen Deklaration gefolgert, sodass der Programmierer ihn nicht manuell in das Programm eingeben muss.

Die Deklaration mit der lokalen Variablen SmallCountries wird mit einem SQL-artigen Abfrageausdruck initialisiert, um alle Länder herauszufiltern, die weniger als eine Million Einwohner haben. Die Ähnlichkeit mit SQL ist beabsichtigt und ermöglicht mit SQL vertrauten Programmierern eine schnellere Einarbeitung in die Abfragesyntax von Visual Basic.

Dim smallCountries = From country In Countries _
                     Where country.Population < 1000000 _
                     Select country

Das folgende Codebeispiel stellt eine andere Anwendung der impliziten Typisierung dar: Der Compiler folgert den Typ von SmallCountries auf Grundlage des Ergebnistyps des Abfrageausdrucks als IEnumerable(Of Country). Der Compiler übersetzt den Abfrageausdruck selbst in Aufrufe an das LINQ-fähige API, das die Abfrageoperatoren für alle IEnumerable(Of T) implementierenden Typen implementiert. In diesem Fall ist die Übersetzung ganz einfach:

Dim smallCountries As IEnumerable(Of Country) = _
       Countries.Where(Function(country) country.Population < 1000000 _
                      ).Select(Function(country) country)

Die erweitete verwendet Lambda-Ausdrücke, welche Inlinefunktionen repräsentieren, die das Ergebnis eines Ausdrucks zurückgeben. Der Lambda-Ausdruck wird in einen Delegaten konvertiert und an die Erweiterungsfunktion Where weitergeleitet, die in der Standard Query-Operatorenbibliothek als Erweiterung der IEnumerable(Of T)-Schnittstelle definiert ist.

Nach der Einführung einiger neuer Funktionen in Visual Basic 9.0 wenden wir uns nun einer detaillierteren Ansicht zu.

 

Implizit typisierte lokale Variable

In einer implizit typisierten Deklaration mit lokalen Variablen wird der Typ der lokalen Variablen von dem Initialisiererausdruck auf der rechten Seite einer lokalen Deklarationsanweisung gefolgert. So ermittelt der Compiler beispielsweise die Typen sämtlicher unten angeführten Variablendeklarationen:

Dim population = 31719
Dim name = "Belize"
Dim area = 1.9
Dim country = New Country With { .Name = "Palau", ...}

Sie stellen somit eine genaue Entsprechung der folgenden explizit typisierten Deklarationen dar:

Dim population As Integer = 31719
Dim name As String = "Belize"
Dim area As Float = 1.9
Dim country As Country = New Country With { .Name = "Palau", ...}

Da die Typen der lokal deklarierten Variablen unabhängig von der Option Strict-Einstellung gemäß der neuen Option Infer On (der Standardoption für neue Projekte) ermittelt werden, erfolgt der Zugriff auf solche Variablen immer mit früher Bindung. Die späte Bindung muss dann vom Programmierer in Visual Basic 9.0 explizit festgelegt werden, indem die Variablen als Object-Typ wie folgt deklariert werden:

Dim country As Object = New Country With { .Name = "Palau", ... }

Durch die Typermittlung wird verhindert, dass es versehentlich zu einer späten Bindung kommt. Außerdem werden dadurch leistungsfähige Bindungserweiterungen für neue Datentypen wie XML ermöglicht (dazu später mehr).

Bei der Schleifensteuerungsvariablen in einer For...Next- oder einer For Each...Next-Anweisung kann es sich ebenfalls um eine implizit typisierte Variable handeln. Wenn die Schleifensteuerungsvariable wie in For I = 0 To SmallCountries.Count oder in For Each country In smallCountries festgelegt wurde, definiert der Bezeichner eine neue implizit typisierte lokale Variable, deren Typ aus dem Initialisierer oder dem Sammelausdruck abgeleitet wird und für die gesamte Schleife gilt. Durch Anwendung dieser Typermittlung kann unsere Schleife, die alle kleinen Länder ausgibt, folgendermaßen neu geschrieben werden:

For Each country In smallCountries
  Console.WriteLine(country.Name)
Next

Der Typ von country wird als Country, dem Elementtyp von SmallCountries, ermittelt.

 

Objekt- und Arrayinitialisierer

Die With-Anweisung in Visual Basic vereinfacht den Zugriff auf mehrere Elemente eines Aggregatwerts, ohne dass dabei der Zielausdruck mehrmals angegeben werden muss. Innerhalb des With-Anweisungsblocks wird ein Elementzugriffsausdruck, der mit einem Punkt beginnt, ausgewertet, als ob dem Punkt der Zielausdruck der With-Anweisung vorangehen würde. Die folgenden Anweisungen initialisieren beispielsweise eine neue Country-Instanz und anschließend deren Felder mit den gewünschten Werten:

Dim palau As New Country()
With palau
  .Name = "Palau"  
  .Area = 458
  .Population = 16952
End With

Die neuen Objektinitialisierer in Visual Basic 9.0 stellen eine ausdrucksbasierte Form der With-Anweisung dar und dienen dem kompakten Erstellen komplexer Objektinstanzen. Durch die Verwendung von Objektinitialisierern können die beiden obigen Anweisungen wie folgt in einer einzelnen (implizit typisierten) lokalen Deklaration erfasst werden:

Dim palau = New Country With { _
  .Name = "Palau", _
  .Area = 458, _
  .Population = 16952 _
}

Diese Art der Objektinitialisierung von Ausdrücken ist für Abfragen von Bedeutung. In der Regel ähnelt eine Abfrage einer Objektdeklaration, die durch eine Select-Klausel auf der rechten Seite des Gleichheitszeichens initialisiert wurde. Da die Select-Klausel einen Ausdruck zurückgibt, muss das ganze Objekt durch einen einzelnen Ausdruck initialisiert werden können.

Wie wir gesehen haben, können Objektinitialisierer auch beim Erstellen komplexer Objektsammlungen hilfreich sein. Durch die Verwendung eines Ausdrucks zur Arrayinitialisierung können Arrays initialisiert und Elementtypen ermittelt werden. Im Fall der folgenden Klassendeklaration für Städte

Class City
  Public Property Name As String
  Public Property Country As String
  Public Property Longitude As Long 
  Public Property Latitude As Long
End Class

kann für unsere Beispielländer das folgende Hauptstädte-Array erstellt werden:

Dim Capitals = { _
  New City With { _
    .Name = "Antanarivo", _
    .Country = "Madagascar", _
    .Longitude = 47.4, _
    .Latitude = -18.6 }, _
  New City With { _
    .Name = "Belmopan", _
    .Country = "Belize", _
    .Longitude = -88.5, _
    .Latitude = 17.1 }, _
  New City With { _
    .Name = "Monaco", _
    .Country = "Monaco", _
    .Longitude = 7.2, _
    .Latitude = 43.7 }, _
  New City With { _
    .Country = "Palau",
    .Name = "Koror", _
    .Longitude = 135, _
    .Latitude = 8 } _
}

 

Anonyme Typen

Häufig sollen bestimmte Elemente eines Typs als Abfrageergebnis lediglich entfernt oder ausgeblendet werden. Wenn beispielsweise nur Name und Country aller Hauptstädte in Tropengebieten in Erfahrung gebracht werden sollen und dabei die Spalten Latitude und Longitude der Ausgangsdaten zur Ermittlung von Tropengebieten verwendet werden, können diese Spalten im Ergebnis ausgeblendet werden. In Visual Basic 9.0 lässt sich dies anhand einer neuen Objektinstanz für jede Stadt C bewerkstelligen, deren Längengrad zwischen dem nördlichen und südlichen Wendekreis liegt, ohne dass dabei der Typ benannt wird.

Const TropicOfCancer = 23.5
Const TropicOfCapricorn = -23.5

Dim tropical = From city In Capitals _
               Where   TropicOfCancer <= city.Latitude
               AndAlso city.Latitude  >= TropicOfCapricorn _
               Select New With {city.Name, city.Country}

Der per Rückschluss ermittelte Typ der lokalen Variablen tropical ist eine Instanzensammlung eines anonymen Typen, als Pseudocode: IEnumerable(Of { Name As String, Country As String }). Der Visual Basic-Compiler erstellt eine implizite Klasse, etwa _Name_As_String_Country_As_String_, deren Elementnamen und -typen aus dem Objektinitialisierer wie folgt ermittelt werden:

Class _Name_As_String_Country_As_String_ 
  Public Property Name As String
  Public Property Country As String
  ...
End Class

Der Compiler führt identische anonyme Typen innerhalb desselben Programms zusammen. Zwei anonyme Objektinitialisierer, die eine Eigenschaftensequenz mit denselben Namen und Typen in derselben Reihenfolge angeben, generieren Instanzen desselben anonymen Typs. Extern offeriert Visual Basic erzeugte anonyme Typen in unumkehrbarer Typkonvertierung als Object, wodurch der Compiler anonyme Typen einheitlich als Argumente und Funktionsergebnisse weitergeben kann.

Da anonyme Typen in der Regel dazu verwendet werden, Elemente eines bestehenden Typs zu projizieren, ermöglicht Visual Basic 9.0 die Kurzschrift-Projektionsnotation New With { city.Name, city.Country }, durch die die lange Form New With { .Name = city.Name, .Country = city.Country } abgekürzt wird. Im Ergebnisausdruck eines Abfragebegriffs können Projektionsinitialisierer sogar noch weiter abgekürzt werden:

Dim Tropical = From city In Capitals _
               Where   TropicOfCancer <= city.Latitude _
               AndAlso city.Latitude  >= TropicOfCapricorn _
               Select city.Name, city.Country

Beide abgekürzten Formen haben dieselbe Bedeutung wie die oben angeführte Langform.

 

Tiefgreifende XML-Unterstützung

LINQ to XML ist ein neues, speicherinternes XML-Programmierungs-API, das insbesondere die neuesten .NET Framework-Funktionen wie das Language Integrated Query-Framework nutzt. In derselben Weise, in der Abfragebegriffe den zugrundeliegenden .NET Framework-Abfrageoperatoren vertraute und praktische Syntax hinzufügen, bietet Visual Basic 9.0 über XML-Literale und XML-Eigenschaften tiefgreifende Unterstützung für LINQ to XML.

Lassen Sie uns zur Illustration von XML-Literalen eine Abfrage zu den – im Wesentlichen flachen – relationalen Datenquellen Countries und Capitals erstellen, um ein hierarchisches XML-Modell zu konstruieren, in dem die Hauptstädte der einzelnen Länder als untergeordnete Elemente geschachtelt werden und die Bevölkerungsdichte als Attribut berechnet wird.

Um die Hauptstadt eines bestimmten Landes zu ermitteln, wird ein Join über das Name-Element der einzelnen Länder und das Country-Element der einzelnen Städte vorgenommen. Wenn Land und Hauptstadt feststehen, kann das XML-Fragment danach einfach konstruiert werden, indem eingebettete Ausdruckslücken mit berechneten Werten aufgefüllt werden. Beispiele einer „Lücke“ für einen Visual Basic-Ausdruck mit ASP-ähnlicher Syntax wäre etwa Name=<%= country.Name %> oder <Name><%= city.Name %></Name>. Im Folgenden ist die Abfrage angeführt, in der XML-Literale und Abfragebegriffe kombiniert werden.

Dim countriesWithCapital As XElement = _
  <Countries>
  <%= From country In Countries, city In Capitals _
    Where country.Name = city.Country _
    Select <Country Name=<%= country.Name %>
                    Density=<%= country.Population / country.Area %>>
              <Capital>
                <Name><%= city.Name %></Name>
                <Longitude><%= city.Longitude %></Longitude>
                <Latitude><%= city.Latitude %></Latitude>
              </Capital>
           </Country> _
  %>
  </Countries>

Der Typ XElement könnte bei der Deklaration weggelassen werden und würde dann wie in jeder anderen lokalen Deklaration ermittelt.

In dieser Deklaration soll das Ergebnis der Select-Abfrage innerhalb des <Countries>-Elements ersetzt werden. Die Select-Abfrage ist somit der Inhalt der ersten „Lücke“ und wird durch die vertrauten Tags im ASP-Stil <%= und %> innerhalb von <Countries> abgegrenzt. Da es sich beim Ergebnis einer Select-Abfrage um einen Ausdruck handelt und XML-Literale Ausdrücke sind, bietet sich das Schachteln eines anderen XML-Literals im Select-Ausdruck selbst an. Dieses geschachtelte Literal enthält wiederum geschachtelte „Lücken“ für Country.Name und das berechnete Verhältnis für die Bevölkerungsdichte Country.Population/Country.Area sowie geschachtelte Elementlücken für Namen und Koordinaten der Hauptstadt.

Nach Kompilierung und Ausführung gibt die obige Abfrage das folgende XML-Dokument zurück (hier aus Platzgründen etwas kompakter formatiert).

<Countries>
 <Country Name="Palau" Density="0.037117903930131008">
   <Capital>
     <Name>Koror</Name><Longitude>135</Longitude><Latitude>8</Latitude>
   </Capital>
 </Country>
 <Country Name="Monaco" Density="16694.21052631579">
   <Capital>
     <Name>Monaco</Name><Longitude>7.2</Longitude><Latitude>3.7</Latitude>
   </Capital>
 </Country>
 <Country Name="Belize" Density="9.5512195121951216">
   <Capital>
     <Name>Belmopan</Name><Longitude>-88.5</Longitude><Latitude>17.1</Latitude>
   </Capital>
 </Country>
 <Country Name="Madagascar" Density="23.287181452711909">
   <Capital>
     <Name>Antananarivo</Name>
     <Longitude>47.4</Longitude><Latitude>-18.6</Latitude>
   </Capital>
  </Country>
</Countries>

Visual Basic 9.0 kompiliert XML-Literale in normale System.Xml.Linq-Objekte und gewährleistet dadurch eine volle Interoperabilität zwischen Visual Basic und anderen Sprachen, die LINQ to XML verwenden. Für die Beispielabfrage würde der vom Compiler produzierte Code (der allerdings nicht sichtbar ist) folgendermaßen aussehen:

Dim countriesWithCapital As XElement = _ 
  New XElement("Countries", _
        From country In Countries, city In Capitals _
        Where country.Name = city.Country _
  Select New XElement("Country", _
             New XAttribute("Name", country.Name), _
             New XAttribute("Density", country.Population/country.Area), _
             New XElement("Capital", _
               New XElement("Name", city.Name), _
               New XElement("Longitude", city.Longitude), _
               New XElement("Latitude", city.Latitude))))

Neben dem vereinfachten Erstellen von XML erleichtert Visual Basic 9.0 über XML-Eigenschaften auch den Zugriff auf XML-Strukturen. Bezeichner im Visual Basic-Code werden somit zur Laufzeit an entsprechende XML-Attribute und -Elemente gebunden. Die Bevölkerungsdichte sämtlicher Beispielländer kann somit folgendermaßen gedruckt werden:

  • Bei Verwendung der Child-Achse countriesWithCapital.<Country> werden alle „Country“-Elemente aus der XML-Struktur countriesWithCapital abgerufen.

  • Bei Verwendung der Attributachse country.@Density wird das Attribut „Density“ des Elements Country abgerufen.

  • Bei Verwendung der Nachfahren-Achse country...<Latitude> (im Quellcode als drei Punkte dargestellt) werden alle untergeordneten „Latitude“-Elemente des Country-Elements unabhängig von ihrer Hierarchietiefe abgerufen.

  • Bei Verwendung der Erweiterungseigenschaft .Value auf IEnumerable(Of XElement) wird das erste Element der Ergebnissequenz ausgewählt, bei Verwendung des Erweiterungsindexers (i) das i-te Element.

Durch eine Kombination all dieser Features kann der Code drastisch gekürzt und vereinfacht werden:

For Each country In countriesWithCapital.<Country>
  Console.WriteLine("Density = " & country.@Density)
  Console.WriteLine("Latitude = " & country...<Latitude>.Value)
Next

Der Compiler verwendet erfahrungsgemäß späte Bindung statt normaler Objekte, wenn der Zielausdruck einer Deklaration, Zuweisung oder Initialisierung vom Typ Object und nicht genauer angegeben ist. Der Compiler zieht zudem eine Bindung über XML vor, wenn der Zielausdruck vom Typ (bzw. eine Sammlung von) XElement, XDocument oder XAttribute ist.

Als Folge einer späten Bindung über XML übersetzt der Compiler folgendermaßen:

  • Der Ausdruck der Child-Achse countriesWithCapital.< Country> wird in den reinen LINQ to XML-Aufruf countrieswithcapital.elements("Country") übersetzt, der eine Sammlung aller untergeordneten Elemente mit dem Namen „Country“ vom Country-Element zurückgibt.

  • Der Ausdruck der Attributachse country.@Density wird in Country.attribute("Density").Value übersetzt, der das einzige untergeordnete Attribut namens „Density“ von Country zurückgibt.

  • Der Ausdruck der Nachfahren-Achse country...<Latitude> wird in den reinen LINQ to XML-Aufruf country.Descendants(“Latitude”) übersetzt, der die Sammlung aller Elemente zurückgibt, die unterhalb von Country genannt werden.

 

Abfragebegriffe

Ein Abfrageoperator ist ein Operator wie etwa Select, Order By oder Where, der über eine gesamte Wertesammlung hinweg gleichzeitig angewendet werden kann. Bei einem Abfrageausdruck handelt es sich um einen Ausdruck, der eine Reihe von Abfrageoperatoren auf eine bestimmte Sammlung anwendet. Der folgende Abfrageausdruck nimmt beispielsweise eine Ländersammlung als Grundlage und gibt die Namen aller Länder mit weniger als einer Million Einwohnern zurück:

Dim smallCountries = From country In Countries _
                     Where country.Population < 1000000 _
                     Select country

Die Syntax des Abfrageausdrucks ist in Anlehnung an die standardmäßige relationale SQL-Syntax konzipiert, sodass mit SQL vertraute Personen die Abfrageausdrücke ohne lange Einweisung verwenden können. Die Syntax wird jedoch von SQL nicht eingeschränkt, und die Abfrageausdrücke stellen auch keinerlei Übersetzung von SQL in Visual Basic dar. Da SQL auf Grundlage eines rein relationalen Modells konzipiert wurde, lassen sich einige Ausdrücke nur schlecht auf ein System anwenden, in dem Hierarchien erlaubt sind – und sogar bevorzugt werden. Darüber hinaus stehen einige syntaktische und semantische SQL-Elemente in Konflikt mit der bestehenden Visual Basic-Syntax oder -Semantik bzw. lassen sich nicht leicht in diese eingliedern. Es bestehen deshalb trotz der aus SQL bekannten Abfrageausdrücke dennoch Unterschiede, die erlernt werden müssen.

Die Abfrageausdrücke werden in Aufrufe an die zugrunde liegenden Sequenzoperatoren auf Typen übersetzt, über die Abfragen erstellt werden können, die als Quelltyp in der From-Klausel angegeben sind. Da Sequenzoperatoren im Allgemeinen als Erweiterungsmethoden zum Quelltyp definiert werden, sind sie an die im Bereich befindlichen Sequenzoperatoren gebunden. Das bedeutet, dass die Syntax des Abfrageausdrucks durch das Importieren einer bestimmten Implementierung erneut an andere LINQ-fähige APIs gebunden werden kann. So können Abfrageausdrücke erneut an eine Implementierung gebunden werden, die LINQ to SQL oder LINQ to Objects (ein lokales Abfrageausführungsmodul, das gespeicherte Abfragen ausführt) verwendet:

Einige Abfrageoperatoren wie etwa From, Select und Group by leiten eine besondere Art einer lokaler Variablen ein, die so genannte Bereichsvariable. Bei der Bereichsvariablen erfolgt standardmäßig eine Eingrenzung vom Einleitungsoperator aus bis zu einem Operator, der die Bereichsvariable beim Auswerten der Abfrage verbirgt und eine Eigenschaft oder Spalte der einzelnen Zeilen in einer Sammlung darstellt. Betrachten Sie das folgende Beispiel:

Dim smallCountries = From country In Countries _
                     Where country.Population < 1000000 _
                     Select country

Der Operator From leitet eine Bereichsvariable country vom Typ Country ein. Der folgende Abfrageoperator Where bezieht sich dann auf die Bereichsvariable country, um jeden einzelnen Kunde im Filterausdruck country.Population < 1000000 zu repräsentieren.

Manche Abfrageoperatoren, wie z. B. Distinct, verwenden oder ändern die Steuervariable nicht. Andere Abfrageoperatoren, wie z. B. Select, blenden die aktuellen Bereichsvariablen im Bereich aus und leiten neue Bereichsvariablen ein. Ein Abfragebeispiel:

Dim smallCountries = From country In Countries _
                     Select country.Name, Pop = country.Population
                     Order By Pop

Der Abfrageoperator Order By hat lediglich Zugriff auf die Bereichsvariablen Name und Pop, die vom Operator Select eingeführt wurden. Würde der Operator Order By auf country verweisen wollen, käme es zu einem Kompilierungsfehler.

Endet eine Abfrage ohne einen Select-Operator, ist der daraus resultierende Elementtyp der Sammlung so beschaffen, als ob bei allen Steuervariablen im Bereich eine Select-Projektion vorhanden wäre:

Dim countriesWithCapital = From country In Countries, city In Capitals _
                           Where country.Name = city.Country

Der ermittelte Typ für diese locale Deklaration ist (als Pseudocode-Darstellung eines anonymen Typen) IEnumerable(Of { Country As Country, City As City }).

Unterschiede zu SQL bei Abfrageausdrücken: Kompositionalität und hierarchische Daten

Abfrageausdrücke sind in Visual Basic 9.0 vollständig kompositionell. Das bedeutet, dass Abfragebegriffe beliebig geschachtelt oder aufgebaut werden können, indem eine Abfrage mit zusätzlichen Abfrageoperatoren angehängt wird. Durch Kompositionalität werden große Abfragen leichter verständlich, da die einzelnen Unterausdrücke isoliert betrachtet und nachvollzogen werden können. Außerdem sind Semantik und Typen, die mit den einzelnen Abfrageoperatoren einhergehen, besser nachvollziehbar. Als Entwurfsprinzip für das Schreiben einer Abfrage allerdings zeigt Kompositionalität deutlich andere Auswirkungen als SQL, wo Abfragen als monolithische Blöcke analysiert werden.

Darüber hinaus implementieren LINQ-fähige APIs die Sequenzoperatoren meist mit verzögerter Ausführung. Verzögerte Ausführung bedeutet, dass die Abfrage erst beim Auflisten der Ergebnisse ausgewertet wird. Für LINQ to SQL hat das zur Folge, dass die Abfrage erst dann an SQL weitergegeben wird, wenn die Ergebnisse abgefragt werden. Somit wird die Datenbank bei einer Trennung der Abfragen in mehrere Anweisungen nicht mehrfach angesprochen. Was in SQL in der Regel eine geschachtelte Abfrage wäre, wird in LINQ somit zu einer kompositionellen Abfrage.

Ein Grund für die mangelnde Kompositionalität in SQL besteht darin, dass das SQL zugrunde liegende relationale Datenmodell selber nicht kompositionell ist. So können Tabellen beispielsweise keine Untertabellen enthalten und müssen daher „flach“ sein. So verfassen SQL-Programmierer monolithische Ausdrücke, die als Ergebnis flache Tabellen aufweisen und damit dem SQL-Datenmodell entsprechen, anstatt komplexe Ausdrücke in kleinere Einheiten aufulösen. Da Visual Basic auf dem CLR-Typensystem beruht, gibt es keine Einschränkungen, welche Typen als Komponenten anderer Typen auftreten können. Mit Ausnahme der statischen Typisierungsregeln gibt es keine Einschränkungen bezüglich der Art der Ausdrücke, die als Komponenten anderer Ausdrücke auftreten können. Damit sind nicht nur Zeilen, Objekte und XML, sondern auch Active Directory, Dateien, Registrierungseinträge usw. ein wesentlicher Bestandteil in Abfragequellen und -ergebnissen.

Abfrageoperatoren

Programmierer, die mit der Implementierung von SQL vertraut sind, werden in den zugrunde liegenden .NET-Sequenzoperatoren viele der kompositionellen Operatoren mit relationaler Algebra wieder erkennen, wie z. B. Projektion, Selektion, Kreuzprodukt, Gruppierung und Sortierung, die Abfragepläne innerhalb der Abfrageprozessoren darstellen.

  • Der Operator From führt eine oder mehrere Bereichsvariable ein und gibt entweder eine Abfragesammlung an oder berechnet einen Wert für die Bereichsvariable.

  • Der Operator Select gibt die Form der Ausgabesammlung an.

  • Die Operatoren Where und Distinct schränken die Sammlungswerte ein.

  • Der Operator Order by gibt eine Sortierung für die Sammlung vor.

  • Die Operatoren Skip, Skip While, Take und Take While geben eine Teilmenge einer Sammlung auf Grundlage einer Reihenfolge oder Bedingung zurück.

  • Die Operatoren Union, Union All, Except und Intersect erstellen aus zwei Sammlungen eine Sammlung.

  • Der Operator Group By gruppiert die Sammlung auf Grundlage mindestens eines Schlüssels.

  • Die Operatoren Avg, Sum, Count, Min und Max aggregieren eine Sammlung und generieren einen Wert.

  • Die Operatoren Any und All aggregieren eine Sammlung und geben einen booleschen Wert auf Grundlage einer Bedingung zurück.

  • Der Operator Join erstellt aus zwei Sammlungen eine Sammlung auf Grundlage der entsprechenden Schlüssel, die aus den Elementen abgeleitet werden.

  • Der Operator Group Join führt eine Gruppenverbindung zweier Sammlungen auf Grundlage der entsprechenden Schlüssel durch, die aus den Elementen extrahiert wurden.

Die vollständige Syntax für alle Operatoren kann der umfassenden Sprachspezifikation entnommen werden. Aus Illustrationsgründen wird aber mit der folgenden Abfrage gezeigt, wie die Hauptstädte der einzelnen Länder gefunden und die Ländernamen nach Längengrad der Hauptstadt geordnet werden können:

Dim countriesWithCapital = _
  From country In Countries _
  Join city In Capitals On country.Name Equals city.Country _
  Order By city.Latitude _
  Select country.Name

Für Abfragen, die einen skalaren Wert auf Grundlage einer Sammlung berechnen, arbeitet der Operator Aggregate über die gesamte Sammlung. Die folgende Abfrage ermittelt die Zahl der kleinen Länder und berechnet deren durchschnittliche Dichte in einer Anweisung:

Dim popInfo = _
  Aggregate country In Countries _
  Where country.Population < 1000000 _
  Into Total = Count(), Density = Average(country.Population/country.Area)

Aggregatfunktionen kommen am häufigsten in Kombination mit der Partitionierung der Quellsammlung vor. So lassen sich z. B. alle Länder danach gruppieren, ob sie sich in den Tropen befinden. Die Zahl der einzelnen Gruppen wird anschließend aggregiert. Zu diesem Zweck können die Aggregatsoperatoren in Verbindung mit den Klauseln Group By und Group Join verwendet werden. Im unten angeführten Beispiel kapselt die Hilfsfunktion IsTropical den Test, ob eine Stadt ein Tropenklima aufweist:

Function IsTropical() As Boolean
  Return (TropicOfCancer =< Me.Latitude) AndAlso (Me.Latitude >= TropicOfCapricorn)
End Function

Geht man von dieser Hilfsfunktion aus, wird dieselbe Aggregation wie oben verwendet, zunächst aber die Eingabesammlung von Country- and Capital-Paaren in Gruppen partitioniert, für die Country.IsTropical gleich ist. In diesem Fall gibt es zwei solcher Gruppen: eine mit tropischen Ländern wie Palau, Belize und Madagaskar und eine andere, die das nicht tropische Land Monaco enthält.

Schlüssel

Land

Stadt

Country.IsTropical() = True

Palau

Koror


Belize

Belmopan


Madagaskar

Antanarivo

Country.IsTropical() = False

Monaco

Monaco

Danach werden die Werte in diesen Gruppen aggregiert, indem die Gesamtzahl und die durchschnittliche Bevölkerungsdichte berechnet wird. Der Ergebnistyp ist nun eine Paaresammlung von Total As Integer und Density As Double:

Dim countriesByClimate = _
  From country In Countries _
 Join city In Capitals On country.Name Equals city.Country _
 Group By country.IsTropical()
 Into Total = Count(), Density = Average(country.Population/country.Area)

Bei der oben angeführten Abfrage werden komplexe Angaben ausgeblendet. Die unten angeführte Abfrage führt mit Lambda-Ausdrücken und Erweiterungsmethoden zum Formulieren der Abfrage anhand der Methodenaufrufsyntax zu denselben Ergebnissen.

Dim countriesByClimate7 = _
  countries. _
    SelectMany( _
      Function(country) Capitals, _ 
      Function(country, city) New With {country, city}). _
    Where(Function(it) it.country.Name = it.city.Country). _ 
    GroupBy( _
      Function(it) it.city.IsTropical(), _
      Function(IsTropical, Group) _
        New With { _
          IsTropical, _
          .Total = Group.Count(), _
          .Density = Group.Average( _
             Function(it) it.country.Population / it.country.Area _
          ) _
        } _
    )

 

Erweiterungsmethoden und Lambda-Ausdrücke

Ein großer Teil der zugrunde liegenden Leistungsfähigkeit der Standard Query-Infrastruktur von .NET Framework ist auf Erweiterungsmethoden und Lambda-Ausdrücke zurückzuführen. Erweiterungsmethoden sind gemeinsam genutzte Methoden, die durch benutzerdefinierte Attribute gekennzeichnet sind. Durch diese Attribute wird ein Aufruf anhand der Instanzmethodensyntax ermöglicht. Die meisten Erweiterungsmethoden weisen ähnliche Signaturen auf. Das erste Argument ist die Instanz, auf die die Methode angewendet wird. Das zweite Argument ist das anzuwendende Prädikat. So weist z. B. die Where-Methode, in die die Where-Klausel übersetzt wird, die folgende Signatur auf:

Module IEnumerableExtensions
  <Extension> _
  Function Where (Of TSource) _
    (Source As IEnumerable(Of TSource), _
     predicate As Func(Of TSource, Boolean)) As IEnumerable(Of TSource)
    ...
  End Function
End Module

Da viele der standardmäßigen Abfrageoperatoren, wie z. B. Where, Select, SelectMany usw. als Erweiterungsmethoden definiert sind, die Delegaten des Typs Func(Of S,T) als Argumente übernehmen, macht der Compiler die Anforderung zum Erstellen von Delegaten, die das Prädikat darstellen, gegenstandslos. Der Compiler erstellt Closures, Delegaten, die ihren umgebenden Kontext aufnehmen, und leitet sie an den zugrunde liegenden Methodenaufruf weiter. Bei der folgenden Abfrage erstellt der Compiler z. B. Lambda-Ausdrücke, die die an die Select- und Where-Funktionen weiterzuleitenden Delegaten darstellen.

Dim smallCountries = From country In countries _
                     Where country.Population < 1000000 _
                     Select country.Name

Der Compiler generiert jeweils zwei Lambda-Ausdrücke für die Projektion und das Prädikat:

Function(Country As Country) country.Name
Function (Country As Country) country.Population < 1000000

Die oben angeführte Abfrage wird zudem in Methodenaufrufe für das Standard Query-Framework übersetzt und leitet Quell- und Lambdaausdrücke weiter, damit diese als Argumente verwendet werden können.

Dim smallCountries = _
  Enumerable.Select( _
      Enumerable.Where(countries, _
        Function (country As Country) country.Population < 1000000), _
        Function(country As Country) country.Name)

 

Nullfähige Typen

Relationale Datenbanken bieten die Semantik für nullfähige Werte, die häufig mit herkömmlichen Programmiersprachen nicht vereinbar sind und von wenigen Programmierern beherrscht werden. In datenintensiven Anwendungen ist es besonders wichtig, dass diese Semantik von den Programmen klar und korrekt ausgeführt wird. In Erkenntnis dieser Notwendigkeit wurde in .NET Framework 2.0 eine Laufzeitunterstützung für nullfähige Typen unter Einsatz des generischen Typs Nullable(Of T As Structure) hinzugefügt. Mit diesem Typ können nullfähige Versionen von Wertetypen, wie etwa Integer, Boolean, Date usw. deklariert werden. Aus gleich offensichtlich werdenden Gründen lautet die Visual Basic-Syntax für nullfähige Typen T?.

Da z. B. nicht alle Länder unabhängig sind, kann zu unserer Klasse Country ein neues Element hinzugefügt werden, in dem gegebenenfalls das Unabhängigkeitsdatum angeführt wird:

Partial Class Country
  Public Property Independence As Date?
End Class

Das Unabhängigkeitsdatum für Palau lautet #10/1/1994#. Die britischen Jungferninseln jedoch sind ein von Großbritannien abhängiges Gebiet, sodass als Unabhängigkeitsdatum Nothing angeführt ist.

Dim palau = _
  New Country With { _
    .Name = "Palau", _
    .Area = 458, _
    .Population = 16952, _
    .Independence = #10/1/1994# }

Dim virginIslands = _
  New Country With { _
    .Name = "Virgin Islands", _
    .Area = 150, _
    .Population = 13195, _
    .Independence = Nothing }

Visual Basic 9.0 unterstützt eine Dreiwertelogik und arithmetische Nullpropagierung für nullfähige Werte. Das heißt, dass als Ergebnis Nothing erscheint, wenn einer der Operanden arithmetischer, komparativer, logischer oder bitweiser Verschiebungen, Zeichenfolgen oder Typvorgängen Nothing aufweist. Verfügen beide Operanden über tatsächliche Werte, wird der Vorgang auf die Werte angewendet, die den Operanden zugrunde liegen, und das Ergebnis wird in einen nullfähigen Wert konvertiert.

Da sowohl Palau.Independence als auch VirginIslands.Independence den Typ Date? aufweisen, verwendet der Compiler arithmetische Nullpropagierung für die unten angeführten Subtraktionen, und der abgeleitete Typ für die lokale Deklaration PLength und VILength lautet deshalb TimeSpan?.

Dim pLength = #8/24/2005# - Palau.Independence ‘ 3980.00:00:00

Der Wert PLength beträgt 3980.00:00:00, da keiner der Operanden Nothing ist. Da der Wert VirginIslands.Independence andererseits Nothing ist, handelt es sich beim Ergebnis erneut um einen Typ TimeSpan?, wobei der Wert von VILength jedoch aufgrund der Nullpropagierung Nothing ist.

Dim vILength = #8/24/2005# - virginIslands.Independence ‘ Nothing

Wie bei SQL verwenden Vergleichsoperatoren Nullpropagierung, während logische Operatoren auf die Dreiwertelogik zurückgreifen. In If- und While-Anweisungen wird Nothing als False ausgelegt, sodass im folgenden Codeausschnitt die Else-Verzweigung übernommen wird:

If vILength < TimeSpan.FromDays(10000)
  ...
Else
  ...
End If

Gemäß der Dreiwertelogik werden die Gleichheitsprüfungen X = Nothing und Nothing = X immer als Nothing ausgewertet. Um zu überprüfen, ob X Nothing ist, sollte der zweiwertige Logikvergleich x Is Nothing oder Nothing Is X verwendet werden.

 

Lockere Delegaten

Beim Erstellen von Delegaten mittels AddressOf oder Handles in Visual Basic 8.0 muss eine der Methoden zur Bindung an Delegatbezeichner der Signatur des Delegattyps genau entsprechen. Im unten angeführten Beispiel muss die Signatur der OnClick-Unterroutine eine genaue Entsprechung der Signatur des Ereignishandlerdelegaten Delegate Sub EventHandler(sender As Object, e As EventArgs), die im Button-Typ im Hintergrund deklariert wird, darstellen.

Dim WithEvents btn As New Button()

Sub OnClick(sender As Object, e As EventArgs) Handles B.Click
  MessageBox.Show("Hello World from" & btn.Text)
End Sub

Beim Aufrufen von Nichtdelegatfunktionen und -unterroutinen benötigt Visual Basic die eigentlichen Argumente jedoch nicht, um eine genaue Entsprechung einer der aufzurufenden Methoden zu erzielen. Wie sich an folgendem Fragment zeigt, kann die OnClick-Unterroutine mit einem Argument des Typs Button und des Typs MouseEventArgs aufgerufen werden. Es handelt sich dabei jeweils um Untertypen der formalen Parameter Object und EventArgs:

Dim m As New MouseEventArgs(MouseButtons.Left, 2, 47, 11,0)
OnClick(btn, m)

Angenommen, man könnte umgekehrt eine Unterroutine RelaxedOnClick definieren, die zwei Object-Parameter übernimmt, dann könnte diese mit tatsächlichen Argumenten des Typs Object und EventArgs aufgerufen werden:

Sub RelaxedOnClick(sender As Object, e As Object) Handles btn.Click
  MessageBox.Show("Hello World from" & btn.Text))
End Sub
Dim e As EventArgs = m
Dim s As Object = btn
RelaxedOnClick(btn,e)

In Visual Basic 9.0 ist die Bindung an Delegaten locker, um mit dem Methodenaufruf konsistent zu bleiben. Das heißt, falls der Aufruf einer Funktion oder Unterroutine mit tatsächlichen Argumenten möglich ist, die den Formalparameter- und Rückgabetypen eines Delegaten genau entsprechen, kann diese Funktion oder Unterroutine an den Delegaten gebunden werden. Anders gesagt folgt die Delegatbindung und -definition derselben Überladungsauflösungslogik, die vom Methodenaufruf befolgt wird.

Das bedeutet, dass es in Visual Basic 9.0 nun möglich ist, eine Unterroutine RelaxedOnClick zu binden, die zwei Object-Parameter zum Click-Ereignis eines Button übernimmt:

Sub RelaxedOnClick(sender As Object, e As Object) Handles btn.Click
  MessageBox.Show(("Hello World from" & btn.Text)
End Sub

Die zwei Argumente des Ereignishandlers sender und EventArgs spielen dabei selten eine Rolle. Stattdessen greift der Handler auf den Zustand des Steuerelements zu, auf dem das Ereignis direkt registriert wurde, und ignoriert die beiden Argumente. Zur Unterstützung dieses häufig vorkommenden Falls können Delegaten gelockert werden, sodass sie keine Argumente annehmen, wenn daraus keine Zweideutigkeiten resultieren. Es kann also einfach Folgendes geschrieben werden:

Sub RelaxedOnClick Handles btn.Click
  MessageBox.Show("Hello World from" & btn.Text)
End Sub

Die Delegatlockerung wird außerdem beim Erstellen von Delegaten mithilfe von AddressOf bzw. eines Delegatenerstellungsausdrucks angewendet, selbst wenn es sich bei der Methodengruppe um einen Aufruf mit später Bindung handelt:

Dim F As EventHandler = AddressOf RelaxedOnClick
Dim G As New EventHandler(AddressOf btn.Click)

 

Schlussbemerkung

Visual Basic 9.0 vereinheitlicht den Datenzugriff unabhängig von der jeweiligen Quelle in relationalen Datenbanken, XML-Dokumenten oder beliebigen Objektdiagrammen und unabhängig von der Persistenz oder Speicherung. Die Vereinheitlichung umfasst Formate, Verfahren, Tools und Programmiermuster. Durch die besonders flexible Syntax von Visual Basic wird das Hinzufügen von Erweiterungen wie XML-Literalen und SQL-ähnlichen Abfrageausdrücken tief in die Sprache hinein erleichtert. Dadurch wird der Oberflächenbereich der neuen .NET-Language Integrated Query-APIs deutlich reduziert, die Auffindbarkeit von Datenzugriffsfeatures mithilfe von IntelliSense und Smarttags erhöht und das Debuggen sowie die Überprüfung zur Kompilierungszeit verbessert, indem fremde Syntax heraus aus Strings und hinein in Visual Basic integriert wird.

Darüber hinaus wird die Coderedundanz durch Features wie Typermittlung, Objektinitialisierer und lockere Delegaten maßgeblich reduziert und die Zahl der Ausnahmen zu Regeln, die Programmierer erlernen oder nachlesen müssen, ohne Einschränkung der Leistungsfähigkeit verringert.

Obwohl die Liste der neuen Features in Visual Basic 9.0 lang erscheinen mag, hoffen wir, dass die hier behandelten Themen Sie davon überzeugen, dass Visual Basic 9.0 in Kohärenz, Aktualität und Zielsetzung darauf hinwirkt, Visual Basic auch weiterhin zur weltweit besten Programmiersprache zu machen. Wir hoffen, Ihre Phantasie angeregt zu haben, und dass Sie mit uns gemeinsam zu der Überzeugung kommen, dass dies erst der Anfang einer Zukunft ist, die noch sehr viel mehr zu bieten haben wird.