Sprachpflege mit Roslyn: Neues in C#
Kommentare

Eine der aufsehenerregenden Ankündigungen von Microsoft auf der Build 2014 Konferenz war der Schritt, die Ergebnisse des Projekts Roslyn Open Source zu machen. Hinter dem Codenamen versteckt sich nichts Geringeres als die komplette Neugestaltung und -Entwicklung der C# und VB Compiler. Open Source bedeutet in diesem Zusammenhang neben der Veröffentlichung des Quellcodes auch das Publikmachen der Road Map. Neugierig, wie sich Ihr C#-Code in Zukunft verändern wird? In diesem Artikel fassen wir die kommenden Änderungen kompakt für Sie zusammen.

Dank der Road Map sehen wir Entwickler, worauf wir uns in Sachen C# Sprachneuerungen in nächster Zeit freuen dürfen und was wir mit Hilfe der Roslyn CTP sogar schon ausprobieren können.

Initialisierung von Auto-Properties

Ein kleiner Appetithappen zum Start gefällig? C# bekommt eine Möglichkeit, Auto-implemented Properties zu initialisieren. Sie können also in Zukunft public string FirstName { get; set; } = „Max Muster“ schreiben.

Kaum erwähnenswert meinen Sie? Wäre es auch, gäbe es nicht eine Zugabe. Werfen Sie zum Beispiel einen Blick auf Listing 1. Wollte man eine Klasse früher komplett immutable, also unveränderbar, machen, konnte man dafür das readonly Schlüsselwort verwenden, verlor dadurch aber die Möglichkeit Auto Properties zu verwenden (siehe Klasse Product_OldStyleWithField im Listing). Alternativ konnte man den Setter private machen, war dann aber gezwungen, zum Initialisieren einen Konstruktor zu schreiben (siehe Klasse Product_AutoProperty im Listing). Wie Sie im Listing an der Klasse Product_AutoPropertyInitializer sehen, gibt es jetzt Immutables, Auto-Properties und Initialisierung auf einmal. Elegant, oder?

public class Product_OldStyleWithField
{
    // Note that we have to care for the private field
    //      ourselves.
    private readonly Guid id = Guid.NewGuid();
    public Guid Id { get { return this.id; } }
}

public class Product_AutoProperty
{
    public Product_AutoProperty()
    {
        this.Id = Guid.NewGuid();
    }

    // Note that we cannot define "Id" as "readonly" as
    // read-only properties are not supported in C#.
    public Guid Id { get; private set; }
}

public class Product_AutoPropertyInitializer
{
    // Note that "Id" is a getter-only property -> immutable 🙂
    public Guid Id { get; } = Guid.NewGuid();
}

Primary Constructors

Dass die verschiedenen Sprachen aus dem Hause Microsoft sich gegenseitig beeinflussen, hat man schon am Beispiel F# und C# gesehen. Das folgende Feature fand man in ähnlicher Form vor seinem Auftauchen in C# bereits in Microsofts neuer Sprache TypeScript. Es geht darum, Konstruktorparameter mit einem Access Modifier (z. B. private, public) zu versehen und diese dadurch ohne weiteren Aufwand zu Feldern zu machen. In C# wird diese neue Funktion als Primary Constructors bezeichnet, da man implizit einen Konstruktor für die jeweilige Klasse anlegt.

Schluss mit der Theorie, in Listing 2 finden Sie den Code. Achten Sie als erstes auf die Deklaration der Klasse Product. Gleich hinter dem Klassennamen folgen die Parameter des Primary Constructors. Durch Angabe des Access Modifiers macht man den Parameter automatisch zu einem entsprechend sichtbaren Feld (z. B. wird aus dem Parameter Id in unserem Beispiel das public-Feld Id). Wie man am Parameter Available sieht, werden optionale Parameter mit Standardwerten unterstützt.

Die Primary Constructor Parameter stehen auch zum Initialisieren von Eigenschaften und Feldern zur Verfügung wie man am Beispiel von Product.Name sieht.

Natürlich können Sie weiterhin zusätzliche Konstruktoren zu Ihren Klassen hinzufügen. Diese müssen jedoch mit dem this-Operator den Primary Constructor inklusive Angabe aller seiner Parameter aufrufen. Gleiches gilt für abgeleitete Klassen (z. B. OldProduct in unserem Listing). Der Basisklasse müssen alle notwendigen Parameter übergeben werden.

// Class with "primary constructor"
public class Product(
    public int Id,
    string ProductName,
    private bool Available = true)
{
    // "Id" automatically becomes a public field because 
    //      of the primary constructor.

    // Primary constructor parameter used for initialization.
    public string Name { get; } = ProductName;

    // "Available" automatically becomes a private field
    //             because of the primary constructor.
    public void Buy()
    {
        if (!this.Available)
        {
            throw new InvalidOperationException("Not available");
        }

        // Note that the following line would not work because
        //      primary constructor parameters have to be captured
        //      (with e.g. "public" or "private") or used to 
        //      initialize members.
        // Debug.WriteLine("Buying product {0}", this.ProductName);
    }

    // The following constructor would not work because
    //     it has to provide parameters for primary constructor.
    // public Product() { }

    // This constructor works because it contains a call to "this"
    public Product() : this(0, null) { }
}

