Tipps zum Einsatz der neuen Sprachfeatures von C#

Die Top-6-Sprachfeatures in C# 6
Kommentare

Mit C# 6.0 hat Microsoft endlich einmal wieder eine neue Sprachversion vorgestellt, die eine Reihe von neuen Sprachfeatures mitbringt. Strategische Einschränkungen gab es bei der Gestaltung trotzdem noch: Nur solche Features haben es letztlich in diese Version geschafft, die Vereinfachungen bestehender Syntax darstellen, also dem Entwickler das Leben einfacher machen und etwas Tipparbeit ersparen. Man hat sich dagegen entschieden, manche anderen guten Ideen umzusetzen, die „richtiges“ Neuland erschlossen hätten. Dies könnte in zukünftigen Versionen nachgeholt werden, aber dazu gibt es bisher noch keine Versprechungen.

Die Liste aller Neuerungen in C# 6 umfasst je nach Interpretation etwa 11 Features:

  1. Initializer für Auto Properties
  2. Auto Properties ohne Setter
  3. Funktionen mit Expression Bodies
  4. „using static“
  5. Der „Null Conditional Operator“
  6. String-Interpolation
  7. „nameof“
  8. Initializer für Collections mit Indexzugriff
  9. Exception-Filter
  10. Collection-Initializer mit Add-Extension-Methoden
  11. Await in catch– und finally-Blöcken

Auf die ersten sechs Neuheiten in dieser Liste soll im Folgenden eingegangen werden. Alle diese Features bieten praktische Arbeitserleichterung, aber in manchen Fällen gibt es noch einiges mehr zu den Details und Hintergründen sowie potentiellen Einsatzzwecken zu sagen.

Auto-Properties

Der erste Punkt ist schnell abgedeckt: Auto-Properties können nun direkt mit Werten initialisiert werden.

 public int AutoInteger { get; set; } = 42;

Beachten Sie, dass die Syntax am Ende der Zeile ein Semikolon vorsieht, logisch und üblich bei Zuweisungen, aber auch etwas ungewöhnlich im Anschluss an eine Deklaration mit geschwungenen Klammern.

Besonders wichtig wird die Möglichkeit der Initialisierung, wenn das zweite neue Feature zum Einsatz kommt: Auto-Properties ohne Getter:

 public int AutoReadOnlyInteger { get; } = 42;

Natürlich können die Werte für Auto-Properties auch nach wie vor im Konstruktor einer Klasse initialisiert werden. Interessanterweise gilt dies auch, wenn die Property keinen Getter hat:

public class WeekStartDay {
  public string Day { get; }
  
  public WeekStartDay() {
    Day = "Monday";
  }
}

Diese Neuheiten sind nicht revolutionär, vervollständigen aber das Bild bei der Arbeit mit automatischen Properties. Das alte Pattern, in dem eine „readonly“ Property mithilfe eines privaten Setters simuliert wurde, ist somit endlich hinfällig.

Funktionen mit Expression Bodies

Die Idee ist wiederum einfach: eine einfachere Syntax sollte her zur Erstellung von Methoden in Klassen. Das neue Feature ist eng an Lambda-Ausdrücke angelehnt. Zum Vergleich hier ein Stück Code, mit dem bisher auf Klassenebene ein Lambda-Ausdruck verwendet werden konnte:

public class Logic {
  public Func<int, int, int> Add {
    get {
      return (x, y) => x + y;
    } 
  }
}

Dieser Code verwendete eine Property, um einen Lambda-Ausdruck zurückzugeben. An dieser Stelle soll nicht weiter auf die Gründe für eine solche Entscheidung eingegangen werden, aber mithilfe eines solchen Konstrukts ließ sich dann der Ausdruck etwa so auswerten:

Console.WriteLine(Add(10, 20));

Damit erhielt der Programmierer ein Element in der Klasse, das beim Aufruf aussah wie eine Methode, technisch jedoch keine war. Natürlich bleibt diese Möglichkeit auch in C# 6 bestehen, aber nun gibt es außerdem auch folgende Syntax:

public class Logic {
  public int Add(int x, int y) => x + y;
}

Es sei ganz deutlich gesagt, dass der Compiler hier eine normale Methode erzeugt. Das Ziel ist es lediglich, dem Programmierer einen kürzeren Ausdruck verfügbar zu machen. Die Implementation der Klasse Logic in diesem Beispiel ist auf IL-Ebene nicht von dieser zu unterscheiden:

public class Logic {
  public int Add(int x, int y) {
    return x + y;
  }
}

Die Bezeichnung des Features „… mit Expression Bodies“ weist auf eine wichtige Einschränkung hin: es dürfen keine geschwungenen Klammern in dem Ausdruck verwendet werden, um einen Statement Body zu erzeugen. Die Syntax kann somit nur verwendet werden, wenn die fragliche Funktion tatsächlich nichts anderes tut, als einen Rückgabewert zu erzeugen – oder noch nicht einmal das, wie hier:

public void Output(string x) => Console.WriteLine(x);

Die Regeln für die Gültigkeit des Expression Bodies sind dieselben, die schon immer bei Lambda-Ausdrücken in C# angewandt wurden. Es können grundsätzlich alle Arten von Methoden auf diese Art implementiert werden, so wie hier ein Operator:

public static MyType operator +(MyType x, int y) => x.Add(y);

Technisch ist es ein wenig bedauerlich, dass die Implementation in genau dieser Form gewählt wurde. Die Syntax erinnert auf den ersten Blick an die kurze und bündige Form von Funktionsdeklarationen in funktionalen Sprachen, aber leider fehlt ein wichtiges Detail, um diesen Ansatz nun auch in C# umsetzen zu können. Dieses Detail ist sozusagen der Kontext der Funktion. Natürlich hat die hier erzeugte Methode einen Kontext in dem Sinne, dass sie ihre eigenen lokalen Variablen hat. Aber in funktionalen Sprachen ist es auch möglich, Funktionen innerhalb anderer zu erzeugen und somit zu kapseln, wodurch wesentlich komplexere Zusammenhänge in ähnlich prägnanter Schreibweise abgebildet werden können. Zur Illustration hier ein kurzes Beispiel in Haskell:

force xs = go xs 'pseq' ()
  where
    go (_:xs) = go xs
    go [] = 1

Ohne Haskell im Detail zu verstehen, können Sie ablesen, dass die Funktion force abhängig deklariert wird von einer eingebetteten rekursiven Funktion go. Letztere hat sogar zwei „Overloads“, wie man in C# sagen würde, die ebenfalls in sehr kurzer Syntax deklariert werden können. Leider bietet das neue C#-Feature nicht die Fähigkeit, solche oder ähnliche Ausdrücke zu schreiben und zu verschachteln, so dass der verbleibende Vorteil lediglich eine verkürzte Schreibweise für den spezifischen Fall des direkten Returns ist.

Zum Schluss sei noch erwähnt, dass nicht nur Methoden, sondern auch Properties von der neuen Syntax profitieren können. In diesem Fall wird ein Getter mit einem Return automatisch generiert.

public int Magic => 42;
public string this[string key] => dict[key];

using static

Die Beschreibung dieses Features ist einfach. Mithilfe eines using-Statements – kurz vor Toresschluss hat sich Microsoft noch entschieden, das Schlüsselwort static einzuschieben, um die Unterscheidung vom normalen using deutlicher zu machen – können auf die Member einer static-Klasse im Code direkt zugegriffen werden, ohne die Qualifizierung mit dem Klassennamen zu erfordern. Klassische Beispiele bedienen sich der Klassen System.Math oder System.Console zur Demonstration (Listing 1).

using System;
using static System.Math;
using static System.Console;

