Monday, March 27, 2017

Das neue und verbesserte C# 6.0



Das neue und verbesserte C# 6.0

Microsoft hat in den Programmiersprachversionen C# 6.0 und Visual Basic 14, die mit .NET Framework 4.6 ausgeliefert werden, einige syntaktische Zuckerstückchen ergänzt, zum Beispiel:
·         automatische Properties mit Zuweisung
·         Read-Only automatic Properties
·         Null-Propagating Operator ?.
·         Operator nameof
·         String Interpolation      
  
Feature
  
Beispiel

  
C#  
Auto-property initializers
public int X { get; set; } = x;
Added
Read-only auto-properties
public int Y { get; } = y;
Added
Ctor assignment to   getter-only autoprops
Y = 15
Added
Static imports
using static System.Console; …   Write(4);
Added
Index initializer
new JObject { ["x"] = 3 }
Added
Await in catch/finally
try … catch { await … } finally {   await … }
Added
Exception filters
catch(E e) when (e.Count > 5) { … }
Added
Partial modules
Partial Module M1
N/A
Partial interfaces
Partial Interface I1
Exists
Multiline string literals
"Hello<newline>World"
Exists
Year-first date literals
Dim d = #2014-04-03#
N/A
Comments after implicit   line continuation
Dim addrs =   From c in Customers ' comment
N/A
TypeOf ... IsNot ...
If TypeOf x IsNot Customer Then …
N/A
Expression-bodied members
public double Dist => Sqrt(X * X +   Y * Y);
Added
Null-conditional operators
customer?.Orders?[5]
Added
String interpolation
$"{p.Name} is {p.Age} years   old."
Added
nameof operator
string s = nameof(Console.Write);
Added
#pragma
#Disable Warning BC40008
Added
Smart name resolution

N/A
Read-write props can   implement read-only interface properties

Exists
#Region inside methods

Exists
Overloads inferred from Overrides

N/A
CObj in attributes

Exists
CRef and parameter name

Exists
Extension Add in collection   initializers

Added
Improved overload resolution

Added

Null-Bedingter Operator
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:
1           public string PadCenter(string expr, int totalWidth) {
2               var length = expr.Length;
3                var left = (totalWidth - length) / 2;
4               var right = totalWidth - left - length;
5               return new String(' ', left) + expr + new String(' ', right);
6           }
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:
7           var length = expr == null ? 0 : expr.Length;
 Deutlich kürzer und leserlicher geht es mit dem neuen Null-Conditional Operator:
8           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 nullwar. 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:
9           public class Device {
10           event EventHandler<string> MessageReceived;
11           private void MessageCallback(string message) {
12               var handler = this.MessageReceived;
13               if(handler != null) {
14                   handler(this, message);
15               }
16           }
17       }
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:
18       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:
19       public class Circle {
20           public double Radius { get; set; }
21           public double Diameter => 2.0 * this.Radius;
22           public double Area => Math.PI * this.Radius * this.Radius;
23           public double Circumference => Math.PI * this.Diameter;
24       }
Ohne Verwendung dieser neuen Art der Funktions- und Eigenschaftsdeklaration würde die Klasse wie folgt aussehen:
25       public class Circle {
26           public double Radius { get; set; }
27           public double Diameter {
28               get {
29                   return 2 * this.Radius;
30               }
31           }
32           public double Area {
33               get {
34                   return Math.PI * this.Radius * this.Radius;
35               }
36           }
37           public double Circumference {
38               get {
39                   return Math.PI * this.Diameter;
40               }
41           }
42       }
 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:
43       public void DoSomething(string expr) {
44           if(expr == null) {
45               throw new ArgumentNullException(nameof(expr));
46           }
47       }
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:
48       public class MyKeyValuePair<TKey, TValue> {
49           public TKey Key { get; set; }
50           public TValue Value { get; set; }
51          
52           public string ToString() => $"{this.Key}={this.Value}";
53       }
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:
54       public class Circle {
55           public double Radius { get; set; }
56           public double Diameter => 2.0 * this.Radius;
57           public double Area => Math.PI * this.Radius * this.Radius;
58           public double Circumference => Math.PI * this.Diameter;
59           // Neu - Die ToString-Methode
60           public string ToString() => $"Kreis, Radius {this.Radius:0.00}, Fläche {this.Area:0.00}";
61       }
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:
62       return String.Format("{0}{1}={2}", separator, key, value);
63       return $"{separator}{key}={value}";

Initialisierer für Auto-Properties 
C# 6 ermöglicht uns nun auch, Standardwerte für Auto-Properties festzulegen:
64       public class Circle {
65           public double Radius { get; set; } = 5.0;
66       }
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:
67       public class Message {
68           public DateTime Received { get; } = DateTime.Now;
69           public string Content { get; }
70          
71           public Message(string content) {
72               this.Content = content;
73           }
74       }
 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:
75       try {
76           httpClient.Get(uri);
77       } catch(HttpException e) when (e.StatusCode == HttpStatusCode.Unauthorized) {
78           // Login ausführen und noch einmal probieren
79       }
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:
80       try {
81           // Irgendwie eine Exception auslösen
82       } catch(Exception e) when (Log(e)) {}
83        
84       // ...
85       private static bool Log(Exception e) {
86           // Exception loggen
87           return false;
88       }
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:
89       var dict = new Dictionary<int, string>() {
90           { 10, "Hallo, Welt!" },
91           { 20, "Foobar" },
92           { 30, "Noch eine Zeile" }
93       };
Dies geht nun etwas schöner, sodass der Zusammenhang zwischen beiden Werten deutlich klarer wird:
94       var dict = new Dictionary<int, string>() {
95           [10] = "Hallo, Welt!",
96           [20] = "Foobar",
97           [30] = "Noch eine Zeile"
98       };

Statische Using-Direktiven 
Ohne große Worte über dieses Feature zu verlieren, fangen wir direkt mit einem Beispiel an:
99       using System.Math;
100  class MyMath {
101      public Point PointOnCircle(double radius, double angle) => new Point(
102          radius * Cos(angle),
103          radius * Sin(angle)
104      );
105  }
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:
106  try {
107      var result = await PerformSomeOperationAsync();
108      return result;
109  } catch(Exception e) {
110      await LogExceptionToServerFarAwayAsync(e);
111  } finally {
112      await FreeResourcesAsync();
113  }

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 (PDF): 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:
114  // Der zu parsende Wert steht in expr
115  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:
116  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:
117  public class MyKeyValuePair<TKey, TValue>(TKey key, TValue value) {
118      public TKey Key { get; } = key;
119      public TValue Value { get; } = value;
120  }
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
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.
Auto-property initializers
public int X { get; set; } = x;
Getter-only auto-properties
public int Y { get; } = y;
Using static members
using System.Console; …   WriteLine(4);
Dictionary initializer
new JObject { ["x"] = 3,   ["y"] = 7 } 
Await in catch/finally
try … catch { await … } finally {   await … } 
Exception filters
catch(E e) if (e.Count > 5) { …   } 
Expression-bodied members
public double Dist => Sqrt(X * X   + Y * Y);
Null propagation Operator
customer?.Orders?[5]?.$price
Declaration expressions
int.TryParse(s, out var x);
Params IEnumerabl
e int Avg(params   IEnumerable<int> numbers) { … } 
Primary constructors
class Point(int x, int y) { … }
Binary literals
0b00000100
Digit separators
0xEF_FF_00_A0
Event initializers
new Customer { Notify += My Handler }; 
Field targets on autoprops
<Field: Serializable>   Property p As Integer

Viel Spaß


No comments:

Post a Comment