// Note how inheritance works with primary constructors
public class OldProduct(int Id, string Name) : Product(Id, Name, false)
{
}

Using Static – Grund zum Streiten?

Bei fast allen Vorträgen, die ich zum Thema C# mache, gibt es ein Feature, das so gut wie immer zu Diskussionen führt: Verwendet man var oder nicht? Ähnliche Streitgespräche befürchte ich bei der folgenden C# Erweiterung: Statt bei using nur Namespaces angeben zu können, kann man dort jetzt auch Klassen hinschreiben. Dadurch werden alle statischen Member ohne Klassenprefix aufrufbar.

Listing 3 zeigt an einem Beispiel, wie using static verwendet wird. Achten Sie auf den Aufruf der Methode ConvertToEuro am Ende des Listings. Im Gegensatz zu früher fehlt der Name der statischen Klasse CurrencyConverter. Weniger lesbar, schwerer zu warten? Man wird es wohl von Fall zu Fall entscheiden müssen. Eine generelle Empfehlung, immer using static zu verwenden, ist aber meiner Meinung nach auf keinen Fall auszusprechen. Ich persönlich plane sogar, diese neue Funktion spärlich einzusetzen – übrigens im Gegensatz zum, am Beginn dieses Abschnitts angesprochenen var, von dem ich ein großer Fan bin.

namespace MyOtherNamespace
{
    // Note that "CurrencyConverter" is a static class.
    public static class CurrencyConverter
    {
        public static decimal ConvertToEuro(string sourceCurrency, 
            decimal price)
        {
            if (sourceCurrency == "USD")
            {
                return price * 0.73m;
            }

            throw new ArgumentException(
                "Unknown source currency", 
                "sourceCurrency");
        }
    }
}

namespace RoslynUpcomingCSharpNews
{
    // Note that the "using" refers to a class instead a namespace.
    using MyOtherNamespace.CurrencyConverter;

    [TestClass]
    public class UsingStatic
    {
        [TestMethod]
        public void TestClassWithPrimaryConstructor()
        {
            const decimal priceInUsd = 100m;

            // Note how we call CurrencyConverter.ConvertToEuro
            // without class name.
            var priceInEuro = ConvertToEuro("USD", 100);

            Assert.IsTrue(priceInEuro < priceInUsd);
        }
    }
}

Declaration Expressions

Declaration Expressions sind eine Lösung für ein kleines aber doch lästiges Problem. Die meisten Entwickler wissen, dass man in C# Ausdrücken auch Werte zuweisen kann: if (areEqual = x != y) { … }. Unangenehm ist oft, dass im vorigen Beispiel die Variable areEqual explizit zuvor mit bool areEqual; deklariert werden muss. Diese Einschränkung gehört jetzt der Vergangenheit an. In Zukunft kann man in C# in Ausdrücken Variablen deklarieren: if (var areEqual = x != y) { … }

In Listing 4 sehen Sie, wie sehr diese Neuerung Code verkürzen kann. Aus neun Zeilen werden zwei, indem die Deklaration des out Parameters im Aufruf der TryParse Methode eingebettet wird.

var products = new[]
{
    new { Id = 1, Name = "Bike", PriceAsString = "1000" },
    new { Id = 2, Name = "Car", PriceAsString = "20000" },
    new { Id = 3, Name = "Plane", PriceAsString = "A lot of money!" }
};

// Our goal: Calculate total price and ignore prices that 
//           contain invalid characters.

// Calculate total price (old style)
var totalPrice = products.Sum(p =>
    {
        int price;
        if (Int32.TryParse(p.PriceAsString, out price))
        {
            return price;
        }
        return 0;
    });

// Calculate total price (new style)
totalPrice = products
    .Sum(p => Int32.TryParse(p.PriceAsString, out var price) ? price : 0);

Aufmacherbild: Jumbled letters made of wood close up von Shutterstock / Urheberrecht: Simon Bratt

[ header = Seite 2: Exception Handling ]

Exception Handling

Exception Filter helfen Ihnen in Zukunft, unnötige re-throws zu vermeiden. Ihr Code kann dadurch kürzer und prägnanter werden. Die Funktion wird aber unterschätzt, wenn man sie als reine „Schönheitsmaßnahme“ ansieht. Sie kann nämlich auch das Logging und damit die Fehlersuche erleichtern.

In Listing 5 sehen Sie ein Beispiel für genau einen solchen Fall. Vergleichen Sie die catch Statements der Methoden Update_OldRethrowing und Update_RethrowingWithExceptionFilter. Die if Bedingung wandert ins catch Statement. Die Folge daraus sieht man beim Vergleich von Abbildung 1 und 2. Während in Abbildung 1 der tatsächliche Ort der Exception im Stack Trace durch das re-throw nicht mehr sichtbar ist und die Fehlersuche dadurch deutlich erschwert wird, ist in Abbildung 2 durch die neuen Exception Filter alles im Stack Trace nachvollziehbar.

