C#

Vorschau auf C# 6.0

Mark Michaelis

Wenn Sie diesen Artikel lesen, ist Build, die Microsoft-Entwicklerkonferenz, bereits vorüber, und Entwickler denken darüber nach, wie sie auf die vorgestellten Neuerungen reagieren sollen: sofort übernehmen, mit Zurückhaltung abwarten oder erst einmal ignorieren. Für .NET/C#-Entwickler war zweifellos die wichtigste Ankündigung das Release der nächsten Version des C#-Compilers („Roslyn“) als Open Source. Die Sprachverbesserungen hängen damit zusammen. Auch wenn Sie nicht vorhaben, C# vNext (ab jetzt von mir inoffiziell als C# 6.0 bezeichnet) sofort zu verwenden, sollten Sie zumindest die neuen Features zur Kenntnis nehmen und auf diejenigen achten, für die es sich lohnen könnte, direkt loszulegen.

In diesem Artikel gehe ich ausführlich darauf ein, was zum Zeitpunkt, als dieser Artikel entstand (März 2014), in C# 6.0 verfügbar war oder als Open Source von roslyn.codeplex.com heruntergeladen werden konnte. Im Folgenden behandle ich diese Features als ein Release mit der Bezeichnung März-Vorschau. Die C#-spezifischen Features dieser März-Vorschau sind vollständig im Compiler implementiert, ohne Abhängigkeit von einem aktualisierten Microsoft .NET Framework oder einer aktualisierten Laufzeitumgebung. Sie können C# 6.0 also in Ihre Entwicklung übernehmen, ohne .NET Framework für die Entwicklung oder Bereitstellung aktualisieren zu müssen. Tatsächlich ist für die Installation des C# 6.0-Compilers aus diesem Release kaum mehr als das Installieren einer Visual Studio 2013-Erweiterung nötig, die wiederum die MSBuild-Zieldateien aktualisiert.

Bedenken Sie bei der Vorstellung der einzelnen C# 6.0-Features folgende Punkte:

  • Gab es in der Vergangenheit angemessene Möglichkeiten für die Programmierung derselben Funktionalität, sodass dieses Feature in erster Linie ein syntaktisches „Bonbon“ – eine Verkürzung oder einen optimierten Ansatz – darstellt? Das Filtern von Ausnahmen hat beispielsweise keine Entsprechung in C# 5.0, primäre Konstruktoren dagegen schon.
  • Ist das Feature in der März-Vorschau enthalten? Die meisten Features, die ich beschreibe, sind in der März-Vorschau verfügbar, einige aber nicht (wie ein neues binäres Literal).
  • Haben Sie Feedback für das Team hinsichtlich des neuen Sprachfeatures? Das Team steht noch relativ am Anfang des Releasezyklus und ist an Ihrer Meinung sehr interessiert (Feedbackanleitungen finden Sie unter msdn.com/Roslyn).

Die Beschäftigung mit diesen Fragen kann Ihnen helfen, die Bedeutung der neuen Features in Bezug auf Ihre eigenen Entwicklungsbemühungen einzuschätzen.

Indizierte Member und Elementinitialisierer

Sehen Sie sich zuerst den Komponententest in Abbildung 1 an.