namespace CS6
{
  class MainClass
  {
    public static void Main (string[] args)
    {
      Console.WriteLine (Math.Max (10, 3));
      WriteLine (Max (10, 3));
      ...

Nachdem mit zwei using static-Zeilen die beiden Klassen Math und Console verfügbar gemacht wurden, sind die beiden Zeilen Code im Hauptprogramm äquivalent. Auch hier handelt es sich wieder um eine syntaktische Abkürzung. Natürlich ist nicht anzuraten, diesen Mechanismus mit allzu vielen Klassen gleichzeitig zu verwenden, da sonst die Namen kollidieren und doch wieder die Qualifikation mit dem Klassennamen notwendig wird. Wer viele Klassen entlang funktionaler Ideen aufgebaut hat und oft mit statischen Funktionen arbeitet, wird erhebliche Einsparungen beim Tippen machen können und gleichzeitig die Lesbarkeit des Codes drastisch verbessern.

Es gibt ein kritisches Thema, dessen Sie sich in diesem Zusammenhang bewusst sein sollten. Beim Einsatz von Extension-Methoden schreibt Microsoft ein bestimmtes Verhalten vor, so dass diese nicht gemeinsam mit anderen statischen Methoden aus der Quellklasse zugänglich gemacht werden, sondern „nur“ als Extensions verfügbar sind (Listing 2).

using static System.Linq.Enumerable;

class Program {
  static void Main() {
    // Range kann aufgerufen werden, da es als Methode in der Klasse Enumerable
    // nun direkt verfügbar ist.
    var range = Range(5, 17);
    
    // Where kann so nicht aufgerufen werden, da es eine Extension-Methode ist
    var odd = Where(range, i => i % 2 == 1);
    
    // Where kann so aufgerufen werden - als Extension-Methode
    var even = range.Where(i => i % 2 == 0); 
  }
}

Sie sollten dieses besondere Verhalten im Kopf behalten, da es hier Konfliktpotential gibt. Außerdem ist zu beachten, dass sich in Tests die C#-6-Implementation in Mono bzw. Xamarin nicht an diese Regel hielt und den eigentlich ungültigen Aufruf in obigem Beispiel anstandslos akzeptierte. Hier ist Vorsicht geboten!

Stellen Sie Ihre Fragen zu diesen oder anderen Themen unseren entwickler.de-Lesern oder beantworten Sie Fragen der anderen Leser.

Der „Null Conditional Operator“

Sie erinnern sich sicher an den „Null Coalescing Operator“, den es in C# seit Langem gibt:

var a = b ?? c;

Nach dieser Zuweisung ist a gleich b, wenn b ungleich Null ist. Andernfalls ist a gleich c.
Die Idee des Null Conditional Operators ist es hingegen, explizite Null-Prüfungen überflüssig zu machen.

var name = customer?.Name;

Diese Zeile entspricht grob diesem Code:

string name = null;
if (customer != null) 
  name = customer.Name;

Sie müssen bei Verwendung des Operators also nicht mehr gesondert prüfen, ob Elemente ihres Zugriffspfades Null sind – wenn das so sein sollte, bekommen sie einfach direkt einen Null-Wert zurück.

Manchmal müssen Sie etwas über den Rückgabetyp nachdenken. Zum Beispiel möchten Sie etwa folgenden Code schreiben:

int count = contacts.Count();

Wenn allerdings an dieser Stelle contacts Null sein könnte, bietet sich diese Abwandlung an:

int? count = contacts?.Count();

Der Typ des Rückgabewertes hat sich notwendigerweise geändert, so dass Null verwendet werden kann, falls der Aufruf an Count() niemals stattfindet. Alternativ können Sie auch Null Conditional Operator und Null Coalescing Operator gemeinsam verwenden:

int count = contacts?.Count() ?? 0;

Damit enthielte count nun einfach den Wert 0, wenn contacts null sein sollte.

Eine Besonderheit des Patterns, die Microsoft gern speziell betont, ist die Verwendbarkeit bei der Auslösung von Events. Dabei wird auch eine bestimme Syntax notwendig. In der Vergangenheit hatten Sie zum Beispiel Code wie diesen, um ein Event auszulösen:

PropertyChangedEventHandler propertyChanged;

protected virtual void OnPropertyChanged(string propertyName) {
  if (propertyChanged != null)
    propertyChanged (this, new PropertyChangedEventArgs (propertyName));
}

Die Prüfung des Eventhandlers auf null ist erforderlich, da er nicht aufgerufen werden darf, wenn es keine Subscriber für den Event gibt. Oft werden noch wesentlich kompliziertere Patterns implementiert, um die Prüfung zusätzlich Thread-sicher zu machen, indem zunächst eine lokale Kopie des Handlers erzeugt wird. Diese Patterns können nun alle wesentlich verkürzt werden:

protected virtual void OnPropertyChanged(string propertyName) {
  propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Verschwunden ist die separate Prüfung auf null, und Microsoft betont, dass die Auswertung intern Thread-sicher und effizient genau einmal ausgeführt wird. Im Gegenzug müssen Sie allerdings die Hilfsmethode Invoke verwenden, um das Delegate auszuführen. Es ist nicht möglich, direkt nach dem ?.-Operator eine öffnende Klammer für eine Parameterliste zu schreiben. Microsoft befürchtete an dieser Stelle syntaktische Konflikte und die Verwendung von Invoke wurde zur Vorschrift für diese Anwendung.

In meinem Buch zur funktionalen Programmierung in C# habe ich im Kapitel über Monads einige Beispiele gezeigt, die stark von der Verwendung des Null-Conditional-Operators profitieren können, aber auch dessen Grenzen aufzeigen. Zunächst gibt es da die folgende Hilfsmethode GetThirdLeftChild. Wie der Name andeutet, greift diese Methode auf das dritte „Kind“-Element in einer Baumstruktur zu – ähnlichen Code gibt es in Anwendungen mit verschachtelten Datenstrukturen oft (Listing 3).

static string GetThirdLeftChild(FCSColl::UnbalancedBinaryTree tree) {
  if (tree != null) {
    if (tree.Left != null) {
      if (tree.Left.Left != null) {
        if (tree.Left.Left.Left != null) {
          return tree.Left.Left.Left.Value;
        }
        else {
          return "No such child";
        }
      }
      else {
        return "No such child";
      }
    }
    else {
      return "No such child";
    }
  }
  else {
    return "No such child";
  }
}

Code, wie in Listing 3 gezeigt, kann mithilfe des Null-Conditional-Operators vollständig eliminiert werden. Etwa so:

var thirdLeftChild = tree?.Left?.Left?.Left?.Value;

Oder, um die Semantik der vorhandenen Implementation besser abzubilden, vielleicht auch so:

var thirdLeftChild_ = tree?.Left?.Left?.Left?.Value ?? "No such child";

Hier ergibt sich aber eine interessante Frage: was, wenn Null ein tatsächlich zu erwartender, also in diesem Sinne gültiger Wert innerhalb der Kette ist? Der alte Algorithmus würde etwa für einen Nullwert im Value am Ende der Kette auch Null zurückliefern. Wenn Sie hingegen den maximalen Nutzen aus dem Null-Conditional-Operator ziehen wollen, können Sie nicht unterscheiden, ob Null als gültiger Wert aus dem Property-Pfad extrahiert oder aber als Fehlerindikator geliefert wurde. Und selbst wenn Sie wissen, dass Null kein gültiger Wert ist, bleibt es unmöglich zu sagen, an welcher Stelle des Pfades ein Nullwert gefunden wurde.

In funktionalen Sprachen gibt es oft Mechanismen, die die Abwesenheit eines Wertes anzeigen können, ohne dadurch den Wertebereich einer Variablen einzuengen. In F# gibt es einen Typ Option<T>, der zu diesem Zweck verwendet werden kann. Für mein oben erwähntes Buch habe ich einen einfachen Typ Maybe<T> entwickelt (namentlich angelehnt an die Sprache Haskell), der in ähnlicher Weise eine Kapselung für eine Variable darstellt und anzeigen kann, ob deren Wert überhaupt existiert oder nicht. Hier ist diese Implementation, frisch angepasst für C# 6.0 (Listing 4).

public class Maybe<T> {
  public static Maybe<T> Empty => new Maybe<T>( );

  public T Value { get; }
  public bool IsEmpty { get; }
  public bool HasValue => !IsEmpty;

  public Maybe(T val) {
    Value = val;
  }

  private Maybe( ) {
    IsEmpty = true;
  }

  public Maybe<R> Bind<R>(Func<T, Maybe<R>> g) {
    return IsEmpty ? Maybe<R>.Empty : g(Value);
  }
}

Mithilfe der Klasse Maybe<T> ist es möglich, den Algorithmus von oben, zum Zugriff auf das dritte Kind links, so darzustellen wie in Listing 5.

static Maybe<string> GetMaybeThirdLeftChild(FCSColl::UnbalancedBinaryTree<string> tree) {
  if (tree != null) {
    if (tree.Left != null) {
      if (tree.Left.Left != null) {
        if (tree.Left.Left.Left != null) {
          return tree.Left.Left.Left.Value.ToMaybe( );
        }
        else {
          return Maybe<string>.Empty;
        }
      }
      else {
        return Maybe<string>.Empty;
      }
    }
    else {
      return Maybe<string>.Empty;
    }
  }
  else {
    return Maybe<string>.Empty;
  }
}

Das wichtigste Detail ist hier, dass der Empty-Wert der Kapselung in Maybe<T> nur dann verwendet wird, wenn der Wert nicht abgefragt werden kann. Hingegen wird Value in ein Maybe<T> gekapselt (mit der Hilfsfunktion ToMaybe), wenn ein Wert abgefragt wurde – auch wenn dieser Wert Null gewesen sein sollte. Eine Unterscheidung zwischen den beiden Fällen ist so also direkt möglich.

Dieser Artikel ist nicht der richtige Platz, viele Details zu einer monadischen Implementation aufzuführen. Es sei daher abschließend zu diesem Exkurs auf die Funktion Bind verwiesen, die in Maybe<T> enthalten ist. Damit ist es möglich, die Abfrage des dritten Kindes links auf diesen Code zu reduzieren:

static Maybe<FCSColl::UnbalancedBinaryTree<string>> GetMonadicThirdLeftChild(FCSColl::UnbalancedBinaryTree<string> tree) {
  return tree.ToNotNullMaybe( ).
    Bind(t => t.Left.ToNotNullMaybe( )).
    Bind(t => t.Left.ToNotNullMaybe( )).
    Bind(t => t.Left.ToNotNullMaybe( ));
}

Der neue Null-Conditional-Operator in C# 6 bietet eine sehr interessante syntaktische Abkürzung und wird zweifellos bald von vielen C#-Programmierern eingesetzt werden. Mit der Beschreibung von Alternativen aus anderen Welten der Programmierung soll allerdings auch deutlich gemacht werden, dass es in dem gewählten Ansatz Lücken gibt, die sich letztlich aus dem C-Ursprung von C# herleiten.

String-Interpolation

Für den Umgang mit Strings gibt es in C# 6 eine Neuigkeit, die hoffentlich die Wartbarkeit von entsprechendem Code wesentlich verbessern wird. Mithilfe besonderer Formatelemente können externe Daten nun direkt in Strings integriert werden, ohne dass der Programmierer dazu nummerierte Platzhalter in die richtige Reihenfolge bringen muss.

Customer customer = GetCustomer(....);

string info = $"Der Kunde heist {customer.Name} und stammt aus {customer.Country}.";

Der Deutlichkeit halber: das Resultat wäre hier ein ganz normaler String, etwa „Der Kunde heißt Microsoft und stammt aus USA.“ Der String wird mit $ eingeleitet, und der Compiler erzeugt Code wie diesen dafür:

string info = String.Format("Der Kunde heißt {0} und stammt aus {1}.", customer.Name, customer.Country);

Wenn Sie mit dem Feature arbeiten, behalten Sie immer diese Umwandlung im Kopf. Unter Umständen können die Formatstrings durch die direkte Einbeziehung der Ausdrücke recht kompliziert werden. Sie können zum Beispiel direkt in eine Liste indizieren:

var customers = GetCustomerList(....);

var info = $"Der Kunde heißt {customers[0].Name} und stammt aus {customers[0].Country,5}.";

Beachten Sie, dass das Land hier auf fünf Zeichen formatiert ist, einfach durch das Anhängen von ,5 an den Ausdruck. Optional können Sie den Ausdruck in Klammern setzen, um ihn von der Formatangabe besser abzusetzen.

Auch der Zugriff auf Methoden ist möglich, Extension-Methoden werden korrekt aufgelöst. Numerische Werte können natürlich auch formatiert werden:

var info = $"Es gibt {(customers.Count()):d4} Kunden.";

Es ist wichtig zu realisieren, dass Sie leicht übertreiben können mit der Anwendung dieses Features. Die Wartbarkeit des folgenden Strings ist sicherlich nicht gesteigert gegenüber der längeren String.Format-basierten Version (Listing 6). Schließlich müssen interpolierte Strings an einem Stück gesetzt werden, und Kommentare gibt es nicht. Hier ist Disziplin gefragt. Zu diesem Beispiel gibt es sicher auch Verfechter des StringBuilders – auch eine gute Möglichkeit!

int customerCount = customers.Count();

var info = $"Die Liste {(customerCount == 0 ? "ist leer" : ($"hat {(customerCount == 1 ? "einen" : $"{customerCount}")} Eintr{(customerCount == 1 ? "ag" : "äge")}"))}"

var info_ = 
  String.Format("Die Liste {0}",
    customerCount == 0 ? "ist leer" :  // keine Kunden in der Liste
      String.Format("hat {0} Eintr{1}",
        customerCount == 1 ? "einen" : // genau ein Kunde
          customerCount.ToString(),    // mehrere Kunden
        customerCount == 1 ? "ag" :    // Endsilbe fuer einen einzelnen Kunden
          "äge"));                     // Endsilbe fuer mehrere Kunden

Ein interessantes Detail der Stringinterpolation in C# ist, dass es sich um ein erweiterbares Konzept handelt. Die Strings werden in dem Typ FormattableString dargestellt, und Sie können selbst Methoden zu deren Manipulation schreiben. Ein gängiges Beispiel ist die Anwendung einer bestimmten CultureInfo für die Formatierung von Werten, da die CurrentCulture, die als Standard verwendet wird, sich nicht einfach durch einen Parameter wie bei String.Format ändern lässt. So eine Umwandlung sieht etwa so aus:

string MakeInvariant(FormattableString fs) {
    return fs.ToString(CultureInfo.InvariantCulture);
}

var invariantString = MakeInvariant($"Order total on date {o.Date:D} is {o.Total}");

Sie können online eine Reihe von Beispielen finden, wie die Möglichkeit der Manipulation des FormattableString verwendet werden kann, um etwa bestimmte Zeichenkodierungen zu erzeugen (zum Beispiel für URIs oder andere Strings mit eingeschränkten Gültigkeitsbereichen) oder besondere Formate für numerische Werte.

Zum Schluss

Ursprünglich sollte C# 6.0 noch aufregender werden, als es sich letztlich darstellte, mit Neuerungen wie primären Konstruktoren, inline-Deklarationen oder sogar Record-Typen. Manche Ideen hätten noch wesentlich mehr interessante Möglichkeiten besonders für Anhänger der funktionalen Programmierung eröffnet. Microsoft wollte sich aber auf eine bestimmte Klasse von Neuheiten konzentrieren, hauptsächlich weil zur selben Zeit auch die Einführung des Roslyn-Compilers anstand. Das Ergebnis ist ein geglückter Wurf, der eine Reihe nützlicher Änderungen mitbringt. Ich lege Ihnen sehr ans Herz, sich damit zu beschäftigen, und hoffentlich sieht C# 7.0 demnächst noch ganz anders aus!

Aufmacherbild: C# concept green background with green text von Shutterstock.com / Urheberrecht: ScandinavianStock

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -