C#

Wie C# 6.0 Ihren Code vereinfacht, klarer gestaltet und komprimiert

Mark Michaelis

C# 6.0 ist keine radikale Revolution in der C#-Programmierung. Im Gegensatz zur Einführung von Generika in C# 2.0, zu C# 3.0 und dem bahnbrechenden Verfahren zum Programmieren von Auflistungen mit LlNQ oder der Vereinfachung asynchroner Programmiermuster in C# 5.0 revolutioniert C# 6.0 die Entwicklung nicht. Nichtsdestotrotz wird C# 6.0 ändern, wie Sie C#-Code in bestimmten Szenarien schreiben. Dies liegt an Funktionen, die so viel effizienter sind, dass Sie wahrscheinlich schnell vergessen, dass die Codierung einmal anders erfolgt ist. In dieser Version werden neue Syntaxverkürzungen eingeführt, Standardcode wird gelegentlich verringert, und letztlich wird das Schreiben von C#-Code verschlankt. In diesem Artikel befasse ich mich mit den Details der neuen C# 6.0-Featuresammlung, die all dies ermöglicht. Insbesondere lege ich den Schwerpunkt auf die Elemente in der in Abbildung 1 gezeigten Mindmap.

Mindmap zu C# 6.0
Abbildung 1 – Mindmap zu C# 6.0

Beachten Sie, dass zahlreiche der hier aufgeführten Beispiele aus der nächsten Ausgabe meines Buchs „Essential C# 6.0“ (Addison-Wesley Professional) stammen.

Using Static

Viele der C# 6.0-Funktionen können in den grundlegendsten Konsolenprogrammen genutzt werden. „using“ wird nun z. B. für bestimmte Klassen in einer Funktion unterstützt, die als „using static-Direktive“ bezeichnet wird. Diese führt statische Methoden aus, die im globalen Bereich verfügbar sind, ohne dass ein Typpräfix erforderlich ist. Abbildung 2 zeigt dies. Da „System.Console“ eine statische Klasse ist, ist sie ein hervorragendes Beispiel für die Nutzung dieser Funktion.

Abbildung 2 – Die using static-Direktive steigert die Übersichtlichkeit Ihres Codes

using System;
using static System.ConsoleColor;
using static System.IO.Directory;
using static System.Threading.Interlocked;
using static System.Threading.Tasks.Parallel;
public static class Program
{
  // ...
  public static void Main(string[] args)
  {
    // Parameter checking eliminated for elucidation.
    EncryptFiles(args[0], args[1]);
  }
  public static int EncryptFiles(
    string directoryPath, string searchPattern = "*")
  {
    ConsoleColor color = ForegroundColor;
    int fileCount = 0;
    try
    {
      ForegroundColor = Yellow
      WriteLine("Start encryption...");
      string[] files = GetFiles(directoryPath, searchPattern,
        System.IO.SearchOption.AllDirectories);
      ForegroundColor = White
      ForEach(files, (filename) =>
      {
        Encrypt(filename);
        WriteLine("\t'{0}' encrypted", filename);
        Increment(ref fileCount);
      });
      ForegroundColor = Yellow
      WriteLine("Encryption completed");
    }
    finally
    {
      ForegroundColor = color;
    }
    return fileCount;
  }
}

In diesem Beispiel werden using static-Direktiven für „System.ConsoleColor“, „System.IO.Directory“, „System.Threading.Interlocked“ und „System.Threading.Tasks.Parallel“ verwendet. Diese ermöglichen den direkten Aufruf zahlreicher Methoden, Eigenschaften und Enumerationen: „ForegroundColor“, „WriteLine“, „GetFiles“, „Increment“, „Yellow“, „White“ und „ForEach“. In allen genannten Fällen ist es nicht erforderlich, das statische Element durch seinen Typ zu qualifizieren. (Für Benutzer von Visual Studio 2015 Preview oder früher gilt für die Syntax, dass das Schlüsselwort „static“ nicht hinter „using“ hinzugefügt werden muss – es heißt also beispielsweise nur „using System.Console“. Außerdem funktioniert die using static-Direktive erst ab Visual Studio 2015 Preview für Enumerationen und Strukturen sowie statische Klassen.)

In den meisten Fällen wird durch das Auslassen des Typqualifizierers die Klarheit des Codes nicht wesentlich verringert, obwohl weniger Code vorhanden ist. „WriteLine“ in einem Konsolenprogramm ist ebenso einleuchtend wie der Aufruf von „GetFiles“. Da die Hinzufügung der using static-Direktive für „System.Threading.Tasks.Parallel“ offensichtlich beabsichtigt war, stellt „ForEach“ außerdem eine Möglichkeit zum Definieren einer parallelen ForEach-Schleife dar, die mit jeder Version von C# mehr und mehr wie eine C#-ForEach-Anweisung aussieht (wenn Sie sie nur im richtigen Licht betrachten).

Wenn Sie die using static-Direktive verwenden, müssen Sie Vorsicht walten lassen, damit die Klarheit nicht verloren geht. Sehen Sie sich beispielsweise die in Abbildung 3 definierte Encrypt-Funktion an.

Abbildung 3 – Nicht eindeutiger Exists-Aufruf (mit dem Operator „nameof“)

private static void Encrypt(string filename)
  {
    if (!Exists(filename)) // LOGIC ERROR: Using Directory rather than File
    {
      throw new ArgumentException("The file does not exist.", 
        nameof(filename));
    }
    // ...
  }

