Aufbruch zu neuen Ufern: C# 3.0

20.09.2005 von ULRICH PROELLER 
Der Vater von C#, Anders Hejlsberg, hat auf Microsofts Entwicklerkonferenz PDC die zukünftigen Erweiterungen von C# 3.0 vorgestellt. Damit wachsen C#, SQL und XML noch enger zusammen und eröffnen dem Entwickler ganz neue Möglichkeiten beim Zugriff auf Datenquellen.

Wer erwartet hatte, auf der diesjährigen Microsoft Professional Developers Conference in Los Angeles (12. bis 16. September) vieles über Visual Studio 2005 alias Whidbey zu erfahren, der wurde enttäuscht. Der Release-Termin vom 7. November 2005 wurde noch einmal bestätigt und der Release Candidate an die Teilnehmer verteilt. Ansonsten, so scheint es, ist über VS 2005, C# 2.0 und die CLR2 alles gesagt:

Die eigentlichen Themen auf der diesjährigen PDC waren aber, zumindest aus Entwicklersicht, die geplanten Erweiterungen für C# 3.0 und damit verbunden das Projekt LINQ. Anders Hejlsberg, der als führender Kopf schon Turbo Pascal bei Borland und die Foundation Classes sowie C# bei Microsoft entwickelt hat, ließ es sich nicht nehmen, den Entwicklern die Neuerungen persönlich vorzustellen.

Die Crux mit SQL und XML

LINQ steht für .Net Language Integrated Query. Gemeint ist damit die Möglichkeit, mit nativen C#- oder VB.Net-Sprachelementen Abfragen gegen relationale, XML-basierte oder auch völlig andere Datenquellen zu schreiben. Um die Tragweite von LINQ zu verstehen, muss man wissen, dass heutzutage die meisten Entwickler größerer Applikationen in zwei oder drei verschiedenen Welten zu Hause sein müssen:

Problematisch daran ist vor allem, dass die SQL- oder XQuery-Ausdrücke einfach als Text-String in einem C#-Programm stehen. Der Compiler kann die Ausdrücke also weder parsen noch den Anwender bei der Entwicklung unterstützen. Der Test solcher Programme gestaltet sich daher meist mühsam und zeitaufwendig. Der Entwickler kann erst zur Laufzeit durch Fehlermeldungen der Datenbank sehen, ob die SQL-Befehle in seinem Code syntaktisch richtig sind.

Ein weiteres Problem besteht darin, dass die nativen Datentypen in den .NET-Programmiersprachen und die SQL-Datentypen verschieden sind. Parameter und Ergebnisse von SQL-Aufrufen muss das Programm daher oft erst passend umwandeln. Dies geht manchmal ganz einfach, erfordert bisweilen jedoch auch einigen Aufwand, um alle möglicherweise vorkommenden Fälle sauber abzubilden.

Wenig flexible OR-Mapper

Aus diesem Grund wurden zahlreiche Objekt-Relationale (OR)-Mapper entwickelt, um die objektorientierte Programmiererwelt und die relationale Datenbankwelt zusammenzubringen. OR-Mapper sind leistungsfähige und meist umfangreiche Werkzeuge. Sie erlauben dem Programmierer, die Objekte seines Programms direkt in eine Datenbank zu schreiben oder aus ihr auszulesen. Dabei muss er theoretisch weder explizit Datenbanktabellen anlegen noch SQL-Code schreiben. In der Praxis haben diese OR-Mapper aber vor allem dann Probleme, wenn es um Performance und Flexibilität geht.

C# 3.0 geht nun einen ganz anderen und neuen Weg und integriert SQL in den Sprachumfang. Typische Sprachelemente aus SQL wie Select, Where oder OrderBy werden in C# zu Schlüsselwörtern. Ein Syntaxcheck kann schon beim Schreiben oder Kompilieren viele Fehler abfangen. Zudem vereinfachen sich datenbankabhängige Programme drastisch, wie folgendes Beispiel zeigt:

