Die Community zu .NET und Classic VB.
Menü

Neue Features in C# 6

 von 

Einleitung 

Am 20.07.2015 hat Microsoft - kurz vor dem Release von Windows 10 - nun endlich Visual Studio 2015 veröffentlicht. ( Zur Download-Seite). Neben der Unterstützung von Frameworks wie dem neuen ASP.NET 5 (zur Zeit in der Beta-Version), Xamarin und Apache Cordova haben auch die Sprachen Visual Basic und C# neue Features erhalten, die dem Programmierer die Arbeit einfacher machen sollen.

Diese neuen Features möchte ich Ihnen im folgenden Artikel kurz anhand praktischer Beispiele vorstellen.

Null-Bedingter Operator  

Hinter dem womöglich etwas umständlich anmutenden Begriff "Null-Conditional Operator" und dem damit verbundenen Prinzip der "Null Propagation" versteckt sich ein eigentlich ganz einfacher Sachverhalt: Angenommen, eine Funktion akzeptiert einen string als Parameter und muss dessen Länge bestimmen, beispielsweise, um den String bis zu einer gewissen Länge aufzufüllen, sodass er zentriert wird. Kurzerhand konsultieren wir natürlich die Length-Eigenschaft:

public string PadCenter(string expr, int totalWidth) {
    var length = expr.Length;
    var left = (totalWidth - length) / 2;
    var right = totalWidth - left - length;
    
    return new String(' ', left) + expr + new String(' ', right);
}

Das geübte Auge erkennt schnell: Wird hier null für den ersten Parameter expr übergeben, kommt es zu einer der berühmt-berüchtigten NullReferenceExceptions. Um dies zu verhindern, ist zunächst eine Prüfung nötig, ob expr eventuell null ist:

var length = expr == null ? 0 : expr.Length;

Deutlich kürzer und leserlicher geht es mit dem neuen Null-Conditional Operator:

var length = expr?.Length ?? 0;

Zu beachten ist: Der Ausdruck expr?.Length allein gibt hier ein Nullable<int> zurück - nämlich die Länge des Strings, oder aber null, wenn auch der String null war. Anschließend wird über den ??-Operator aus diesem null-Wert die gewünschte 0.

Auch der Aufruf von Delegaten wird somit deutlich einfacher. Nehmen wir beispielsweise einen ganz typischen Aufruf an ein Event:

public class Device {
    event EventHandler<string> MessageReceived;
    
    private void MessageCallback(string message) {
        var handler = this.MessageReceived;
        if(handler != null) {
            handler(this, message);
        }
    }
}

Bei diesem Eventaufruf ist es notwendig, den Delegaten explizit an eine lokale Variable zuzuweisen, um Threadsicherheit gewährleisten zu können. Unter Verwendung des neuen Operators verkürzt sich der Inhalt der Methode auf eine einzige Zeile:

this.MessageReceived?.Invoke(this, message);

Definition von Funktionsrümpfen per Ausdruck  

Im Englischen treffender als "Expression Bodied Functions and Properties" bezeichnet, verbirgt sich hier ebenfalls ein Feature, das uns ein wenig Schreibarbeit spart und den Code besser lesbar gestaltet: Anstatt wie bisher Funktionen oder Eigenschaften im C-Stil innerhalb geschweifter Klammern zu definieren, kann dies nun - angelehnt an funktionale Sprachen wie F# - auch per Ausdruck passieren:

public class Circle {
    public double Radius { get; set; }
    public double Diameter => 2.0 * this.Radius;
    public double Area => Math.PI * this.Radius * this.Radius;
    public double Circumference => Math.PI * this.Diameter;
}

Ohne Verwendung dieser neuen Art der Funktions- und Eigenschaftsdeklaration würde die Klasse wie folgt aussehen:

public class Circle {
    public double Radius { get; set; }
    public double Diameter {
        get {
            return 2 * this.Radius;
        }
    }
    public double Area {
        get {
            return Math.PI * this.Radius * this.Radius;
        }
    }
    public double Circumference {
        get {
            return Math.PI * this.Diameter;
        }
    }
}

Wie Sie sehen, wird durch die neue Art der Deklaration sehr viel Platz eingespart und visueller "Lärm" - also eigentlich nicht benötigte Zeichen, die den Lesefluss stören - verhindert, wodurch der Fokus stärker auf den eigentlichen Berechnungen liegt.

Der nameof-Operator  

Anders als die Neuerungen, die wir bisher gesehen haben, erspart uns der nameof-Operator nicht nur Tipparbeit - er erleichtert uns auch die spätere Wartung des Codes enorm. Wie man anhand des Namens vielleicht schon vermuten kann, kommt der nameof-Operator immer dann zum Tragen, wenn der Name von irgendetwas benötigt wird. Schauen wir ihn uns an einem kleinen Beispiel an:

public void DoSomething(string expr) {
    if(expr == null) {
        throw new ArgumentNullException(nameof(expr));
    }
}

Der Konstruktor der ArgumentNullException nimmt ein einziges Argument an: Den Namen des Parameters, der null ist, obwohl er es nicht sein dürfte. Anstatt aber wie bisher den Namen des Parameters als Magic String zu hinterlegen (throw new ArgumentNullException("expr")), wird der Name beim Kompilieren automatisch eingefügt. Dank IntelliSense gehören Tippfehler auf diese Weise der Vergangenheit an, zudem müssen nach einem Umbenennen des Parameters nicht noch manuell irgendwelche String-Vorkommen geändert werden.

String-Interpolation  

Mit C# 6 hält ein bereits aus Sprachen wie PHP bekanntes Feature Einzug in die Sprache: String-Interpolation. Wiederum soll hier ein Beispiel klar machen, worum es geht:

public class MyKeyValuePair<TKey, TValue> {
    public TKey Key { get; set; }
    public TValue Value { get; set; }
    
    public string ToString() => $"{this.Key}={this.Value}";
}

Wie wir sehen, signalisieren wir mit einem Dollar-Zeichen $, dass der folgende String interpoliert werden soll - das bedeutet, dass alles, was in geschweiften Klammern steht, als Ausdruck ausgewertet wird. Natürlich unterstützt uns die IDE hier tatkräftig mit IntelliSense, ebenfalls bemerkt der Compiler etwaige Tippfehler und verweigert entsprechend die Arbeit, sodass Fehler nicht erst bei der Ausführung auffallen.

Erwähnenswert ist hier, dass - ähnlich wie bei Verwendung von String.Format - auch Format-Ausdrücke per Doppelpunkt angegeben werden können. Um Beispielsweise unsere Kreis-Klasse von früher noch einmal zu erweitern:

public class Circle {
    public double Radius { get; set; }
    public double Diameter => 2.0 * this.Radius;
    public double Area => Math.PI * this.Radius * this.Radius;
    public double Circumference => Math.PI * this.Diameter;
    // Neu - Die ToString-Methode
    public string ToString() => $"Kreis, Radius {this.Radius:0.00}, Fläche {this.Area:0.00}";
}

Natürlich ließ sich derselbe Effekt in älteren Varianten von C# (und auch in C# 6 noch) auch mit String.Format erreichen, allerdings bietet die String-Interpolation auch hier wieder eine kürzere, prägnantere Syntax mit mehr Fokus auf das, was eigentlich passiert. Man vergleiche:

return String.Format("{0}{1}={2}", separator, key, value);
return $"{separator}{key}={value}";

Initialisierer für Auto-Properties  

C# 6 ermöglicht uns nun auch, Standardwerte für Auto-Properties festzulegen:

public class Circle {
    public double Radius { get; set; } = 5.0;
}

Ebenfalls können Auto-Properties nun auch als readonly deklariert werden (indem das set einfach weggelassen wird), der Wert kann dann entweder auf dieselbe Art und Weise zugewiesen werden - oder aber im Konstruktor:

public class Message {
    public DateTime Received { get; } = DateTime.Now;
    public string Content { get; }
    
    public Message(string content) {
        this.Content = content;
    }
}

Dies macht es deutlich einfacher, immutable Typen zu definieren, da das Backing Field entfallen kann.

Exception Filter  

Wollten Sie auch schon einmal eine Exception nur dann abfangen, wenn eine bestimmte Bedingung erfüllt ist? Das geht nun:

try {
    httpClient.Get(uri);
} catch(HttpException e) when (e.StatusCode == HttpStatusCode.Unauthorized) {
    // Login ausführen und noch einmal probieren
}

Bisher musste man im Catch-Block prüfen, ob die Bedingung erfüllt war oder nicht - und falls nicht, die Exception mittels throw; erneut werfen, wodurch allerdings der Call Stack in Mitleidenschaft gezogen wird.

Eine weitere Verwendungsart ist das "stille" Loggen von Exceptions:

try {
    // Irgendwie eine Exception auslösen
} catch(Exception e) when (Log(e)) {}

// ...
private static bool Log(Exception e) {
    // Exception loggen
    return false;
}

Die Log-Methode wird aufgerufen, um zu bestimmen, ob der Catch-Block betreten werden soll oder nicht - da sie allerdings false zurückgibt, wird der Block nicht ausgeführt und die Exception wird wie üblich nach oben hin durchgereicht. Auch hier wird der Call Stack nicht in Mitleidenschaft gezogen.

Index-Initialisierer  

Bisher war es nicht "schön" möglich, beispielsweise Schlüssel-Wert-Paare bei der Instanzierung eines Dictionary anzugeben. Bisher lautete die Syntax dafür:

var dict = new Dictionary<int, string>() {
    { 10, "Hallo, Welt!" },
    { 20, "Foobar" },
    { 30, "Noch eine Zeile" }
};

Dies geht nun etwas schöner, sodass der Zusammenhang zwischen beiden Werten deutlich klarer wird:

var dict = new Dictionary<int, string>() {
    [10] = "Hallo, Welt!",
    [20] = "Foobar",
    [30] = "Noch eine Zeile"
}; 

Statische Using-Direktiven  

Ohne große Worte über dieses Feature zu verlieren, fangen wir direkt mit einem Beispiel an:

using System.Math;

class MyMath {
    public Point PointOnCircle(double radius, double angle) => new Point(
        radius * Cos(angle), 
        radius * Sin(angle)
    );
}

Wie Sie sehen, erlaubt das statische Using using System.Math, auf die statischen Methoden der Klasse System.Math zuzugreifen, ohne den Klassennamen voranzustellen. Dies ist insbesondere von Vorteil, wenn viele Aufrufe an solche Methoden notwendig sind - etwa bei komplexen geometrischen Berechnungen.

Was gibt's noch Neues?  

Neben den demonstrierten neuen Sprachfeatures gibt es auch eine Reihe an Verbesserungen, die nicht direkt auffallen. Beispielsweise ist es nun möglich, in einem try-catch-Block auch im catch und im finally asynchrone Methodenaufrufe zu verwenden:

try {
    var result = await PerformSomeOperationAsync();
    return result;
} catch(Exception e) {
    await LogExceptionToServerFarAwayAsync(e);
} finally {
    await FreeResourcesAsync();
}

Auch können nun Erweiterungsmethoden zur Initialisierung von Collection-Typen verwendet werden, was bisher nur in VB.NET möglich war. Auch heißt es in einem Dokument, das die Sprachfeatures zusammenfasst:

There are a number of small improvements to overload resolution, which will likely result in more things just working the way youd expect them to.

Es hat also offensichtlich Verbesserungen bei der Auflösung von Methodenüberladungen gegeben, insbesondere wohl im Bezug auf Nullable-Datentypen. Eine genaue Erläuterung der Änderungen findet sich in dieser Übersicht allerdings nicht.

Was wurde nicht umgesetzt?  

Wie wohl in jedem größeren Softwareprojekt, mussten auch bei C# 6 Kompromisse eingegangen werden, wodurch Features vorerst wegfallen, die bereits fest eingeplant waren.

Eine kleine Änderung an der Syntax, die allerdings sicher vielen Entwicklern willkommen gewesen wäre, sind Deklarationsausdrücke. Stellen Sie sich vor, sie sollen eine Zahl parsen, die in einem String steht - etwas, das wohl jeder C#-Entwickler schon etliche Male gemacht hat. Sie beginnen also zu tippen:

// Der zu parsende Wert steht in expr
if(Int32.TryParse(expr, out value)) {

Fällt Ihnen etwas auf? Natürlich, Sie haben vergessen, die Variable value zu deklarieren - und das fällt Ihnen natürlich erst auf, als Sie das out schon getippt haben. C# 6 kommt (oder viel mehr: käme) zur Hilfe und ermöglicht(e) Ihnen, die Deklaration an Ort und Stelle nachzuholen, anstatt erst aufwendig wieder zwei Zeilen nach oben du navigieren, die Variable zu deklarieren und wieder an die richtige Stelle im Code zu navigieren:

if(Int32.TryParse(expr, out int value)) {

Leider scheint es, dass dieses Feature die Dinge für das C#-Team sehr kompliziert gemacht hätte, weswegen man sich entschied, es zu streichen - der Aufwand stand wohl nicht im Verhältnis zum Nutzen.

Ein weiteres solches Feature sind sogenannte Primärkonstruktoren, die es ermöglichen sollten, Eigenschaften, die nur einen Getter haben, zu initialisieren:

public class MyKeyValuePair<TKey, TValue>(TKey key, TValue value) {
    public TKey Key { get; } = key;
    public TValue Value { get; } = value;
}

Geplant war, auch die Deklaration eines Primärkonstruktorrumpfes zu ermöglichen, wodurch komplexe Regeln für die "normalen" Konstruktoren entstanden wären: So hätte nur der Primärkonstruktor den Basiskonstruktor aufrufen dürfen, wodurch jeder andere Konstruktor auf jeden Fall den Primärkonstruktor hätte aufrufen müssen... Sie sehen, es wäre kompliziert geworden. Obwohl das Konzept durchaus seine Reize hat (wie oben zu sehen - es erleichtert die Definition immutabler Klassen enorm), hat Microsoft sich aber glücklicherweise zunächst dagegen entschieden, Primärkonstruktoren in C# 6 zu implementieren. Da diese zwischenzeitlich in Preview-Versionen allerdings schon verfügbar waren, ist davon auszugehen, dass wir sie in einer späteren Version noch einmal wiedersehen werden.

Fazit  

Wir haben in diesem Artikel gesehen, dass C# sich mit der neuesten Version stark in Richtung der funktionalen Programmierung weiterentwickelt - der Fokus liegt stärker auf dem "was" denn auf dem "wie". Einige neue Sprachfeatures wie String-Interpolation, Deklaration von Funktionen per Ausdrücken und auch die Index-Initialisierer bieten zwar keine neue Funktionalität, dafür aber eine prägnante Syntax, durch die Code schneller zu erfassen und zu verstehen ist.

Sicherlich ist es größtenteils Geschmackssache, ob man diese neuen Features verwenden möchte oder nicht - aber meines Erachtens machen sie die Sprache deutlich einfacher.

Andere Features hingegen - insbesondere die Verwendung von await in catch und finally-Blöcken - beheben Probleme der Sprache, die Entwickler zuvor vor die Herausforderung stellten, unangenehme Workarounds zu verwenden.

Glücklicherweise beweist das C#-Team bei der Weiterentwicklung der Sprache aber ein gutes Gefühl dafür, welche Features uns als Entwickler voranbringen und welche zusätzliche Verwirrung stiften oder schlicht und ergreifend den Aufwand nicht wert sind. So entschied man sich dagegen, Primärkonstruktoren und Deklarationsausdrücke zu implementieren, was ich persönlich begrüße.

Wie sehen Sie diese neue Stufe der Entwicklung von C#? Befürworten Sie die Annäherung an die funktionale Programmierung oder bevorzugen Sie die bisherige Syntax? Ich freue mich auf Diskussionen rund um die Neuerungen in C# 6 im Forum!

Ihre Meinung  

Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.

readonly Auto property - OlimilO 24.07.15 21:21 2 Antworten
Gute Zusammenfassung - Rene Niediek 30.07.15 12:06 9 Antworten