Es scheint so, als wäre der Aufruf von „Exists“ die richtige Entscheidung. Der Aufruf lautet jedoch eigentlich „Directory.Exists“, obwohl hier tatsächlich „File.Exists“ benötigt wird. Anders gesagt: Obwohl der Code sicherlich lesbar ist, ist er falsch, und zumindest in diesem Fall ist das Vermeiden der using static-Syntax möglicherweise die bessere Lösung.

Beachten Sie, dass bei Angabe von using static-Direktiven für „System.IO.Directory“ und „System.IO.File“ der Compiler beim Aufrufen von „Exists“ einen Fehler ausgibt und erzwingt, dass der Code mit einem Präfix für Typmehrdeutigkeitsvermeidung geändert werden muss, damit die Mehrdeutigkeit aufgelöst wird.

Eine weitere Funktion der using static-Direktive ist deren Verhalten mit Erweiterungsmethoden. Erweiterungsmethoden werden nicht in den globalen Bereich verschoben, wie es normalerweise für statische Methoden der Fall ist. Eine Direktive „using static ParallelEnumerable“ würde z. B. die Select-Methode nicht in den globalen Bereich verschieben, und Sie könnten den folgenden Aufruf nicht ausführen: Select(files, (filename) => {... }). Diese Einschränkung ist entwurfsbedingt. Einerseits sollten Erweiterungsmethoden als Instanzmethoden für ein Objekt ausgeführt werden (Beispiel: files.Select((filename)=>{... })), und es ist kein normales Muster, sie als statische Methoden direkt aus dem Typ aufzurufen. Andererseits stehen Bibliotheken (z. B. „System.Linq“) mit Typen wie etwa „Enumerable“ und „ParallelEnumerable“ zur Verfügung, deren Methodennamen sich überschneiden (z. B. „Select“). Wenn alle diese Typen dem globalen Bereich hinzugefügt werden, fallen für IntelliSense unnötige Stördaten an, und es erfolgt möglicherweise ein nicht eindeutiger Aufruf (allerdings nicht im Fall von auf „System.Linq“ basierenden Klassen).

Auch wenn Erweiterungsmethoden nicht im globalen Bereich platziert werden, ermöglicht C# 6.0 dennoch Klassen mit Erweiterungsmethoden in using static-Direktiven. Die using static-Direktive erzielt das gleiche Ergebnis wie eine using-Direktive (Namespace), allerdings nur für die spezifische Klasse, für die die using static-Direktive gilt. Anders gesagt: Mit „using static“ kann der Entwickler einschränken, welche Erweiterungsmethoden bis zu der betreffenden angegebenen Klasse verfügbar sind (nicht der gesamte Namespace). Sehen Sie sich beispielsweise den in Abbildung 4 gezeigten Codeausschnitt an.

Abbildung 4 – Nur Erweiterungsmethoden aus „ParallelEnumerable“ befinden sich im Bereich

using static System.Linq.ParallelEnumerable;
using static System.IO.Console;
using static System.Threading.Interlocked;
// ...
    string[] files = GetFiles(directoryPath, searchPattern,
      System.IO.SearchOption.AllDirectories);
    files.AsParallel().ForAll( (filename) =>
    {
      Encrypt(filename);
      WriteLine($"\t'{filename}' encrypted");
      Increment(ref fileCount);
    });
// ...

Beachten Sie, dass keine Anweisung „using System.Linq“ vorhanden ist. Stattdessen wird die Direktive „using static System.Linq.ParallelEnumerable“ verwendet, die bewirkt, dass nur Erweiterungsmethoden aus „ParallelEnumerable“ als Erweiterungsmethoden im Bereich liegen. Alle Erweiterungsmethoden für Klassen wie „System.Linq.Enumerable“ sind nicht als Erweiterungsmethoden verfügbar. Für „files.Select(...)“ tritt z. B. ein Fehler auf, weil „Select“ für ein Zeichenfolgenarray (und auch für „IEnumerable<string>“) nicht im Bereich liegt. Im Gegensatz dazu liegt „AsParallel“ über „System.Linq.ParallelEnumerable“ im Bereich. Zusammengefasst kann gesagt werden, dass die using static-Direktive für eine Klasse mit Erweiterungsmethoden die Erweiterungsmethoden dieser Klasse als Erweiterungsmethoden im Bereich bereitstellt. (Methoden für die gleiche Klasse, die keine Erweiterungsmethoden sind, werden normal im globalen Bereich bereitgestellt.)

Im Allgemeinen ist es eine bewährte Methode, die Verwendung der using static-Direktive auf einige Klassen einzuschränken, die wiederholt im Bereich verwendet werden (anders als „Parallel“), z. B. „System.Console“ oder „System.Math“. Wenn „using static“ für Enumerationen verwendet wird, stellen Sie analog dazu sicher, dass die Elemente der Enumeration ohne ihren Typbezeichner selbsterklärend sind. Geben Sie z. B. „using Microsoft.VisualStudio.TestTools.UnitTesting.Assert“ (vielleicht meine Lieblingsdirektive) in Komponententestdateien an, um Testassertionaufrufe wie etwa „IsTrue“, „AreEqual<T>“, „Fail“ und „IsNotNull“ zu aktivieren.

Der Operator „nameof“.

Abbildung 3 zeigt eine weitere neue Funktion in C# 6.0 – den Operator „nameof“. Dies ist ein neues Kontextschlüsselwort zum Identifizieren eines Zeichenfolgenliterals, das (zur Kompilierungszeit) eine Konstante für den nicht qualifizierten Namen für den Bezeichner extrahiert, der als Argument angegeben wurde. In Abbildung 3 gibt „nameof(filename)“ den Wert „filename“ zurück – den Namen des Parameters der Encrypt-Methode. „nameof“ funktioniert jedoch mit jedem programmgesteuerten Bezeichner. In Abbildung 5 wird „nameof“ z. B. zum Übergeben des Eigenschaftennamens an „INotifyPropertyChanged.PropertyChanged“ verwendet. (Übrigens ist die Verwendung des Attributs „CallerMemberName“ zum Abrufen eines Eigenschaftennamens für den PropertyChanged-Aufruf noch immer ein gültiges Verfahren zum Abrufen des Eigenschaftennamens, siehe itl.tc/?p=11661.)

Abbildung 5 – Verwenden des Operators „nameof“ für „INotifyPropertyChanged.PropertyChanged“

public class Person : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  public Person(string name)
  {
    Name = name;
  }
  private string _Name;
  public string Name
  {
    get{ return _Name; }
    set
    {
      if (_Name != value)
      {
        _Name = value;
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
          propertyChanged(this,
            new PropertyChangedEventArgs(nameof(Name)));
        }
      }
    }
  }
  // ...
}
[TestClass]
public class PersonTests
{
  [TestMethod]
  public void PropertyName()
  {
    bool called = false;
    Person person = new Person("Inigo Montoya");
    person.PropertyChanged += (sender, eventArgs) =>
    {
      AreEqual(nameof(CSharp6.Person.Name), eventArgs.PropertyName);
      called = true;
    };
    person.Name = "Princess Buttercup";
    IsTrue(called);
  }   
}

Beachten Sie Folgendes: Unabhängig davon, ob nur der nicht qualifizierte „Name“ angegeben wird (weil er im Bereich liegt) oder der vollqualifizierte Bezeichner „CSharp6.Person.Name“ wie im Test verwendet wird, ist das Ergebnis nur der abschließende Bezeichner (das letzte Element in einem Namen mit Punkten).

Indem der Operator „nameof“ genutzt wird, ist es möglich, die meisten „magischen“ Zeichenfolgen zu beseitigen, die sich auf Codebezeichner beziehen, wenn sich diese im Bereich befinden. Auf diese Weise werden nicht nur Laufzeitfehler aufgrund von falscher Schreibweise in den magischen Zeichenfolgen beseitigt, die der Compiler niemals überprüft, sondern es werden auch Umgestaltungstools wie „Rename“ zum Aktualisieren aller Verweise auf den Namenänderungsbezeichner aktiviert. Wenn sich ein Name ohne ein Umgestaltungstool ändert, gibt der Compiler einen Fehler aus, der besagt, dass der Bezeichner nicht mehr vorhanden ist.

Zeichenfolgeninterpolation

Abbildung 3 kann optimiert werden, indem nicht nur die Ausnahmemeldung angegeben wird, die besagt, dass die Datei nicht gefunden wurde, sondern auch der Dateiname selbst angezeigt wird. Vor C# 6.0 geschah dies mithilfe von „string.Format“, um den Dateinamen in das Zeichenfolgenliteral einzubetten. Zusammengesetzte Formatierung war jedoch nicht besonders gut lesbar. Für das Formatieren des Namens einer Person war es z. B.erforderlich, Platzhalter basierend auf der Reihenfolge der Parameter wie in der Nachrichtenzuordnung in Abbildung 6 gezeigt zu ersetzen.

Abbildung 6 – Zusammengesetzte Zeichenfolgenformatierung im Vergleich zu Zeichenfolgeninterpolation

[TestMethod]
public void InterpolateString()
{
  Person person = new Person("Inigo", "Montoya") { Age = 42 };
  string message =
    string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
    person.FirstName, person.LastName, person.Age);
  AreEqual<string>
    ("Hello!  My name is Inigo Montoya and I am 42 years old.", message);
  string messageInterpolated =
    $"Hello!  My name is {person.FirstName} {person.LastName} and I am
    {person.Age} years old.";
  AreEqual<string>(message, messageInterpolated);
}

Beachten Sie den Alternativansatz für zusammengesetzte Formatierung durch die Zuordnung zu „messageInterpolated“. In diesem Beispiel ist der „messageInterpolated“ zugewiesene Ausdruck ein Zeichenfolgenliteral mit dem Präfix „$“. Geschweifte Klammern identifizieren Code, der inline in die Zeichenfolge eingebettet ist. In diesem Fall werden die Eigenschaften von „person“ verwendet, damit diese Zeichenfolge erheblich leichter lesbar ist als eine zusammengesetzte Zeichenfolge. Außerdem verringert die Zeichenfolgen-Interpolationssyntax Fehler, die durch Argumente verursacht werden, die auf die Formatzeichenfolge folgen und eine falsche Reihenfolge aufweisen oder ganz fehlen und eine Ausnahme auslösen. (In Visual Studio 2015 Preview ist kein Zeichen „$“ vorhanden. Stattdessen muss jeder linken geschweiften Klammer ein Schrägstrich vorangestellt werden. Spätere Versionen als Visual Studio 2015 Preview werden so aktualisiert, dass sie stattdessen das Zeichen „$“ vor der Zeichenfolgenliteral-Syntax verwenden.)