Abbildung 1 Zuweisen einer Auflistung mit einem Auflistungsinitialisierer (in C# 3.0 hinzugefügt)

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
// ...
[TestMethod]
public void DictionaryIndexWithoutDotDollar()
{
  Dictionary<string, string> builtInDataTypes = 
    new Dictionary<string, string>()
  {
    {"Byte", "0 to 255"},
    // ...
    {"Boolean", "True or false."},
    {"Object", "An Object."},
    {"String", "A string of Unicode characters."},
    {"Decimal", "±1.0 × 10e-28 to ±7.9 × 10e28"}
  };
  Assert.AreEqual("True or false.", builtInDataTypes["Boolean"]);
}

Bei Abbildung 1 handelt es sich nur um eine Name/Wert-Auflistung, obwohl die Syntax das etwas verschleiert. Die Syntax könnte deshalb viel klarer sein: <Index> = <Wert>. C# 6.0 ermöglicht dies durch die C#-Objektinitialisierer und einer neuen Syntax für Indexmember. Der folgende Code zeigt int-basierte Elementinitialisierer:

var cppHelloWorldProgram = new Dictionary<int, string>
{
  [10] = "main() {",
  [20] = "    printf(\"hello, world\")",
  [30] = "}"
};
Assert.AreEqual(3, cppHelloWorldProgram.Count);

Obwohl in diesem Code der integer-Datentyp für den Index verwendet wird, kann Dictionary<TKey,TValue> einen beliebigen Typ als Index unterstützen (sofern er IComparable<T> unterstützt). Im nächsten Beispiel wird eine Zeichenfolge für den Index-Datentyp verwendet, und die Elementwerte werden mit einem indizierten Memberinitialisierer festgelegt:

Dictionary<string, string> builtInDataTypes =
  new Dictionary<string, string> {
    ["Byte"] = "0 to 255",
    // ...
    // Error: mixing object initializers and
    // collection initializers is invalid
    // {" Boolean", "True or false."},
    ["Object"] = "An Object.",
    ["String"] = "A string of Unicode characters.",
    ["Decimal"] = "±1.0 × 10e?28 to ±7.9 × 10e28"
  };

Die neue Initialisierung von Indexmembern enthält jetzt einen neuen $-Operator. Diese Syntax für indizierte Zeichenfolgenmember ist speziell aufgrund der überwiegenden Verwendung der zeichenfolgenbasierten Indizierung vorgesehen. Mit dieser neuen Syntax (siehe Abbildung 2) können Sie Elementwerte in der Syntax eher wie in einem dynamischen Memberaufruf (in C# 4.0 eingeführt) als in der Zeichenfolgennotation wie im vorigen Beispiel zuweisen.

Abbildung 2 Initialisieren einer Auflistung mit einer indizierten Memberzuweisung als Teil des Elementinitialisierers

[TestMethod]
public void DictionaryIndexWithDotDollar()
{
  Dictionary<string, string> builtInDataTypes = 
    new Dictionary<string, string> {
    $Byte = "0 to 255",   // Using indexed members in element initializers
    // ...
    $Boolean = "True or false.",
    $Object = "An Object.",
    $String = "A string of Unicode characters.",
    $Decimal = "±1.0 × 10e?28 to ±7.9 × 10e28"
  };
  Assert.AreEqual("True or false.", builtInDataTypes.$Boolean);
}

Sehen Sie sich den Aufruf der AreEqual-Funktion an, um den $-Operator zu verstehen. Der Dictionary-Memberaufruf erfolgt mit „$Boolean“ für die builtInDataTypes-Variable, obwohl für Dictionary kein „Boolean“-Member vorhanden ist. Ein derartiger expliziter Member wird nicht benötigt, weil der $-Operator den indizierten Member im Wörterbuch aufruft, was gleichbedeutend mit dem Aufruf von buildInDataTypes["Boolean"] ist.

Wie bei allen zeichenfolgenbasierten Operatoren erfolgt zur Kompilierzeit keine Überprüfung, ob das Zeichenfolgenindexelement (z. B. „Boolean“) im Wörterbuch vorhanden ist. Aus diesem Grund kann jeder gültige C#-Membername (mit Unterscheidung zwischen Groß- und Kleinschreibung) nach dem $-Operator erscheinen.

Denken Sie zum vollständigen Verständnis der Syntax von indizierten Membern an die häufige Verwendung von Zeichenfolgenindexern in lose typisierten Datenformaten, wie XML, JSON, CSV und sogar in Datenbanksuchvorgängen (ohne Codegenerierungstrick durch Entity Framework). Abbildung 3 zeigt beispielsweise, wie einfach der indizierte Zeichenfolgenmember mit dem Newtonsoft.Json-Framework verwendet werden kann.

Abbildung 3 Nutzung der indizierten Methode mit JSON-Daten

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
// ...
[TestMethod]
public void JsonWithDollarOperatorStringIndexers()
{
  // Additional data types eliminated for elucidation
  string jsonText = @"
    {
      'Byte':  {
        'Keyword':  'byte',
        'DotNetClassName':  'Byte',
        'Description':  'Unsigned integer',
        'Width':  '8',
        'Range':  '0 to 255'
                },
      'Boolean':  {
        'Keyword':  'bool',
        'DotNetClassName':  'Boolean',
        'Description':  'Logical Boolean type',
        'Width':  '8',
        'Range':  'True or false.'
                  },
    }";
  JObject jObject = JObject.Parse(jsonText);
  Assert.AreEqual("bool", jObject.$Boolean.$Keyword);
}

Ein letzter Punkt ist noch zu beachten, falls es nicht bereits offensichtlich ist: Die $-Operatorsyntax funktioniert nur mit Indizes vom Typ „String“ (wie Dictionary<string, …>).

Automatische Eigenschaftsinitialisierer

Das Initialisieren einer Klasse kann gegenwärtig ziemlich aufwendig sein. Denken Sie zum Beispiel an den simplen Fall eines benutzerdefinierten Auflistungstyps (wie Queue<T>), der intern eine private System.Collections.Generic.List<T>-Eigenschaft für eine Liste mit Einträgen verwaltet. Beim Instanziieren der Auflistung müssen Sie die Warteschlange mit der Liste der Einträge initialisieren, die sie enthalten soll. Für die sinnvollsten Optionen, dies mit einer Eigenschaft auszuführen, brauchen Sie aber ein dahinter liegendes Feld mit einem Initialisierer oder einem else-Konstruktor; diese Kombination verdoppelt praktisch den Umfang des erforderlichen Codes.

C# 6.0 bietet eine Syntaxverkürzung: automatische Eigenschaftsinitialisierer. Sie können jetzt automatische Eigenschaften direkt zuweisen, wie hier gezeigt:

class Queue<T>
{
  private List<T> InternalCollection { get; } = 
    new List<T>; 
  // Queue Implementation
  // ...
}

Beachten Sie, dass die Eigenschaft in diesem Fall schreibgeschützt ist (es ist kein Setter definiert). Der Eigenschaft kann aber trotzdem bei der Deklaration ein Wert zugewiesen werden. Lese-/Schreibeigenschaften mit einem Setter werden auch unterstützt.

Primäre Konstruktoren

Wie bei Eigenschaftsinitialisierern bietet C# 6.0 auch syntaktische Verkürzungen für die Definition von Konstruktoren. Sehen Sie sich die häufige Verwendung des C#-Konstruktors und der Eigenschaftsvalidierung in Abbildung 4 an.

Abbildung 4 Ein allgemeines Konstruktormuster

[Serializable]
public class Patent
{
  public Patent(string title , string yearOfPublication)
  {
    Title = title;
    YearOfPublication = yearOfPublication;
  }
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    : this(title, yearOfPublication)
  {
    Inventors = new List<string>();
    Inventors.AddRange(inventors);
  }
  [NonSerialized] // For example
  private string _Title;
  public string Title
  {
    get
    {
      return _Title;
    }
    set
    {
      if (value == null)
      {
        throw new ArgumentNullException("Title");
      }
      _Title = value;
    }
  }
  public string YearOfPublication { get; set; }
  public List<string> Inventors { get; private set; }
  public string GetFullName()
  {
    return string.Format("{0} ({1})", Title, YearOfPublication);
  }
}

In diesem allgemeinen Konstruktormuster gibt es mehrere Punkte, die beachtet werden müssen:

  1. Weil die Validierung einer Eigenschaft erforderlich ist, muss das zugrunde liegende Eigenschaftsfeld deklariert werden.
  2. Die Konstruktorsyntax ist ziemlich ausführlich mit häufigen Wiederholungen von „public class Patent{  public Patent(...“.
  3. „Title“ erscheint in verschiedenen Versionen von Groß-/Kleinschreibung sieben Mal für ein ziemlich simples Szenario – die Validierung nicht eingerechnet.
  4. Für die Initialisierung einer Eigenschaft ist ein expliziter Verweis auf die Eigenschaft im Konstruktor erforderlich.

Um ohne Auswirkungen auf die Eigenart der Sprache den Aufwand für dieses Muster zu verringern, führt C# 6.0 Eigenschaftsinitialisierer und primäre Konstruktoren ein (siehe Abbildung 5).

Abbildung 5 Verwenden eines primären Konstruktors

[Serializable]
public class Patent(string title, string yearOfPublication)
{
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    :this(title, yearOfPublication)
  {
    Inventors.AddRange(inventors);
  }
  private string _Title = title;
  public string Title
  {
    get
    {
      return _Title;
    }
    set
    {
      if (value == null)
      {
        throw new ArgumentNullException("Title");
      }
      _Title = value;
    }
  }
  public string YearOfPublication { get; set; } = yearOfPublication;
  public List<string> Inventors { get; } = new List<string>();
  public string GetFullName()
  {
    return string.Format("{0} ({1})", Title, YearOfPublication);
  }
}

Zusammen mit Eigenschaftsinitialisierern vereinfacht die primäre Konstruktorsyntax die C#-Konstruktorsyntax:

  • Automatische Eigenschaften, schreibgeschützte (siehe die Inventors-Eigenschaft nur mit einem Getter) oder mit Lese-/Schreibzugriff (siehe die YearOfPublication-Eigenschaft mit einem Setter und einem Getter), unterstützen die Initialisierung von Eigenschaften, sodass der Initialwert der Eigenschaft als Teil der Eigenschaftsdeklaration zugewiesen werden kann. Die Syntax entspricht der Zuweisung eines Standardwerts zu Feldern bei der Deklaration (z. B. „declaration assigned _Title“).
  • Standardmäßig kann auf Parameter von primären Konstruktoren nicht von außerhalb eines Initialisierers zugegriffen werden. Beispielsweise ist für die Klasse kein yearOfPublication-Feld deklariert.
  • Bei der Verwendung von Eigenschaftsinitialisierern für schreibgeschützte Eigenschaften (nur Getter) kann keine Validierung erfolgen. (Dies ist auf die Tatsache zurückzuführen, dass in der zugrunde liegenden IL-Implementierung der primäre Konstruktorparameter dem dahinter liegenden Feld zugewiesen wird. Außerdem ist erwähnenswert, dass das dahinter liegende Feld in der IL als schreibgeschützt definiert wird, wenn die automatische Eigenschaft nur einen Getter hat.)
  • Wenn angegeben, muss der primäre Konstruktor immer als letzter in der Konstruktorkette ausgeführt werden (deshalb kann er einen this(...)-Initialisierer haben).

Denken Sie als weiteres Beispiel an die Deklaration einer Struktur, die laut Richtlinien unveränderlich sein muss. Im Folgenden wird eine eigenschaftsbasierte Implementierung (im Gegensatz zum atypischen Ansatz mit öffentlichen Feldern) gezeigt:

struct Pair(string first, string second, string name)
{
  public Pair(string first, string second) : 
    this(first, second, first+"-"+second)
  {
  }
  public string First { get; } = second;
  public string Second { get; } = first;
  public string Name { get; } = name;
  // Possible equality implementation
  // ...
}

Die Implementierung von Pair enthält einen zweiten Konstruktor, der den primären Konstruktor aufruft. Im Allgemeinen müssen alle Konstruktoren – direkt oder indirekt – den primären Konstruktor über einen Aufruf des this(...)-Initialisierers aufrufen. Das heißt, nicht alle Konstruktoren müssen den primären Konstruktor direkt aufrufen, aber am Ende der Konstruktorkette wird der primäre Konstruktor aufgerufen. Das ist erforderlich, weil der primäre Konstruktor den Initialisierer des Basiskonstruktors aufruft und dadurch ein gewisser Schutz vor häufigen Initialisierungsfehlern besteht. (Wie in C# 1.0 ist es weiterhin möglich, eine Struktur ohne Aufruf eines Konstruktors zu instanziieren, wie zum Beispiel beim Instanziieren eines Arrays der Struktur.)

Je nachdem, ob der primäre Konstruktor sich in einer benutzerdefinierten Struktur oder in einem Klassen-Datentyp befindet, ist der Aufruf des Basiskonstruktors entweder implizit (und ruft daher den Standardkonstruktor der Basisklasse auf) oder explizit, indem ein bestimmter Basisklassenkonstruktor aufgerufen wird. Im zweiten Fall wird für den Aufruf eines bestimmten System.Exception-Konstruktors bei einer benutzerdefinierten Ausnahme der Zielkonstruktor nach dem primären Konstruktor angegeben:

class UsbConnectionException : 
  Exception(string message, Exception innerException,
  HidDeviceInfo hidDeviceInfo) :base(message, innerException)
{
  public HidDeviceInfo HidDeviceInfo { get;  } = hidDeviceInfo;
}

Ein wichtiges Detail bei primären Konstruktoren, auf das Sie achten müssen, bezieht sich auf die Vermeidung von doppelten, möglicherweise inkompatiblen, primären Konstruktoren in partiellen Klassen: Bei mehreren Teilen einer partiellen Klasse darf der primäre Konstruktor nur in einer Klassendeklaration definiert werden, und nur in diesem primären Konstruktor darf der Basiskonstruktor aufgerufen werden.

Bei primären Konstruktoren in der Implementierung in dieser März-Vorschau gibt es einen bedeutenden Nachteil: Es besteht keine Möglichkeit, die Parameter von primären Konstruktoren zu validieren. Und weil Eigenschaftsinitialisierer nur für automatische Eigenschaften gültig sind, kann auch keine Validierung in der Eigenschaftsimplementierung vorgesehen werden, die möglicherweise zudem öffentliche Eigenschaftensetter für die Zuweisung ungültiger Nachinstanziierungen offenlegen könnte. Die einfachste Problemumgehung ist derzeit, keine primären Konstruktoren zu verwenden, wenn die Validierung wichtig ist.

Ein damit in Zusammenhang stehenden Features, der „Feldparameter“, befindet sich in Planung, wenngleich gegenwärtig nur versuchsweise. Durch Einbeziehen eines Zugriffsmodifizierers in den primären Konstruktorparameter (z. B. private string „Titel“) wird der Parameter in den Klassengeltungsbereich als Feld namens „Titel“ übernommen (mit dem Namen und in der Schreibweise des Parameters). Dadurch ist „Titel“ in der Title-Eigenschaft oder in jedem anderen Instanzklassenmember verfügbar. Außerdem ermöglicht der Zugriffsmodifizierer, dass die gesamte Feldsyntax angegeben werden kann, auch weitere Modifizierer wie „readonly“ oder sogar Attribute wie die folgenden: 

public class Person(
  [field: NonSerialized] private string firstName, string lastName)

Ohne den Zugriffsmodifizierer sind andere Modifizierer (und Attribute) nicht zulässig. Der Zugriffsmodifizierer legt fest, dass die Felddeklaration inline mit dem primären Konstruktor erscheinen soll.

(In dem Material, das ich zum Zeitpunkt, als dieser Artikel entstand, zur Verfügung hatte, war die Implementierung von Feldparametern nicht enthalten, aber das Sprachteam hat mir versichert, dass es in der Microsoft Build-Version enthalten sein wird. Wenn Sie dies lesen, sollte es daher möglich sein, Feldparameter zu testen. Zögern Sie aber in Anbetracht der relativen „Neuheit“ dieses Features nicht, Ihr Feedback unter msdn.com/Roslyn abzugeben, damit es berücksichtigt werden kann, bevor die Entwicklung für Änderungen zu weit fortgeschritten ist.)

Statische Using-Anweisungen

Ein weiteres „syntaktisches Bonbon“ ist die Einführung des C# 6.0-Features „using static“. Mit diesem Feature können Sie beim Aufruf einer statischen Methode einen expliziten Verweis auf den Typ vermeiden. Darüber hinaus können Sie mit „using static“ nur die Erweiterungsmethoden für eine bestimmte Klasse statt aller Erweiterungsmethoden in einem Namespace einführen. Abbildung 6 zeigt ein „Hello World”-Beispiel mit „using static“ für System.Console.

Abbildung 6 Vereinfachen der Übersichtlichkeit des Code mit „using static“

using System;
using System.Console;
public class Program
{
  private static void Main()
  {
    ConsoleColor textColor = ForegroundColor;
    try
    {
      ForegroundColor = ConsoleColor.Red;
      WriteLine("Hello, my name is Inigo Montoya... Who are you?: ");
      ForegroundColor = ConsoleColor.Green;
      string name = ReadLine(); // Respond: No one of consequence
      ForegroundColor = ConsoleColor.Red;
      WriteLine("I must know.");
      ForegroundColor = ConsoleColor.Green;
      WriteLine("Get used to disappointment");
    }
    finally
    {
      ForegroundColor = textColor;
    }
  }
}

In diesem Beispiel wurde der Console-Qualifizierer insgesamt neunmal verwendet. Zugegeben, das Beispiel ist übertrieben, aber es zeigt den springenden Punkt. Häufig bringt ein Typpräfix für einen statischen Member (einschließlich Eigenschaften) keine nennenswerten Vorteile, und durch Weglassen des Typpräfixes kann der Code leichter geschrieben und gelesen werden.

Ein zweites (geplantes) Feature von „using static“, das zwar in der März-Vorschau noch nicht funktioniert, befindet sich in der Diskussion. Dieses Feature unterstützt den Import von Erweiterungsmethoden nur eines bestimmten Typs. Denken Sie zum Beispiel an einen Hilfsnamespace, der zahlreiche statische Typen mit Erweiterungsmethoden umfasst. Ohne „using static“ werden alle (oder keine) Erweiterungsmethoden in diesem Namespace importiert. Mit „using static“ können Sie die für einen bestimmten Typ – nicht für den allgemeineren Namespace – verfügbaren Erweiterungsmethoden identifizieren. Somit könnten Sie einen LINQ-Standardabfrageoperator einfach durch Angabe von „using System.Linq.Enumerable;“ statt des gesamten System.Linq-Namespace aufrufen.

Leider ist dieser Vorteil nicht immer verfügbar (zumindest nicht in der März-Vorschau), weil nur statische Typen „using static“ unterstützen. Deshalb gibt es in Abbildung 6 auch keine „using System.ConsoleColor“-Anweisung. Ob in C# 6.0 diese Einschränkung beibehalten wird, lässt sich bei dem gegenwärtigen Stand der Vorschau noch nicht sagen. Was meinen Sie?

Deklarationsausdrücke

Es ist nicht ungewöhnlich, dass Sie mitten beim Schreiben einer Anweisung eine Variable speziell für diese Anweisung deklarieren müssen. Hier zwei Beispiele:

  • Sie bemerken beim Programmieren einer int.TryParse-Anweisung, dass eine Variable für das out-Argument deklariert sein muss, in die die Analyseergebnisse gespeichert werden sollen.
  • Sie entdecken beim Schreiben einer for-Anweisung, dass Sie eine Auflistung (z. B. ein LINQ-Abfrageergebnis) zwischenspeichern müssen, damit die Abfrage nicht mehrmals ausgeführt werden muss. Um die Variable zu deklarieren, unterbrechen Sie Ihre Überlegungen zum Schreiben der for-Anweisung.

Für diese und ähnliche Situationen enthält C# 6.0 Deklarationsausdrücke. Das heißt, Sie müssen Variablendeklarationen nicht nur auf Anweisungen beschränken, sondern können sie auch in Ausdrücken verwenden. Abbildung 7 zeigt zwei Beispiele hierfür.

Abbildung 7 Beispiele für Deklarationsausdrücke

public string FormatMessage(string attributeName)
{
  string result;
  if(! Enum.TryParse<FileAttributes>(attributeName, 
    out var attributeValue) )
  {
    result = string.Format(
      "'{0}' is not one of the possible {2} option combinations ({1})",
      attributeName, string.Join(",", string[] fileAtrributeNames =
      Enum.GetNames(typeof (FileAttributes))),
      fileAtrributeNames.Length);
  }
  else
  {
    result = string.Format("'{0}' has a corresponding value of {1}",
      attributeName, attributeValue);
  }
  return result;
}

Als erstes Beispiel in Abbildung 7 wird die attributeValue-Variable inline mit dem Aufruf von Enum.TryParse und nicht im Voraus in einer separaten Deklaration deklariert. Ebenso erscheint die Deklaration von fileAttributeNames gleich im Aufruf von string.Join. Dadurch können Sie später in derselben Anweisung auf Length zugreifen. (fileAttributeNames.Length ist der Ersetzungsparameter {2} im string.Format-Aufruf, obwohl er weiter oben in der Format-Zeichenfolge erscheint – dadurch kann fileAttributeNames deklariert werden, bevor darauf zugegriffen wird.)

Der Geltungsbereich eines Deklarationsausdrucks ist lose als Geltungsbereich der Anweisung definiert, in der der Ausdruck erscheint. In Abbildung 7 entspricht der Geltungsbereich von attributeValue dem der if-else-Anweisung, was ermöglicht, dass in den true- und false-Blöcken der Bedingung darauf zugegriffen werden kann. Entsprechend ist fileAttributeNames nur in der ersten Hälfte der if-Anweisung verfügbar, in dem Teil, der mit dem Geltungsbereich des string.Format-Anweisungsaufrufs übereinstimmt.

Der Compiler aktiviert nach Möglichkeit die Verwendung von implizit typisierten Variablen (var) für die Deklaration, indem der Datentyp vom Initialisierer (Deklarationszuweisung) abgeleitet wird. Bei out-Argumenten kann aber die Signatur des Aufrufziels verwendet werden, um implizit typisierte Variablen zu unterstützen, auch wenn kein Initialisierer vorhanden ist. Typableitungen sind aber nicht immer möglich und außerdem sind sie unter dem Gesichtspunkt der Lesbarkeit nicht immer die beste Wahl. Im Fall von TryParse in Abbildung 7 funktioniert var beispielsweise nur, weil das Typargument (FileAttributes) angegeben ist. Ohne dieses Typargument würde die var-Deklaration nicht kompiliert werden und stattdessen wäre der explizite Datentyp erforderlich:

Enum.TryParse(attributeName, out FileAttributes attributeValue)

Im zweiten Beispiel für einen Deklarationsausdruck in Abbildung 7 erscheint eine explizite Deklaration von string[], um den Datentyp als ein Array (statt z. B. als List<string>) zu kennzeichnen. Die folgende Richtlinie ist Standard bei der allgemeinen Verwendung von var: Vermeiden Sie implizit typisierte Variablen, wenn der Ergebnisdatentyp nicht klar ist.

Die Beispiele für Deklarationsausdrücke in Abbildung 7 könnten alle durch einfaches Deklarieren der Variablen in einer Anweisung vor ihrer Zuweisung programmiert werden.

Verbesserungen bei der Ausnahmebehandlung

In C# 6.0 gibt es zwei neue Features für die Ausnahmebehandlung. Die erste Verbesserung betrifft die async- und await-Syntax und die zweite die Unterstützung für Ausnahmefilter.

Durch die Einführung der (kontextuellen) Schlüsselwörter async und await in C# 5.0 erhielten Entwickler eine relativ einfache Möglichkeit zum Programmieren des aufgabenbasierten asynchronen Musters (TAP, Task-based Asynchronous Pattern). Der Compiler übernimmt hierbei die mühsame und komplexe Transformation von C#-Code in eine zugrunde liegende Reihe von Aufgabenfortsetzungen. Leider konnte das Team die Unterstützung für await in catch- und finally-Blöcken nicht in dieses Release aufnehmen. Wie sich herausstelle, war der Bedarf für diese Art des Aufrufs sogar noch größer als ursprünglich angenommen. Daher mussten C# 5.0-Programmierer erhebliche Problemumgehungen anwenden (z. B. die Verwendung des awaiter-Musters). In C# 6.0 ist dieser Mangel behoben; jetzt können await-Aufrufe aus catch- und finally-Blöcken erfolgen (in try-Blöcken wurden sie bereits unterstützt), wie in Abbildung 8 dargestellt.

Abbildung 8 await-Aufrufe in einem catch-Block

try
{
  WebRequest webRequest =
    WebRequest.Create("http://IntelliTect.com");
  WebResponse response =
    await webRequest.GetResponseAsync();
  // ...
}
catch (WebException exception)
{
  await WriteErrorToLog(exception);
}

Die andere Verbesserung der Ausnahmebehandlung in C# 6.0, die Unterstützung für Ausnahmefilter, bringt die Sprache auf den Stand anderer .NET-Sprachen, nämlich Visual Basic .NET und F#. Abbildung 9 zeigt die Details dieses Features.

Abbildung 9 Verwenden von Ausnahmefiltern zum Kennzeichnen der abzufangenden Ausnahme

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.ComponentModel;
using System.Runtime.InteropServices;
// ...
[TestMethod][ExpectedException(typeof(Win32Exception))]
public void ExceptionFilter_DontCatchAsNativeErrorCodeIsNot42()
{
  try
  {
    throw new Win32Exception(Marshal.GetLastWin32Error());
  }
  catch (Win32Exception exception) 
    if (exception.NativeErrorCode == 0x00042)
  {
    // Only provided for elucidation (not required).
    Assert.Fail("No catch expected.");
  }
}

Beachten Sie den zusätzlichen if-Ausdruck nach dem catch-Ausdruck. Der catch-Block überprüft jetzt nicht nur, ob die Ausnahme den Typ Win32Exception hat (oder davon abgeleitet ist), sondern auch weitere Bedingungen – in diesem Beispiel den speziellen Wert des Fehlercodes. Im Komponententest in Abbildung 9 wird erwartet, dass die Ausnahme nicht im catch-Block abgefangen wird – obwohl der Ausnahmetyp zutrifft –, sondern dass die Ausnahme vom ExpectedException-Attribut in der letzten Testmethode behandelt wird.

Im Gegensatz zu einigen der weiter oben behandelten C# 6.0-Features (wie z. B. der primäre Konstruktor) gab es vor C# 6.0 keine gleichwertige Alternative zum Programmieren von Ausnahmefiltern. Bis jetzt bestand der einzige Weg darin, alle Ausnahmen eines bestimmten Typs abzufangen, den Ausnahmekontext explizit zu prüfen und dann die Ausnahme erneut auszulösen, falls der aktuelle Zustand kein gültiges Szenario zum Abfangen der Ausnahme war. Mit anderen Worten: Das Filtern von Ausnahmen in C# 6.0 ermöglicht eine Funktionalität, die bislang in C# nicht in gleicher Weise möglich war.

Zusätzliche numerische Literalformate

Obwohl bisher noch nicht in der März-Vorschau implementiert, wird in C# 6.0 ein Zifferntrennzeichen, der Unterstrich (_), zum Trennen von Ziffern in numerischen Literalen (dezimal, hexadezimal oder binär) eingeführt. Die Ziffern können in jede, für Ihr Szenario sinnvolle Gruppe unterteilt werden. Beispielsweise kann der Maximalwert eines Integer-Datentyps nach Tausendern gruppiert werden:

int number = 2_147_483_647;

Die Größe einer Zahl, gleichgültig ob sie dezimal, hexadezimal oder binär vorliegt, kann so besser erkannt werden.

Das Zifferntrennzeichen ist wahrscheinlich besonders für das neue numerische binäre C# 6.0-Literal hilfreich. Ein binäres Literal könnte die Wartbarkeit von Programmen mit binären logischen und Flag-basierten Enumerationen verbessern, auch wenn dies nicht in jedem Programm benötigt wird. Sehen Sie sich dazu z. B. die FileAttribute-Enumeration in Abbildung 10 an.

Abbildung 10 Zuweisen eines binären Literals für Enumerationswerte

[Serializable][Flags]
[System.Runtime.InteropServices.ComVisible(true)]
public enum FileAttributes
{
  ReadOnly =          0b00_00_00_00_00_00_01, // 0x0001
  Hidden =            0b00_00_00_00_00_00_10, // 0x0002
  System =            0b00_00_00_00_00_01_00, // 0x0004
  Directory =         0b00_00_00_00_00_10_00, // 0x0010
  Archive =           0b00_00_00_00_01_00_00, // 0x0020
  Device =            0b00_00_00_00_10_00_00, // 0x0040
  Normal =            0b00_00_00_01_00_00_00, // 0x0080
  Temporary =         0b00_00_00_10_00_00_00, // 0x0100
  SparseFile =        0b00_00_01_00_00_00_00, // 0x0200
  ReparsePoint =      0b00_00_10_00_00_00_00, // 0x0400
  Compressed =        0b00_01_00_00_00_00_00, // 0x0800
  Offline =           0b00_10_00_00_00_00_00, // 0x1000
  NotContentIndexed = 0b01_00_00_00_00_00_00, // 0x2000
  Encrypted =         0b10_00_00_00_00_00_00  // 0x4000
}

Mit binären numerischen Literalen können Sie jetzt deutlicher zeigen, welche Flags gesetzt sind und welche nicht. Dadurch wird die in den Kommentaren gezeigte hexadezimale Notation oder der bei der Kompilierung berechnete Verschiebeansatz ersetzt:

Encrypted = 1<<14.

(Entwickler, die dieses Feature sofort ausprobieren möchten, können das in Visual Basic .NET mit dem März-Vorschaurelease tun.)

Zusammenfassung

Angesichts dieser Sprachänderungen werden Sie feststellen, dass C# 6.0 nichts wirklich Revolutionäres oder Weltbewegendes enthält. Im Vergleich zu anderen wichtigen Releases, wie Generika in C# 2.0, LINQ in C# 3.0 oder TAP in C# 5.0, ist C# 6.0 eher ein „Punkt“- als ein Hauptrelease. (Die große Neuerung, der Compiler, wurde als Open Source veröffentlicht.) Aber nur weil es Ihre C#-Programmierung nicht revolutioniert, heißt das nicht, dass keine wirklichen Fortschritte bei der Beseitigung einiger Ärgernisse und Ineffizienzen der Programmierung gemacht worden sind. Wenn Sie die Neuerungen erst einmal in Ihre tägliche Arbeit integriert haben, werden Sie sie schnell als selbstverständlich ansehen. Zu meinen persönlichen Favoriten zählen die Features $-Operator (indizierte Zeichenfolgenmember), primäre Konstruktoren (ohne Feldparameter), „using static“ und Deklarationsausdrücke. Ich glaube, dass ich alle diese Features schnell routinemäßig in meiner Programmierung verwenden werde und dass einige davon höchstwahrscheinlich sogar den Programmierstandards hinzugefügt werden.

Mark Michaelis ist der Gründer von IntelliTect und arbeitet als leitender technischer Architekt und Trainer. Er ist seit 1996 Microsoft MVP für C#, Visual Studio Team System (VSTS) und das Windows SDK, und 2007 wurde er als Microsoft Regional Director anerkannt. 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). Michaelis hat an der University of Illinois den Abschluss Bachelor of Arts in Philosophie und am Illinois Institute of Technology den Abschluss Master of Computer Science erworben. Wenn er nicht am Computer arbeitet, kümmert er sich um seine Familie oder trainiert für einen weiteren Ironman.

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