Neuerungen C# 3.0

Das .NET-Framework

C# 2.0

C# 3.0

Fazit

Quellen/Links

Anfang

implizit getypte lokale Variablen

In C# kann man den Compiler entscheiden lassen, welchen Typ eine Variable haben soll. Dazu lässt man bei der Deklaration den Typen weg und ersetzt ihn durch das Schlüsselwort "var". Der Compiler ermittelt dann den Variablentyp aus dem Kontext und behandelt sie dann, als wäre es eine Variable dieses ermittelten Typs:

var i = 5; entspricht int i = 5;
var s = "Ulrich Kratz"; entspricht String s = "Ulrich Kratz";
var d = 1.0; entspricht double d = 1.0;

Bei diesem Konzept gibt es jedoch einige Restriktionen:

  • Variable muss eine lokale sein (z.B. nicht in Klasse, Struct,...)
  • Initialisierung muss bei der Deklaration erfolgen, sprich zur Compile-Zeit feststehen
  • Typ darf kein null-Typ sein
  • Bei implizit getypten lokalen Arrays sehen die Deklarationen wie folgt aus:

    var ia = new[] {1, 2, 3}; entspricht int[] ia = new int[] {1, 2, 3};
    var da = new[] {1, 2.0, 3}; entspricht double[] da = new int[] {1, 2.0, 3};

    Dabei muss darauf geachtet werden, dass alle Elemente im Initializer kompatibel sind. Sollte man das explizit nicht wünschen (Strings und doubles), muss das Array explizit als new Object[] deklariert werden.

    top

    Erweiterungsmethoden

    Eine Erweiterungsmethode ist eine statische Methode, die einer Klasse genau diese Methode hinzufügt, obwohl sie nicht in dieser deklariert wurde.

    Angenommen, es gibt eine Klasse A, dann sähe eine Erweiterungsmethode zu dieser Klasse wie folgt aus:

    public static void foo(this A bar){...},

    wobei diese Methode in einer anderen Klasse steht, die auch in einem anderen Namensraum liegen kann. Der Aufruf sähe dann wie folgt aus:

    A bla = new A();
    bla.foo();

    Sollte die Erweiterungsmethode in einem anderen Namensraum liegen, so muss man diesen über die using-Klausel bekannt machen.
    Wenn eine passende Methode (Name, Parameterliste und Rückgabetyp) in der instanziierten Klasse bereits existiert, so wird diese in jedem Fall ausgeführt.
    Wenn zwei genau gleiche Erweiterungsmethoden (Name, Paramaterliste und Rückgabetyp) existieren und diese Methode als Erweiterungsmethode aufgerufen wird, gibt es einen Compiler-Fehler.

    Der Aufruf geschieht, wie oben gesehen, unter Auslassung des ersten mit this markierten Parameters, wird vom Compiler aber so umgesetzt, dass das Objekt, aus dem die Methode aufgerufen wird, als erster Parameter übergeben wird:

    foo(bla);

    Man kann die Methode foo aber nicht selbst so aufrufen.

    Wie man sich sicherlich denken kann, führt diese Art dazu, dass die Programme sehr unstrukturiert und praktisch unwartbar werden und diese nur dann eingesetzt werden sollten, wenn es gar keine andere Möglichkeit gibt. Ich habe jedoch keinen zwingenden Grund gefunden, dieses Konzept einzusetzen.
    Sehr unterhaltsam finde ich in diesem Zusammenhang folgendes Zitat aus der Spezifikation zu C# 3.0 (!!!) von Anders Hejlsberg(!!!!!):

    Note
    Extension methods are less discoverable and more limited in functionality than instance methods. For those reasons, it is recommended that extension methods be used sparingly and only in situations where instance methods are not feasible or possible.

    Und als ob das noch nicht genug wäre, schickt er noch eine kleine Drohung hinterher:

    Extension members of other kinds, such as properties, events and operators, are being considered but are currently not supported.


    top

    Lambda-Expressions

    Lambda-Expressions ähneln vom Konzept her den bislang schon bekannten anonymen Methoden, nur mit dem Unterschied, dass man als Body entweder eine Expression oder ein Statement-Block sein kann, wobei bei einer anonymen Methode der Body nur ein Statement-Block sein kann.

    Sie beginnen mit einer in runden Klammern eingefassten Parameterliste, bei denen man die Klammern dann weglassen kann, wenn es genau einen ungetypten Parameter gibt. Gefolgt wird die Parameterliste von dem Terminalsymbol "=>" und entweder einer Expression oder einem Statementblock:

    x => x+1 entspricht delegate (int x){return x + 1}
    (x, y) => {return x + y;} entspricht delegate (int x, int y){return x + y;}
    (String s) => {Console.WriteLine(s);} entspricht
    delegate (String s){Console.WriteLine(s);}

    Eine Lambda-Expression kann man überall dort einsetzen, wo eine anonyme Methode gestattet ist (typischerweise in Verbindung mit einem Delegate), da man einer Lambda-Expression keinen Namen geben kann wie einer normalen Methode.

    Eine Lambda-Expression L ist zu einem Delegate D kompatibel, wenn folgende Bedingungen erfüllt sind:

  • D und L haben die gleiche Parameteranzahl
  • Wenn L einen expliziten Parameter enthält, müssen die entsprechenden Typen sowie Übergabetypen identisch sein.
  • Wenn L einen impliziten Parameter enthält, darf der entsprechende Parameter in D kein ref- oder out-Parameter sein
  • Wenn der Rückgabetyp von D void ist und der Body von L eine Expression ist, ist dieser korrekt, wenn er eine Statement-Expression ist (In-, Dekrement, Zuweisung, Objekterzeugung, Aufruf).
  • Wenn der Rückgabetyp von D void ist und der Body von L ein Statement ist, ist dieser korrekt, wenn er kein return enthält.
  • Wenn der Rückgabetyp von D nicht void ist und der Body von L eine Expression ist, ist dieser korrekt, wenn er zu dem Rückgabetyp von D implizit kompatibel ist.
  • Wenn der Rückgabetyp von D nicht void ist und der Body von L ein Statement ist, ist dieser korrekt, wenn jedes return einen zum Rückgabetyp von D kompatiblen Wert zurückliefert.
  • Die Typinferenz einer Lambda-Expression L läuft nach den folgenden Regeln ab:

  • Wenn der Body von L eine Expression ist, ist der Typ von L der Typ der Expression.
  • Wenn der Body von L ein Statement ist, ist der Typ von L der Typ, der von allen return-Anweisungen zurückgeliefert wird, sofern sie alle vom selben sind.
  • Sollte keine der Bedingungen zutreffen, so kann kein Typ ermittelt werden.

  • top

    Object- und Collection-Initializer

    Object-Initializer dienen dazu, Felder oder Properties eines Objekts bei der Erzeugung mit Werten zu belegen. Dazu müssen diese jedoch öffentlich zugänglich sein:

    A a = new A(){ foo = "Joe Turpin", bar = 3};

    In obigem Beispiel wird ein Objekt der Klasse "A" erzeugt, das zwei Felder oder Properties namens foo (String) und bar (int o.ä.) enthält, die jeweils mit dem Wert "Joe Turpin" und 3 belegt werden.

    Mit einem Collection-Initializer kann man eine Collection (ein Typ, der das Interface System.Collections.Generic.ICollection<T> implementiert, z.B. List<T>) initialisieren:

    List<T> intList = new List<T>(){1, 3, 5, 2};

    In obigem Beispiel wird die Liste mit den Werten 1, 3, 5, 2 belegt wird, dergestalt, dass intern für jeden Wert im Initializer einmal ICollection<T>.Add<T> aufgerufen wird.

    top

    anonyme Typen

    Ähnlich zu den bereits bekannten anonymen Methoden, wird C# 3.0 auch die Möglichkeit bieten, anonyme Typen zu erzeugen. Dazu eignet es sich, diesen einer implizit getypten lokalen Variablen zuzuweisen, da man bei Object nicht auf das Objekt zugreifen kann, wie es bei der anderen Art der Fall ist:

    var foo = new {name = "Jürgen Sacher", lage = "Tenor"};
    Console.WriteLine(foo.name);

    Es wird dabei ein rudimentäres Objekt erzeugt, in dem für jeden im Initializer stehenden Bezeichner (hier: name, lage) ein Feld des entsprechenden Typen angelegt wird, auf das man über eine Property des angegebenen Namens (hier: name, lage) zugreifen kann.

    Man kann auch die Initialisierung weglassen, wenn der Bezeichner ein Feld ist. Dann wird auch eine Property des Bezeichners angelegt und das Feld erhält den Typ der Variablen:

    String name = "Lucio Gallo";
    String lage = "Bariton";
    var foo = new { name, lage };

    Zwei anonyme Typen sind zueinander kompatibel, wenn die Bezeichner den selben Namen haben und in der selben Reihenfolge deklariert werden. Man kann allerdings nicht ein Objekt der anonymen Klasse selbst explizit erzeugen, da diese vom Compiler angelegt wird.

    top

    Query-Expressions

    Nachdem C# 3.0 ja bereits mit den Lambda-Expressions einen kleinen Abstecher in die Welt der funktionalen Programmierung unternommen hat, wird es künftig auch die Möglichkeit geben, mit SQL-artigen Abfrage-Ausdrücken bestimmte Werte aus einem oder mehreren Elementen, die das Interface IEnumerable<T> implementieren (z.B. List<T>, Arrays), herauszufiltern.

    Die Syntax erinnert wie gesagt an SQL-Statements, wobei eine Query-Expression zwingend mit einer from-Klausel beginnt, die von einer oder mehreren from-, let- oder where-Klauseln gefolgt werden kann. Eine Query-Expression endet zwingend mit einer select- oder groupby-Klausel.

    Die wichtigsten Klauseln im Überblick:

  • from: spezifiziert eine Laufvariable über ein bestimmtes IEnumerable<T>:
    from p in Personen
  • join: verknüpft eine Tabelle mit einer zweiten:
    join w in Wohnorte on p.Id equals w.Id
  • let: speichert einen Wert in einer Variable zwischen:
    let foo = 8
  • where: schränkt die ermittelten Werte nach gewissen Bedingungen ein:
    where w.Ort == "Hamburg"
  • orderby: sortiert nach bestimmten Feldern:
    orderby w.Ort descending
  • group: gruppiert das Ergebnis nach gewissen Regeln:
    group w by w.Ort
  • select: sucht die gewünschten Felder aus der Abfrage:
    select new { Ort = w.Ort }
  • zum Beispiel:

    List<Person> Personen = new List<Person>{
       new Person("Kratz", "Ulrich", 0),
       new Person("Tsymbalyuk", "Alexander", 1),
       new Person("Young", "Simone", 1),
       new Person("Hulett", "Benjamin", 1),
       new Person("Quinn", "Gerard", 2),
       new Person("Busert", "Patrick", 2)
       };
    List<Wohnort> Wohnorte = new List<Wohnort>{
       new Wohnort(0, "Lüneburg"),
       new Wohnort(1, "Hamburg"),
       new Wohnort(2, "Lübeck")
       };
    var res = from w in Wohnorte
       join p in Personen on w.Id equals p.Id into g
       where g.Count() >= 2
       select new { w.Ort, cnt = g.Count() };

    In obigem Beispiel sehen wir eine Abfrage, die aus zwei Tabellen zunächst eine Verknüpfung bildet, diese mit into gruppiert in der Variable g zwischenspeichert, auf der dann eine Methode Count() definiert ist, die aus dem gruppierten Ergebnis die Anzahl der Einträge zurückliefert.

    Mit anderen Worten: Die Abfrage ermittelt alle Orte, in der mehr als 2 dieser tollen Sänger wohnen. Es wird ein neues Objekt erzeugt, in dem die entsprechden Orte und die Anzahl der dort lebenden Sänger in einem IEnumerable<T> gespeichert wird.

    Vom Compiler werden die Query-Expressions letztendlich in die allgemein bekannte Punkt-Notation übersetzt. So würde der obige Ausdruck in etwa in folgenden Ausdruck übersetzt:

    var res = Wohnorte.GroupJoin(Personen, w => w.Id, p => p.Id,
         (w, g) => new { w, g }).
       Select(x => new { x, t = x.g.Count() }).
       Where(y => y.t >= 2).
       Select(y => new { y.x.w.Ort, cnt = y.t });

    Dem Programmierer steht es dabei offen, welche Notation ihm am meisten behagt.

    top

    andere Projekte im LINQ-Projekt

    Im Rahmen des DLINQ-Projekts, besteht die Möglichkeit, auch eine reale Datenbank anstatt eines o.e. Elements zu verwenden.

    Das XLINQ-Projekt gestattet das Arbeiten mit XML-Daten.

    top