Die Zeichenfolgeninterpolation wird zur Kompilierungszeit transformiert, um einen äquivalenten Aufruf „string.Format“ auszuführen. Die Unterstützung für Lokalisierung bleibt unverändert (mit den traditionellen Formatzeichenfolgen) und verwendet keine Codeinjektion über Zeichenfolgen nach der Kompilierung.

Abbildung 7 zeigt zwei weitere Beispiele für Zeichenfolgeninterpolation.

Abbildung 7 – Verwenden von Zeichenfolgeninterpolation anstelle von „string.Format“

public Person(string firstName, string lastName, int? age=null)
{
  Name = $"{firstName} {lastName}";
  Age = age;
}
private static void Encrypt(string filename)
{
  if (!System.IO.File.Exists(filename))
  {
    throw new ArgumentException(
      $"The file, '{filename}', does not exist.", nameof(filename));
  }
  // ...
}

Beachten Sie, dass im zweiten Fall die throw-Anweisung, Zeichenfolgeninterpolation und der Operator „nameof“ verwendet werden. Zeichenfolgeninterpolation bewirkt, dass die ArgumentException-Nachricht den Dateinamen enthält (also den Text „The file, 'c:\data\missingfile.txt' does not exist“). Der Operator „nameof“ wird zum Identifizieren des Namens des Encrypt-Parameters („filename“) verwendet, des zweiten Arguments des Konstruktors „ArgumentException“. Visual Studio 2015 ist vollständig mit der Zeichenfolgen-Interpolationssyntax kompatibel und stellt Farbcodierung und IntelliSense für die Codeblöcke zur Verfügung, die in die interpolierte Zeichenfolge eingebettet sind.

Bedingter NULL-Operator

Aus Gründen der Klarheit wurde die Methode „Main“ in Abbildung 2 entfernt. Nahezu jede dieser Methoden, die Argumente annimmt, erfordert jedoch die Überprüfung des Parameters auf den Wert null, bevor das Element „Length“ zum Ermitteln der Anzahl der übergebenen Parameter aufgerufen wird. Allgemeiner ausgedrückt, wird sehr häufig auf den Wert null geprüft, bevor ein Element aufgerufen wird, um eine Ausnahme des Typs „System.NullReferenceException“ zu vermeiden (die fast immer einen Fehler in der Programmlogik anzeigt). Aufgrund der Häufigkeit dieses Musters führt C# 6.0 den Operator „?“ ein, der als Nullbedingungsoperator bezeichnet wird:

public static void Main(string[] args)
{
  switch (args?.Length)
  {
  // ...
  }
}

Der Nullbedingungsoperator überprüft, ob der Operand den Wert null aufweist, bevor die Methode oder Eigenschaft (in diesem Falle „Length“) aufgerufen wird. Der logisch äquivalente explizite Code lautet folgendermaßen (auch wenn in der C# 6.0-Syntax der Wert von „args“ nur ein Mal ausgewertet wird):

(args != null) ? (int?)args.Length : null

Die Verwendung des Nullbedingungsoperators ist besonders bequem, weil er verkettet werden kann. Wenn Sie z. B. „string[] names = person?.Name?.Split(' ')“ aufrufen, wird „Split“ nur aufgerufen, wenn „person“ und „person.Name“ ungleich null sind. Wenn bei einer Verkettung der erste Operand null ist, wird die Auswertung des Ausdrucks kurzgeschlossen, und es erfolgen keine weiteren Aufrufe innerhalb der Ausdrucksaufrufkette. Sie sollten jedoch sicherstellen, dass weitere Nullbedingungsoperatoren nicht unbeabsichtigterweise vernachlässigt werden. Sehen Sie sich z. B. „names = person?.Name.Split(' ')“ an. Wenn eine person-Instanz vorhanden, „Name“ jedoch null ist, wird eine „NullReferenceException“ beim Aufruf von „Split“ ausgelöst. Dies bedeutet nicht, dass Sie eine Kette von Nullbedingungsoperatoren verwenden müssen, sondern dass die Programmlogik wohlüberlegt sein sollte. Im Fall von „Person“ ist z. B. kein weiterer Nullbedingungsoperator erforderlich, wenn „Name“ überprüft wird und niemals null sein kann.

Ein wichtiger Aspekt des Nullbedingungsoperators besteht darin, dass er immer eine Version des betreffenden Typs zurückgibt, die Nullwerte zulässt, wenn er mit einem Element verwendet wird, das einen Werttyp zurückgibt. „args?.Length“ gibt z. B. „int?“ und nicht einfach „int“ zurück. Auch wenn es etwas eigentümlich ist (im Vergleich zum Verhalten anderer Operatoren), tritt die Rückgabe eines Werttyps, der Nullwerte zulässt, nur am Ende der Aufrufkette auf. Als Ergebnis daraus lässt der Aufruf des Punktoperators („.“) für „Length“ nur den Aufruf von int-Elementen (nicht „int?“) zu. Wenn „args?.Length“ in Klammern verkapselt wird (und damit das int?-Ergebnis über den Vorrang des Klammeroperators erzwungen wird), wird die int?-Rückgabe aufgerufen, und die spezifischen <T>-Elemente, die Nullwerte zulassen („HasValue“ und „Value“), werden zur Verfügung gestellt.

Der Nullbedingungsoperator ist schon für sich betrachtet eine großartige Funktion. Wenn er jedoch in Kombination mit einem Delegataufruf aufgerufen wird, wird ein Schwachpunkt von C# behoben, der seit C# 1.0 bestand. Beachten Sie, wie ich in Abbildung 5 den PropertyChange-Ereignishandler einer lokalen Kopie („propertyChanged“) zuweise, bevor der Wert für null überprüft und das Ereignis dann ausgelöst wird. Dies ist das einfachste threadsichere Verfahren zum Aufrufen von Ereignissen, ohne zu riskieren, dass die Abonnementstornierung eines Ereignisses zwischen dem Zeitpunkt der Nullüberprüfung und dem Auslösen des Ereignisses auftritt. Leider ist dies wenig intuitiv, und ich sehe regelmäßig Code, der dieses Muster nicht verwendet – mit dem Ergebnis von inkonsistenten „NullReferenceExceptions“. Glücklicherweise wird dieses Problem durch die Einführung des Nullbedingungsoperators in C# 6.0 behoben.

Mit C# 6.0 ändert sich der folgende Codeausschnitt:

PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
  propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
}

einfach in:

PropertyChanged?.Invoke(propertyChanged(
  this, new PropertyChangedEventArgs(nameof(Name)));

Da ein Ereignis nur ein Delegat ist, ist das gleiche Muster zum Aufrufen eines Delegaten über den Nullbedingungsoperator und „Invoke“ immer möglich. Diese Funktion wird mit Sicherheit ändern, wie Sie in Zukunft C#-Code schreiben – vielleicht mehr als jede andere Funktion in C# 6.0. Sobald Sie Nullbedingungsoperatoren für Delegaten nutzen, werden Sie wahrscheinlich nicht mehr auf die alte Art codieren wollen (es sei denn, Sie sind in einem Szenario vor C# 6.0 gefangen).

Nullbedingungsoperatoren können auch in Kombination mit einem Indexoperator verwendet werden. Wenn Sie diese z. B. in Kombination mit einem „Newtonsoft.JObject“ verwenden, können Sie ein JSON-Objekt traversieren, um bestimmte Elemente abzurufen. Abbildung 8 zeigt dies.

Abbildung 8 – Beispiel für eine Konsolenfarbkonfiguration

string jsonText =
    @"{
      'ForegroundColor':  {
        'Error':  'Red',
        'Warning':  'Red',
        'Normal':  'Yellow',
        'Verbose':  'White'
      }
    }";
  JObject consoleColorConfiguration = JObject.Parse(jsonText);
  string colorText = consoleColorConfiguration[
    "ForegroundColor"]?["Normal"]?.Value<string>();
  ConsoleColor color;
  if (Enum.TryParse<ConsoleColor>(colorText, out color))
  {
    Console.ForegroundColor = colorText;
  }

Sie müssen unbedingt berücksichtigen, dass „JObject“ im Gegensatz zu den meisten Auflistungen in MSCORLIB keine Ausnahme auslöst, wenn ein Index ungültig ist. Wenn „ForegroundColor“ nicht vorhanden ist, gibt „JObject“ z. B. null zurück, anstatt eine Ausnahme auszulösen. Dies ist wichtig, weil die Verwendung des Nullbedingungsoperators für Auflistungen, die eine „IndexOutOfRangeException“ auslösen, fast immer überflüssig ist und Sicherheit vorgaukeln kann, wenn diese nicht gegeben ist. Berücksichtigen Sie für den Codeausschnitt mit dem Beispiel „Main“ und „args“ Folgendes:

public static void Main(string[] args)
{
  string directoryPath = args?[0];
  string searchPattern = args?[1];
  // ...
}

Gefährlich an diesem Beispiel ist, dass der Nullbedingungsoperator eine falsche Sicherheit vorgaukelt, wenn angenommen wird, dass das Element vorhanden sein muss, wenn „args“ nicht null ist. Dies ist natürlich nicht der Fall, weil das Element auch dann nicht vorhanden sein kann, wenn „args“ ungleich null ist. Da die Überprüfung der Elementanzahl mit „args?.Length“ bereits bestätigt, dass „args“ ungleich null ist, müssen Sie den Nullbedingungsoperator beim Indizieren der Auflistung nach dem Überprüfen von „length“ niemals zusätzlich verwenden. Vermeiden Sie daher die Verwendung des Nullbedingungsoperators in Kombination mit dem Indexoperator, wenn der Indexoperator eine „IndexOutOfRangeException“ für nicht vorhandene Indizes auslöst. Andernfalls wird eine nicht gegebene Codegültigkeit vorgegaukelt.

Standardkonstruktoren in Strukturen

Eine weitere Funktion von C# 6.0, die Sie kennen sollten, ist die Unterstützung für einen Standardkonstruktor (parameterlos) für einen Werttyp. Dies war zuvor unzulässig, weil der Konstruktor beim Initialisieren von Arrays, beim Festlegen des Standardwerts für ein Feld vom Typ „struct“ oder beim Initialisieren einer Instanz mit dem Standardoperator nicht aufgerufen wurde. In C# 6.0 sind Standardkonstruktoren jetzt zulässig. Bedenken Sie jedoch, dass diese nur aufgerufen werden, wenn der Werttyp mit dem neuen Operator instanziiert wird. Bei der Arrayinitialisierung und der expliziten Zuordnung des Standardwerts (oder der impliziten Initialisierung eines Felds vom Typ „struct“) wird der Standardkonstruktor umgangen.

Zum besseren Verständnis der Nutzung des Standardkonstruktors sehen Sie sich das Beispiel für die Klasse „ConsoleConfiguration“ an, das in Abbildung 9 gezeigt wird. Mithilfe eines Konstruktors, der über den neuen Operator wie in der Methode „CreateUsingNewIsInitialized“ gezeigt aufgerufen wird, werden Strukturen vollständig initialisiert. Wie Sie wahrscheinlich erwartet haben (und wie in Abbildung 9 gezeigt wird), wird Konstruktorverkettung vollständig unterstützt. Dabei kann ein Konstruktor einen anderen Konstruktor mithilfe des Schlüsselworts „this“ im Anschluss an die Konstruktordeklaration aufrufen.

Abbildung 9 – Deklarieren eines Standardkonstruktors für einen Werttyp

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
public struct ConsoleConfiguration
{
  public ConsoleConfiguration() :
    this(ConsoleColor.Red, ConsoleColor.Yellow, ConsoleColor.White)
  {
    Initialize(this);
  }
  public ConsoleConfiguration(ConsoleColor foregroundColorError,
    ConsoleColor foregroundColorInformation,
    ConsoleColor foregroundColorVerbose)
  {
    // All auto-properties and fields must be set before
    // accessing expression bodied members
    ForegroundColorError = foregroundColorError;
    ForegroundColorInformation = foregroundColorInformation;
    ForegroundColorVerbose = foregroundColorVerbose;
  }
   private static void Initialize(ConsoleConfiguration configuration)
  {
    // Load configuration from App.json.config file ...
  }
  public ConsoleColor ForegroundColorVerbose { get; }
  public ConsoleColor ForegroundColorInformation { get; }
  public ConsoleColor ForegroundColorError { get; }
  // ...
  // Equality implementation excluded for elucidation
}
[TestClass]
public class ConsoleConfigurationTests
{
  [TestMethod]
  public void DefaultObjectIsNotInitialized()
  {
    ConsoleConfiguration configuration = default(ConsoleConfiguration);
    AreEqual<ConsoleColor>(0, configuration.ForegroundColorError);
    ConsoleConfiguration[] configurations = new ConsoleConfiguration[42];
    foreach(ConsoleConfiguration item in configurations)
    {
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorError);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorInformation);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorVerbose);
    }
  }
  [TestMethod]
  public void CreateUsingNewIsInitialized()
  {
    ConsoleConfiguration configuration = new ConsoleConfiguration();
    AreEqual<ConsoleColor>(ConsoleColor.Red,
      configuration.ForegroundColorError);
    AreEqual<ConsoleColor>(ConsoleColor.Yellow,
      configuration.ForegroundColorInformation);
    AreEqual<ConsoleColor>(ConsoleColor.White,
      configuration.ForegroundColorVerbose);
  }
}

Zu Strukturen muss ein Schlüsselaspekt berücksichtigt werden: Alle Instanzenfelder und automatischen Eigenschaften (da diese Unterstützungsfelder besitzen) müssen vollständig initialisiert worden sein, bevor andere Instanzelemente aufgerufen werden. Als Ergebnis kann der Konstruktor im Beispiel in Abbildung 9 die Methode „Initialize“ erst aufrufen, nachdem alle Felder und automatischen Eigenschaften zugewiesen wurden. Wenn ein verketteter Konstruktor die gesamte erforderliche Initialisierung übernimmt und über einen Aufruf „this“ aufgerufen wird, ist der Compiler glücklicherweise intelligent genug, um zu erkennen, dass es nicht erforderlich ist, Daten erneut aus dem Text des nicht über „this“ aufgerufenen Konstruktors zu initialisieren. Abbildung 9 zeigt dies.

Optimierungen der automatischen Eigenschaften

Beachten Sie in Abbildung 9 außerdem, dass die drei Eigenschaften (für die keine expliziten Felder vorhanden sind) alle als automatische Eigenschaften (ohne Text) und nur ein Getter deklariert werden. Diese automatischen Nur-Getter-Eigenschaften sind eine Funktion von C# 6.0 zum Deklarieren schreibgeschützter Eigenschaften, die (intern) durch ein schreibgeschütztes Feld unterstützt werden. Aus diesem Grund können diese Eigenschaften nur im Konstruktor geändert werden.

Automatische Nur-Getter-Eigenschaften sind für Struktur- und Klassendeklarationen verfügbar. Sie sind jedoch für Strukturen besonders wichtig, weil eine bewährte Methode besagt, dass Stukturen unveränderbar sein sollten. Vor C# 6.0 waren ungefähr sechs Zeilen zum Deklarieren einer schreibgeschützten Eigenschaft und für ihre Initialisierung erforderlich. Jetzt ist nur noch eine einzeilige Deklaration und die Zuweisung im Konstruktor erforderlich. Die Deklaration unveränderbarer Strukturen ist daher nun nicht nur das richtige Programmiermuster für Strukturen, sondern auch das einfachere Muster – eine Änderung der vorherigen Syntax, in der die richtige Codierung mehr Aufwand verlangte, die sehr begrüßt wird.

Eine zweite Funktion für automatische Eigenschaften, die mit C# 6.0 eingeführt wurde, ist Unterstützung für Initialisierer. Ich kann „ConsoleConfiguration“ z. B. eine automatische Eigenschaft „DefaultConfig“ mit einem Initialisierer hinzufügen:

// Instance property initialization not allowed on structs.
static private Lazy<ConsoleConfiguration> DefaultConfig{ get; } =
  new Lazy<ConsoleConfiguration>(() => new ConsoleConfiguration());

Eine solche Eigenschaft würde ein Factorymuster für eine Instanz für den Zugriff auf eine ConsoleConfiguration-Standardinstanz bereitstellen. Beachten Sie, dass in diesem Beispiel „System.Lazy<T>“ genutzt und als Initialisierer während der Deklaration instanziiert wird, anstatt die automatischen Nur-Getter-Eigenschaften im Konstruktor zuzuweisen. Als Ergebnis wird nach dem Abschluss des Konstruktors die Instanz von „Lazy<ConsoleConfiguration>“ unveränderbar, und ein Aufruf von „DefaultConfig“ gibt immer die gleiche Instanz von „ConsoleConfiguration“ zurück.

Beachten Sie, dass Initialisierer für automatische Eigenschaften für Instanzelemente von Strukturen unzulässig sind (für Klassen sind sie natürlich zulässig).

Expression-Bodied-Methoden und automatische Eigenschaften

Eine weitere mit C# 6.0 eingeführte Funktion sind Expression-Bodied-Elemente. Diese Funktion ist für Eigenschaften und Methoden verfügbar und ermöglicht die Verwendung des Pfeiloperators (=>) zum Zuweisen eines Ausdrucks zu einer Eigenschaft oder einer Methode anstelle eines Anweisungstexts. Da die Eigenschaft „DefaultConfig“ im vorherigen Beispiel z. B. privat und vom Typ „Lazy<T>“ ist, ist für das Abrufen der tatsächlichen Standardinstanz von „ConsoleConfiguration“ eine Methode „GetDefault“ erforderlich:

static public ConsoleConfiguration GetDefault() => DefaultConfig.Value;

Beachten Sie jedoch, dass in diesem Codeausschnitt kein Methodentext vom Anweisungsblocktyp vorhanden ist. Die Methode wird stattdessen nur mit einem Ausdruck (nicht mit einer Anweisung) implementiert, dem als Präfix der Lambda-Pfeiloperator vorangestellt ist. Die Absicht besteht darin, eine einfache einzeilige Implementierung ohne Standardcode bereitzustellen, die mit oder ohne Parameter in der Methodensignatur funktionsfähig ist:

private static void LogExceptions(ReadOnlyCollection<Exception> innerExceptions) =>
  LogExceptionsAsync(innerExceptions).Wait();

Beachten Sie bezüglich der Eigenschaften, dass Expression-Bodied-Elemente nur für schreibgeschützte Eigenschaften (Nur-Getter-Eigenschaften) funktionieren. Abgesehen davon, dass keine Klammern auf den Bezeichner folgen, ist die Syntax mit der Syntax von Expression-Bodied-Methoden praktisch identisch. Im oben aufgeführten Beispiel „Person“ könnte ich schreibgeschützte Eigenschaften „FirstName“ und „LastName“ mithilfe der Expression-Bodied-Elemente implementieren. Abbildung 10 zeigt dies.

Abbildung 10 – Automatische Expression-Bodied-Eigenschaften

public class Person
{
  public Person(string name)
  {
    Name = name;
  }
  public Person(string firstName, string lastName)
  {
    Name = $"{firstName} {lastName}";
    Age = age;
  }
  // Validation ommitted for elucidation
  public string Name {get; set; }
  public string FirstName => Name.Split(' ')[0];
  public string LastName => Name.Split(' ')[1];
  public override string ToString() => "\{Name}(\{Age}";
}

Außerdem können Expression-Bodied-Eigenschaften auch für Indexelemente verwendet werden und beispielsweise ein Element aus einer internen Auflistung zurückgeben.

Wörterbuchinitialisierer

Auflistungen vom Typ „Dictionary“ sind ein sinnvolles Verfahren zum Definieren eines Name-Wert-Paars. Leider ist die Syntax für die Initialisierung suboptimal:

{ {"First", "Value1"}, {"Second", "Value2"}, {"Third", "Value3"} }

C# 6.0 enthält zur Verbesserung dieses Problems eine neue Syntax für die Wörterbuchzuweisung:

Dictionary<string, Action<ConsoleColor>> colorMap =
  new Dictionary<string, Action<ConsoleColor>>
{
  ["Error"] =               ConsoleColor.Red,
  ["Information"] =        ConsoleColor.Yellow,
  ["Verbose"] =            ConsoleColor.White
};

Zur Verbesserung der Syntax hat das Sprachteam den Zuweisungsoperator eingeführt, um ein Elementpaar zuzuweisen, das ein Such-Wert-Paar (Name-Wert-Paar) oder eine Zuordnung bildet. Die Suche ist der für das Wörterbuch deklarierte Indexwert (und Datentyp).

Ausnahmeoptimierungen

Für Ausnahmen wurden in C# 6.0 ebenfalls einige kleinere Sprachoptimierungen vorgenommen. Einerseits ist es nun möglich, await-Klauseln in catch- und finally-Blöcken zu verwenden, wie Abbildung 11 zeigt.

Abbildung 11 – Verwenden von „Await“ in Catch- und Finally-Blöcken

public static async Task<int> EncryptFilesAsync(string directoryPath, string searchPattern = "*")
{
  ConsoleColor color = Console.ForegroundColor;
  try
  {
  // ...
  }
  catch (System.ComponentModel.Win32Exception exception)
    if (exception.NativeErrorCode == 0x00042)
  {
    // ...
  }
  catch (AggregateException exception)
  {
    await LogExceptionsAsync(exception.InnerExceptions);
  }
  finally
  {
    Console.ForegroundColor = color;
    await RemoveTemporaryFilesAsync();
  }
}

Seit der Einführung von „await“ in C# 5.0 stellte sich heraus, dass Unterstützung für „await“ in catch- und finally-Blöcken weitaus gefragter war als ursprünglich erwartet. Das Muster des Aufrufens asynchroner Methoden aus einem catch- oder finally-Block ist recht gängig – insbesondere, wenn es um Bereinigung oder Protokollierung während dieser Zeiträume geht. In C# 6.0 ist dies nun endlich möglich.

Die zweite Funktion, die sich auf Ausnahmen bezieht (die in Visual Basic seit Version 1.0 verfügbar war), ist Unterstützung für Ausnahmefilter. Nach der Filterung anhand eines bestimmten Ausnahmetyps ist es nun möglich, eine if-Klausel anzugeben, um weiter einzuschränken, ob die Ausnahme vom catch-Block abgefangen wird. (Gelegentlich wurde diese Funktion auch für Nebeneffekte genutzt, z. B. für die Protokollierung von Ausnahmen, während diese auftreten, ohne eigentliche Ausnahmeverarbeitung auszuführen.) Wenn die Möglichkeit besteht, dass Ihre Anwendung ggf. lokalisiert wird, beachten Sie bei dieser Funktion unbedingt, dass das Abfangen bedingter Ausdrücke vermieden werden muss, die über Ausnahmemeldungen arbeiten, weil diese ohne Änderungen nach der Lokalisierung nicht mehr funktionieren.

Zusammenfassung

Ein letzter Punkt, der zu den Funktionen von C# 6.0 erwähnt werden sollte: Sie alle erfordern naturgemäß den C# 6.0-Compiler, der im Lieferumfang von Visual Studio 2015 oder höher enthalten ist, sie benötigen jedoch keine aktualisierte Version von Microsoft .NET Framework. Daher können Sie die Funktionen von C# 6.0 selbst dann verwenden, wenn Sie z. B. unter .NET Framework 4 kompilieren. Dies ist möglich, weil alle Features im Compiler implementiert sind und keine Abhängigkeiten von .NET Framework aufweisen.

Damit beende ich meine Vorstellung von C# 6.0. Die einzigen zwei verbleibenden Funktionen, die nicht behandelt wurden, sind Unterstützung zum Definieren einer benutzerdefinierten Erweiterungsmethode „Add“, die für Auflistungsinitialisierer hilfreich ist, und einige kleine Verbesserungen bei der Überladungsauflösung. Zusammengefasst lässt sich sagen, dass C# 6.0 Ihren Code nicht radikal ändert – zumindest nicht so, wie es durch Generika oder LINQ der Fall war. Die neue Version vereinfacht jedoch die richtigen Codierungsmuster. Der Nullbedingungsoperator eines Delegaten ist hierfür wahrscheinlich das beste Beispiel, doch auch die anderen Funktionen wie etwa Zeichenfolgeninterpolation, der Operator „nameof“ und Optimierungen der automatischen Eigenschaften (insbesondere schreibgeschützte Eigenschaften) stehen für Verbesserungen.

Weitere Informationen erhalten Sie hier:

  • What’s new in C# 6.0 von Mads Torgersen (Video): bit.ly/CSharp6Mads
  • C#-Blog von Mark Michaelis mit Aktualisierungen zu Version 6.0 seit der Veröffentlichung dieses Artikels: itl.tc/csharp
  • Diskussionen zur Sprache C# 6.0: roslyn.codeplex.com/discussions

Weitere Informationen finden Sie außerdem in der neuen Auflage meines Buchs „Essential C# 6.0“, das im 2. Quartal 2015 erscheinen wird (intellitect.com/EssentialCSharp).

Wenn Sie diesen Text lesen, werden die Diskussionen über die Funktionen von C# 6.0 wahrscheinlich abgeschlossen sein. Es steht jedoch kaum außer Frage, dass sich Microsoft in eine neue Richtung entwickelt, indem in plattformübergreifende Entwicklung mithilfe bewährter Open Source-Methoden investiert wird, die der Entwicklercommunity ermöglichen, an der Entwicklung großartiger Software teilzuhaben. Aus diesem Grund können Sie schon bald die frühen Entwurfsdiskussionen zu C# 7.0 mitverfolgen, weil die Diskussionen zu dieser Version in einem Open Source-Forum stattfinden.


Mark Michaelis (itl.tc/Mark) ist der Gründer von IntelliTect und als leitender Systemarchitekt und Dozent tätig. Er ist seit 1996 Microsoft MVP für C#, Visual Studio Team System (VSTS) und das Windows SDK. 2007 wurde er zum Microsoft Regional Director ernannt. Er arbeitet auch in mehreren Teams zur Designüberprüfung von Microsoft-Software mit, einschließlich C#, der Connected Systems Division und VSTS. Michaelis hält Vorträge auf Entwicklerkonferenzen, hat zahlreiche Artikel und Bücher geschrieben und arbeitet zurzeit an der nächsten Ausgabe von „Essential C#“ (Addison-Wesley Professional).

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Mads Torgersen