Neuerungen in C# 3.0

C# in der Version 3.0 (auch bekannt als Orcas) weist einige neue Spracherweiterungen auf, mit denen das Arbeiten signifikant erleichtert wird. Viele der neuen Techniken erlauben eine höhere Abstraktion des Codes was wiederum die Wiederverwendbarkeit steigert. Desweiteren wird eine größere Flexibilität geboten die nicht auf Kosten der Typsicherheit geht.

Um die neue Version zu benutzen, benötigen Sie Visual Studio 2005 mit der c# 3.0 Erweiterung oder aber Visual Studio 2008. Dort sind bereits alle benötigten Bibliotheken vorhanden.

Typinferenz

Bislang musste beim Deklarieren einer Variable immer der passende Typ angegeben werden, der zum Inhalt kompatibel ist.

   1:  Int n = 5;
   2:  String x = „Hallo“; 

Durch die Einführung des Schlüsselwortes var hat sich das geändert. Sie können anstelle des Typen nun folgende Schreibweise anwenden:

   1:  Var n = 5;
   2:  Var  x = „Hallo“; 

Auf den ersten Blick erinnert das sehr an die alten COM Zeiten mit dem Variant Datentyp, der zur Laufzeit gebunden wird. Dies hat zu vielen Problemen (gerade beim Endbenutzer) geführt. In Wirklichkeit wird auch bei Verwendung von var die Typsicherheit bewart und nicht zur Laufzeit ausgewertet. Beim Kompilieren des Codes, wird geprüft welcher Datentyp am besten zum Inhalt passt und entsprechend verwendet. Im oberen Beispiel wäre die Variable n vom Typ int, die Variable x vom Typ string. Das lässt sich mit folgendem Codeschnipsel leicht bestätigen:

   1:  Int x = 5;
   2:  Var a=4;
   3:  Var b=“hallo“;
   4:  Int c = x+a; //funktioniert
   5:  Int d = x+b; //funktioniert nicht!! Da b vom Typ string ist

Der var-„Typ“ kann also dazu verwendet werden eine Variable zu deklarieren ohne den Datentypen selbst zu ermitteln. Das nennte man „Implizit typisierte lokale Variablendeklaration“. Jedoch rate ich davon ab, auf eigene Variablendeklarationen komplett zu verzichten und nur noch mit var zu arbeiten. Ein Nachteil ist, dass die Lesbarkeit des Codes abnimmt, da man nicht mehr auf den ersten Blick sieht um welchen Datentypen es sich handelt. Welche Vorteile sich daraus ergeben und in welchen Bereichen das Schlüsselwort Verwendung findet, werden Sie in den folgenden Abschnitten erkennen. Jedoch hat das var-Schlüsselwort auch Einschränkungen. Es darf nur zum deklarieren von lokalen Variablen verwendet werden. Rückgabewerte oder Parameter für Methoden, Klassen-Member, usw. sind nicht erlaubt. Desweiteren darf nicht null zugewiesen werden.

Lambda-Ausdrücke

Lambda-Ausdrücke sind zu vergleichen mit Funktionszeigern in c# 2.0. Das folgende einfache Beispiel, soll Ihnen die bisherige Verwendung von Funktionszeigern veranschaulichen:

   1:  List<string> myNames = new List<string>();
   2:  myNames.Add("Marcel");
   3:  myNames.Add("Sarah");
   4:  myNames.Add("Tobi");
   5:  myNames.Add("Andi");
   6:  //Alte Syntax
   7:  List<string> filtered1 = myNames.FindAll(FindMethod);
   8:   
   9:  private static bool FindMethod(string s)
  10:  {
  11:      return (s.Length == 6);
  12:  }

Als erstes wird eine generische Liste mit verschiedenen Namen definiert. Anschließend rufen wir die FindAll Methode auf, die einen Zeiger zu einer Funktion verlangt. Für jedes Element in der Liste wird unsere Methode FindMethod aufgerufen. Das Ergebnis wird in die Variable filtered1 gespeichert. Das gleiche Ergebnis mit deutlich verkürzter Syntax lässt sich mit Hilfe von Lambda-Ausdrücken erzielen.

   1:  List<string> myNames = new List<string>();
   2:  myNames.Add("Marcel");
   3:  myNames.Add("Sarah");
   4:  myNames.Add("Tobi");
   5:  myNames.Add("Andi");
   6:  //Lambda-Ausdruck
   7:  List<string> filtered2 = myNames.FindAll((s) => s.Length == 6);

Der Unterschied besteht darin, dass die aufzurufende Funktion nun direkt angegeben werden kann. Lassen Sie uns den Ausdruck nochmals genauer anschauen:

FindAll((s) => s.Length == 6)

Die Methode FindAll bekommt folgenden Parameter:

(s) => s.Length == 6

(s) ist der Parameter, welcher in der Funktion FindAll als delegate parameter hinterlegt ist. Sollten mehrere Parameter erforderlich sein, können diese durch kommagetrennt angegeben werden.

=> Das ist der Lambda Operator, der angibt dass auf der rechten Seite nun die aufzurufende Funktion kommt.

s.Length == 6 Ist der eigentliche Inhalt der Funktion. Sie könnten natürlich auch ein return davor schreiben. Also return(s.Length == 6). Da es sich aber um eine einzelen Zeile handelt, kann man das return und die geschweiften Klammern weglassen.

Wenn man die Lambda-Ausdrücke das erste Mal benutzt, scheint es etwas kompliziert zu sein. Nach einer kurzen Einarbeitung erkennt man aber deren Vorteil - schneller anonyme Methoden zu schreiben.

Erweiterungsmethoden

Mit dem Konzept der Erweiterungsmethoden lassen sich Klassen ohne Ableitung mit Funktionen erweitern. Im folgenden Beispiel wird der Typ int (ist im Prinzip auch eine Klasse bzw. Struct) durch die Methode AddNumber() erweitert.

   1:  public static class MyExtensions
   2:  {
   3:      public static int AddNumber(this int n, int number)
   4:      {
   5:          return (n + number);
   6:      }
   7:  }

Im Code kann man die Methode nun sofort verwenden:

   1:  int myNumber = 5;
   2:  int y = myNumber.AddNumber(40);
   3:   

Die Variable y besitzt nun den Wert 45. Da das Prinzip der Erweiterungsmethoden nun geklährt ist, schauen wir uns das Beispiel nochmal genauer an. Um eine Erweiterungsmethode zu schreiben, ist es erfolderlich diese in eine öffentliche und statische Klasse zu implementieren. Die eigentliche Methode muss ebenfalls Öffentlich und statsisch sein. Um eine Methode nun endgültig als Erweiterungsmethode zu kennzeichnen, muss man – wie Ihnen vermutlich schon aufgefallen ist – im ersten Parameter das Schlüsselwort this vor den Datentypen schreiben, den man erweitern möchte. Das schöne an Erweiterungsmethoden ist, dass man auch generics angeben kann. Dies erhöht die Flexibilität enorm. Das folgende Beispiel ist also föllig legitim:

   1:  public static class MyExtensions
   2:  {
   3:      public static void WriteType<T>(this T obj)
   4:      {
   5:          System.Diagnostics.Debug.WriteLine(obj.ToString());
   6:      }
   7:  }

Gerade LINQ macht von Erweiterungsmethoden exzessiv Gebrauch um die Lesbarkeit der Query zu optimieren.

Initialisierung von Objekten

Eine weitere syntaktische Neuerung die den Alltag des Programmierers erleichtern soll, ist die neue Art der Objektinitialisierung. Wollte man bislang ein Objekt initialisieren und gleichzeitig Eigenschaften setzen, konnte man nur hoffen das der Konstruktor eine passende Überladung besitzt, der alle gewünschten Parameter zur Verfügung stellt. War dies nicht der Fall, musste man nach der Initialisierung die Eigenschaften separat setzen. So wie im folgendem Beispiel zu sehen:

   1:  Order myOrder = new Order();
   2:  myOrder.OrderID = 4;
   3:  myOrder.OrderInfo = "Test";

Ab C# 3.0 lassen sich direkt bei der Initialisierung öffentliche Eigenschaften setzen.

   1:  Order myOrder = new Order { OrderID = 4, OrderInfo = "Test" };

Der Compiler verwendet dafür den Standardkonstruktor und setzt anschliessend die Eigenschaften. Erst wenn alle Eigenschaften gesetzt sind, wird dieses Objekt für andere Threads sichtbar. Natürlich kann auch zusätzlich ein Konstruktor verwendet werden (Sofern vorhanden):

   1:  Order myOrder1 = new Order(5) { OrderInfo = "Test" };

Die Syntax zur Initialisierung kennt praktisch keine Grenzen. Selbst verschachtelte Anweisungen sind möglich.

   1:  Order myOrder2 = new Order(5) { OrderInfo = "Test", Details= new OrderDetails{ ID=1, Product="AFKI"}};

Die Klasse Orders besitzt ein weiteres Unterobjekt OrderDetails, welches auf die gleiche Weise Initialisiert werden kann.

Anonyme Typen

Gerade haben wir eine neue Möglichkeit kennengelernt Objekte zu initialisieren. Dabei musste man den Typen der Deklariert werden soll, explizit angeben:

   1:  Order myOrder = new Order { OrderID = 4, OrderInfo = "Test" };

Mit dem Einsatz von anonymen Typen ist das nicht mehr zwingend notwendig. Mit dieser Anweisung wird ein neuer anonymer Typ erstellt:

   1:  var myNewType = new { OrderID = 4, OrderInfo = "Test" };

Die Variable myNewType kann sofort (auch mit IntelliSense) verwendet werden.

   1:  myNewType.OrderID = 5;

Der Kompiler erzeugt beim Kompilieren für jedes angegeben Argument eine öffentliche Eigenschaft und ein privates Feld. Definieren Sie Typen, deren Argumente gleich sind (gleicher Name und gleiche Reihenfolge), wird der anonyme Typ doppelt vergeben

   1:  var myNewType = new { OrderID = 4, OrderInfo = "Test", this.Text}; //Type: <>f__AnonymousType0`3
   2:  var myNewType2 = new { OrderID = 5, OrderInfo = "Test", this.Text }; // Type: <>f__AnonymousType0`3
   3:  var myNewType3 = new { OrderInfo = "Test", OrderID = 5, this.Text }; // Type: <>f__AnonymousType1`3

Da die Argumenten Reihenfolge des letzten Typen anders ist als die beiden oberen, wird ein neuer Typ erstellt. Hier z.B. „<>f__AnonymousType1`3“.
Bei LINQ-Abfragen spielen anonyme Typen eine zentrale Rolle.