using System;
using System.Query;
using System.Collections.Generic;

class Test
{
static void Main
{
string[] cities = {"Amsterdam", "Brüssel", "Bonn", "Berlin", "Helsinki", "Oslo", "Moskau", "Kiew", "Warschau", "Prag", "Budapest", "Wien" };

IEnumerable<string> shortCities = from city in cities where city.Length < 5 orderby city select city.ToUpper();
foreach (string city in shortCities)
Console.WriteLine(city);
}
}

Die Ausgabe dieses Programms würde lauten:

BONN
KIEW
OSLO
PRAG
WIEN

Schon bei diesem einfachen Beispiel werden drei Punkte deutlich:

Verzögerte Ausführung

Die Ergebnisübergabe als Iterator ist besonders wichtig, denn er ermöglicht ein Feature namens Deferred Query Evaluation. Damit ist gemeint, dass die Abfrage erst in dem Moment ausgeführt wird, in dem die Ergebnisse auch benötigt werden. In unserem Beispiel erfolgt der Select also erst in der foreach-Schleife, in der die Ergebnisse auf der Console ausgegeben werden.

Diese verzögerte Ausführung der Abfrage löst ein zentrales Problem vieler OR-Mapper, nämlich deren großen Speicherbedarf und ihre geringe Performance. Würde die Abfrage sofort ausgeführt, so müsste das Ergebnis im Speicher gehalten werden. Bei Queries gegen große Datenbestände können so riesige Ergebnis-Sets entstehen.

Oft jedoch sind diese gar nicht nötig, da nur der erste oder die ersten n Datensätze tatsächlich ausgewertet werden. Aber selbst wenn alle zurückgegebenen Datensätze einer Abfrage verarbeitet werden, bringt die Deferred Query Evaluation Vorteile: C# 3.0 lädt die Ergebnisse nacheinander in den Speicher und nicht mehr alle gleichzeitig, so dass der benötigte Arbeitsspeicher minimal bleibt.

Notwendige Spracherweiterungen

Query-Anweisungen der oben gezeigten Art werden ausschließlich durch einige zusätzliche Sprachkonstrukte sowie durch eine Erweiterung des .NET-Frameworks um neue Namensräume (in unserem Beispiel „System.Query“) möglich. Die Common Language Runtime (CLR), die Microsoft zur Unterstützung von Generics für Visual Studio 2005 erheblich aufgebohrt hat, kann für LINQ unverändert bleiben. Deshalb läuft die auf der PDC 2005 veröffentlichte LINQ Preview auch problemlos mit der CLR 2.0 zusammen.

Im Prinzip könnten Query-Statements ganz ohne Erweiterungen von C# geschrieben werden. Die Konstrukte wären dann allerdings ziemlich unhandlich. Unser obiges Beispiel sähe mit dem im November 2005 erscheinenden C# 2.0 etwa so aus:

class TestOld
{
static void Main()
{
string[] cities = { "Amsterdam", "Brüssel", "Bonn", "Berlin", "Helsinki", "Oslo", "Moskau", "Kiew", "Warschau", "Prag", "Budapest", "Wien" };

Func<string, bool> filter = delegate(string s)
{
return s.Length < 5;
};

Func<string, string> extract = delegate(string s)
{
return s;
};

Func<string, string> project = delegate(string s)
{
return s.ToUpper();
};

IEnumerable<string> shortCities = cities.Where(filter) .OrderBy(extract) .Select(project);
foreach (string city in shortCities)
Console.WriteLine(city);
}
}

Vorausgesetzt, die Methoden Where, Select und OrderBy wären auf allen Collections sinnvoll definiert, so würde der obige Code unter C# 2.0 fehlerfrei übersetzen. Er ist funktional gleichbedeutend zu unserem ersten Codebeispiel, aber erheblich länger, komplizierter zu verstehen und weniger elegant.

Lambda Expressions

Durch die Einführung von so genannten Lambda Expressions in C# 3.0 wird es möglich, auf die explizite Definition der drei Delegates filter, extract und project zu verzichten. Stattdessen werden die entsprechenden Ausdrücke direkt als Argument den Methoden Where, OrderBy und Select übergeben. Die Lambda Expressions sind einfach eine abgekürzte Schreibweise für anonyme Delegates.

Für C# 3.0 wird es mehr als 30 solcher SQL-ähnlicher Methoden wie Where, OrderBy und Select () geben. Eine wichtige Voraussetzung für deren Einsatz ist, dass diese Methoden für alle Collections, Datenbanktabellen und XML-Klassen definiert sind. Dies klingt nach ziemlich viel Arbeit und wäre es auch, käme nicht die zweite Sprachneuerung von C# 3.0 ins Spiel: Extension Methods.

Extension Methods

Extension Methods sind statische Methoden in statischen Klassen, deren erstes Argument aber zusätzlich mit this gekennzeichnet ist.

namespace System.Query
{
using System;
using System.Collections.Generic;

public static class Sequence
{
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
{
if (predicate(item))
yield return item;
}
}
}
}

Das obige Codebeispiel zeigt eine Extension Method Where, die sich von einer normalen statischen Methode durch das this vor dem ersten Parameter unterscheidet. Das Besondere an einer Extension Method ist nun, dass sie nicht nur wie normale statische Methoden in der Form

IEnumerable<string> expr = Sequence.Where(names, s => s.Lenght < 5);

geschrieben werden kann, sondern auch als

IEnumerable<string> expr = names.Where(s => s.Lenght < 5);

Sparsamer Gebrauch ist angeraten

Extension Methods erweitern somit den Kontrakt für bestehende Klassen, ohne dass der Code der bestehenden Klassen selbst verändert werden muss. Beim Übersetzen wird geprüft, welche Namespaces sich im aktuellen Scope befinden. Die in diesen Namespaces definierten Extension Methods werden vom Compiler aufgelöst. Um die im obigen Beispiel definierte Where-Methode also zu aktivieren, schreibt man im entsprechenden Code:

using System.Query;

Extension Methods stellen ein mächtiges Werkzeug dar und es besteht natürlich die Gefahr, dass sie nicht sinnvoll eingesetzt werden. Anders Hejlsberg, der Architekt von C#, empfahl daher bei der Vorstellung von C# 3.0 auf der PDC, Extension Methods äußerst sparsam zu verwenden. Der Programmierer sollte sie nur dort einsetzen, wo der gewünschte Zweck mit anderen Sprachmitteln nicht erreicht werden kann.

Der letzte Schritt, der jetzt noch zu tun ist, um den im ersten Beispiel gezeigten Code übersetzen zu können, ist die Definition der Schlüsselwörter from, where, select, orderby etc. und deren Zuordnung zu entsprechenden Extension Methods.

Fazit

Die gezeigten Spracherweiterungen für C# 3.0 ermöglichen eine universale und erweiterbare Sprache zur Abfrage und Veränderung von Daten jeder Art. Zugleich gewährleistet der eingeschlagene Weg eine leichte Erweiterbarkeit.

Sollte jemand mit den angebotenen Filtern oder Projektoren im Hinblick auf Funktionsumfang oder Performance nicht zufrieden sein, so kann er auf einfache und transparente Weise eigene Methoden für seine Zwecke entwickeln. Die eigentliche Arbeit der Anbindung relationaler und XML-basierter Datenquellen erfolgt in eigenen Assemblies im zu WinFX erweiterten .NET Framework.

Die Erweiterung des Frameworks wird die Entwickler in Redmond wohl noch bis mindesten Ende 2006 beschäftigen. Aber die Geduld, bis dahin zu warten, wird belohnt mit einer neuen, nativ objektorientierten Art, Daten abzufragen und zu bearbeiten. Und dies wird vielen Programmierern das Leben und die tägliche Arbeit deutlich erleichtern. (ala)