// BTW - Note use of primary constructor here
public class DataAccessException(string message, public bool IsCritical) 
    : Exception(message)
{ }

public static class DataAccess
{
    private static void UpdateInternal(bool fastMode)
    {
        if (fastMode)
        {
            // Let's assume something bad happened in "fast mode" ...
            throw new DataAccessException("Something bad happened!", true);
        }
        else
        {
            // Let's assume everything is OK in "normal" mode ...
        }
    }

    public static void Update_OldRethrowing()
    {
        try
        {
            UpdateInternal(fastMode: false);
            UpdateInternal(fastMode: true);
        }
        catch (DataAccessException dae) 
        {
            if (!dae.IsCritical)
            {
                // Log and handle (e.g. retry) non-critical errors
            }
            else
            {
                // We re-throw critical errors
                throw;
            }
        }
    }

    public static void Update_RethrowingWithExceptionFilter()
    {
        try
        {
            UpdateInternal(fastMode: false);
            UpdateInternal(fastMode: true);
        }
        catch (DataAccessException dae) if (!dae.IsCritical)
        {
            // Log and handle (e.g. retry) non-critical errors
        }
    }
}

Abb.1: Tatsächlicher Ort der Exception wird durch re-throw versteckt

Abb. 2: Durch Exception Filter bleibt tatsächlicher Ort der Exception erhalten

Die Behandlung von Exceptions wird durch eine weitere, kommende Neuerung erleichtert. Während Sie bisher das await Schlüsselwort nur innerhalb des try Blocks verwenden konnten, erlaubt C# in Zukunft await auch in catch und finally.

Arbeiten mit indizierten Datentypen

Json.NET ist für alle, die direkt oder indirekt mit Web zu tun haben, zu einem unverzichtbaren Helfer geworden. Den Sinn eines der neuen C# Features kann man anhand dieser Bibliothek gut erklären: Indexed Members. Listing 6 stellt die alte und neue Schreibweise gegenüber. Sie sehen, dass sie jetzt mit Hilfe des $ Zeichens auf den Indexer lesend und schreibend zugreifen können.

Darüber hinaus wird das Initialisieren von indizierten Datentypen wie Dictionary vereinfacht. Werfen Sie einen Blick auf Listing 7. Während bisher ein Dictionary nur mit wiederholten Aufrufen von Add gefüllt werden konnte, gibt es jetzt eine Syntax zum Initialisieren, die ganz ähnlich der von Aufzählungen wie Listen oder Arrays funktioniert.

var jsonContent = "{ 'FirstName': 'Max', 'LastName': 'Muster' }";
var objectContent = JsonConvert.DeserializeObject(jsonContent) as JObject;

// Use "old" indexed member syntax.
Assert.AreEqual("Max", objectContent["FirstName"]);

// Use "new" indexed member syntax.
Assert.AreEqual("Max", objectContent.$FirstName);
objectContent.$Age = 20;
Assert.AreEqual(20, objectContent.$Age);

 

private class Person(public string FirstName, public string LastName)
{ }
[…]

// C# supports collection initialization for quite a while now.
var people = new List()
{
    new Person("Max", "Muster"),
    new Person("Tim", "Smith")
};

// It has not been possible for dictionaries.
var peopleDictionary = new Dictionary();
peopleDictionary.Add("Max", new Person("Max", "Muster"));
peopleDictionary.Add("Tim", new Person("Tim", "Smith"));

// Now we can use element initializers to initialize dictionaries, too.
peopleDictionary = new Dictionary
{
    ["Max"] = new Person("Max", "Muster"),
    ["Tim"] = new Person("Tim", "Smith")
};
}

Lust auf mehr?

Haben Sie Lust auf mehr bekommen? Die aktuelle Preview-Version von Roslyn finden sie hier. Für die, die es ganz genau wissen wollen, gibt es dort auch den Quellcode von Roslyn. Zum Verwenden der neuen C# Features braucht man diesen jedoch natürlich nicht. Den Quellcode aus diesem Artikel finden Sie hier.

Microsoft hat nicht nur die in Roslyn bereits implementierten C# Neuerungen bekannt gegeben sondern auch einen Ausblick auf noch folgende Punkte geliefert. Hier finden Sie eine Übersicht. Wenn man den Ankündigungen glauben darf, ist Microsoft offen für Feedback und sogar Code Contributions.

Bei allen Experimenten rate ich noch zur Vorsicht. Roslyn ist eine Preview-Version und sollte nicht zum Compilieren von Produktionsversionen verwendet werden. Insofern werden die meisten von uns wohl noch etwas Geduld haben müssen, bis sie in den Genuss der beschriebenen Spracherweiterungen kommen. Hoffentlich dauert das Warten nicht mehr allzu lange.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -