Bild mit Buchrücken - Aufdruck: "From the real experts"

Neue C♯ Features die man tatsächlich benutzt

Blogpost für die anic Homepage | Daniel Fahsig | Microsoft 365 Certified: Teams Application Developer Associate/MCP

Die meisten neuen Sprachfeatures sind nicht für den Alltag gedacht, sondern decken nur sehr spezielle Problemstellungen ab. Einige der neuen Features in den Versionen 8 bis 10 sind aber tatsächlich im Alltag hilfreich. In diesem Post möchten wir ein paar teilen, die wir hilfreich finden.

Der switch Ausdruck (8.0)

Wenn man eine switch-Anweisung nutzt um einer einzelnen Variablen abhängig von den Bedingungen einen neuen Wert zuzuweisen, oder wenn man sie nur nutzt um einen Wert zurückzugeben, kann man jetzt den neuen switch Ausdruck nutzen.

Also statt diesem Code hier:

public static RGBColor FromRainbowClassic(Rainbow colorBand)
{
    switch (colorBand)
    {
        case Rainbow.Red:
            return new RGBColor(0xFF, 0x00, 0x00);
        case Rainbow.Orange:
            return new RGBColor(0xFF, 0x7F, 0x00);
        case Rainbow.Yellow:
            return new RGBColor(0xFF, 0xFF, 0x00);
        case Rainbow.Green:
            return new RGBColor(0x00, 0xFF, 0x00);
        case Rainbow.Blue:
            return new RGBColor(0x00, 0x00, 0xFF);
        case Rainbow.Indigo:
            return new RGBColor(0x4B, 0x00, 0x82);
        case Rainbow.Violet:
            return new RGBColor(0x94, 0x00, 0xD3);
        default:
            throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand));
    };
}

Könnte man das jetzt so schreiben:

public static RGBColor FromRainbow(Rainbow colorBand)
    => colorBand switch
    {
        Rainbow.Red     => new RGBColor(0xFF, 0x00, 0x00),
        Rainbow.Orange  => new RGBColor(0xFF, 0x7F, 0x00),
        Rainbow.Yellow  => new RGBColor(0xFF, 0xFF, 0x00),
        Rainbow.Green   => new RGBColor(0x00, 0xFF, 0x00),
        Rainbow.Blue    => new RGBColor(0x00, 0x00, 0xFF),
        Rainbow.Indigo  => new RGBColor(0x4B, 0x00, 0x82),
        Rainbow.Violet  => new RGBColor(0x94, 0x00, 0xD3),
        _               => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
    };

Wenn man die beiden Implementierungen vergleicht, fallen folgende Änderungen auf:

  • Die Variable, die zur Entscheidung genutzt wird, wird vor das switch gesetzt
  • case wird ersetzt durch =>
  • default wird ersetzt durch _ (discard-Muster)
  • es können lediglich Ausdrücke in den einzelnen Zweigen benutzt werden

Enhancements to pattern matching

Mit C♯ 7 wurde der Musterabgleich eingeführt, womit dann das folgende möglich wurde:

if (arg1 is ComplexObject complex)
{
    internal = complex.SophisticatedAction();
}

Statt diesem Code:

var complex = arg1 as ComplexObject;
if (complex != null) {
    internal = complex.SophisticatedAction();
}

Eigenschaftsmuster (8.0)

Eigenschaftsmuster ermöglichen den Zugriff auf Eigenschaften des Objekts auf dem die Entscheidung basieren soll, z.B. bei Verwendung eines switch Ausdrucks.

Statt also das hier zu schreiben:

var state =  location.State;
state switch
{
    "WA" => salePrice * 0.08M;
    "MN" => salePrice * 0.06M;
    "MI" => salePrice * 0.09M;
    _ => 0M;
}

Ist es möglich das hier zu schreiben:

location switch
{
    { State: "WA" } => salePrice * 0.08M;
    { State: "MN" } => salePrice * 0.06M;
    { State: "WI" } => salePrice * 0.09M;
}

Sogar etwas komplexeres wie das hier ist damit möglich:

public static bool IsReleaseDate(DateTime date)  => date is { Year: 2022, Month: 10, Day: 1 };

Ausbau mit C♯ 10

Mit C♯ 10 wurden die Eigenschaftsmuster noch einmal vereinfacht. Wenn man bis dahin auf innere Eigenschaften zugreifen wollte, musste man das so tun:

if (e is MethodCallExpression { Method: { Name: "MethodName" } })

Jetzt geht das ein wenig kürzer und eingängiger:

if (e is MethodCallExpression { Method.Name: "MethodName" })

Tupelmuster (8.0)

Manchmal muss man zwei Variablen für eine Entscheidung prüfen. Dazu könnte man eine verschachtelte if-Anweisung nutzen, um alle Fälle abzudecken. Aber mit einem Tupelmuster ist das nicht mehr nötig:

public static bool CanEditDocument(DocumentState state, UserRole role)
    => (state, role) switch
    {
        (DocumentState.Approval, UserRole.Approver) => true,
        (DocumentState.Open, UserRole.Editor) => true,
        (DocumentState.Open, UserRole.Manager) => true,
        (DocumentState.Closed, UserRole.Reviewer) => true,
        (_,_) => false,
    }

Die Rollen, die ein Dokument bearbeiten dürfen sind hier klar ersichtlich und mit das discard-Muster deckt hier alle Fälle ab, in denen das Bearbeiten nicht erlaubt ist.

Typmuster (9.0)

Typprüfungen können jetzt auch in einem switch-Ausdruck gemacht werden, wenn man Typmuster nutzt:

public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}

public static class TollCalculator
{
    public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
    {
        Car _ => 2.00m,
        Truck _ => 7.50m,
        null => throw new ArgumentNullException(nameof(vehicle)),
        _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
    };
}

Logische und relationale Muster (9.0)

Statt langer if-Anweisungen für jede Bedingung, kann man auch komplexe Bedingungen in einem switch-Ausdruck mithilfe logischer und relationaler Muster abprüfen:

Console.WriteLine(GetCalendarSeason(new DateTime(2021, 3, 14)));  // output: spring
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 7, 19)));  // output: summer
Console.WriteLine(GetCalendarSeason(new DateTime(2021, 2, 17)));  // output: winter

static string GetCalendarSeason(DateTime date) => date.Month switch
{
    >= 3 and < 6 => "spring",
    >= 6 and < 9 => "summer",
    >= 9 and < 12 => "autumn",
    12 or (>= 1 and < 3) => "winter",
    _ => throw new ArgumentOutOfRangeException(nameof(date), $"Date with unexpected month: {date.Month}."),
};

Hier wird mit relationalen Mustern geprüft, ob der Monat in dem festgelegten Bereich ist und mit den logischen and und or-Mustern werden die Bedingungen verknüpft.

Und mit dem logischen not-Muster ist es möglich eine besser lesbare null-Prüfung zu schreiben:

if (input is not null)
{
    // ...
}

using Deklarationen (8.0)

Es kommt oft vor, dass man eine Methode oder einen Zweig einer if-Anweisung mit einer using-Anweisung beginnt, wodurch ein weiteres Klammerpaar und eine weitere Einrückungsebene dazukommt:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
    {
        int skippedLines = 0;
        foreach (string line in lines)
        {
            if (!line.Contains("Second"))
            {
                file.WriteLine(line);
            }
            else
            {
                skippedLines++;
            }
        }
        return skippedLines;
    } // file is disposed here
}

Wenn man eine using Deklaration benutzt, sieht das so aus:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    int skippedLines = 0;
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
        else
        {
            skippedLines++;
        }
    }
    return skippedLines;
    // file is disposed here
}

Dadurch verringert man die Anzahl der Klammern, die Einrückungen des Codes und die Anzahl der Zeilen in einer Methode und einer Datei.

Indizes und Bereiche (8.0)

Um bestimmte Elemente in einer Sequenz zu erreichen gibt es eine verbesserte Syntax, die es ermöglicht statt

var wordBeforeLast = words[words.Length - 2];

das hier zu schreiben:

var wordBeforeLast = words[^2];

Der Operator Index vom Ende verhält sich genau wie zuvor, d.h. ^0 würde eine Exception auslösen (genau wie word[words.Length]).

Und wenn man einen Unterbereich aus einer Sequenz schneiden will kann man jetzt den Bereichsoperator nutzen:

var relevantPart = phoneNumber[4..9];

Das wird die folgenden Elemente einschließen: phoneNumber[4] bis phonenumber[8] – d.h. der Anfang ist eingschlossen, das Ende ist ausgeschlossen. Beide Zahlen sind optional und werden, falls sie fehlen, als Anfang oder Ende der Sequenz angenommen. Der Index vom Ende-Operator kann hier auch benutzt werden.

Null-Coalescing Zuweisung (8.0)

Bei Methoden, die einen Parameter haben, der als Standardwert null hat, wurde oft der folgende Weg gewählt, um einen gültigen Wert dafür sicherzustellen:

void Method(ComplexObject complex = null)
{
    if (complex is null)
    {
        complex = new ComplexObject(...);
    }

    ...
}

Mit der null-Coalescing Zuweisung geht das viel kürzer:

void Method(ComplexObject complex = null)
{
    complex ??= new ComplexObject(...);

    ...
}

init-only-Setter (9.0)

Wenn man das Setzen einer Eigenschaft eines Obekts mit dem Eigenschafteninitialisierer möglich machen wollte, dann musst man den Setter jeder betroffenen Eigenschaft auf public setzen. Das ermöglichte natürlich auch die nachträgliche Veränderung dieser Eigenschaft, was manchmal unerwünscht sein konnte.

Mit den neuen init-only Settern kann das verhindert werden, weil damit eine Eigenschaft nur bei der Initialisierung durch den Eigenschafteninitialisierer gesetzt und danach nicht mehr direkt verändert werden kann.

public class ComplexObject
{
    public int ChildrenCount { get; init; }

    public ComplexObject()
    {
        ...
    }
}

var complex = new ComplexObject { ChildrenCount = 20 };
complex.ChildrenCount = 25; // Error! CS8852 - this is not allowed

Top-Level-Anweisungen (9.0)

Üblicherweise hat ein Programm eine Hauptdatei, z.B. die Program.cs bei ASP.Net Core Applikationen und diese Datei enthielt dann viel Boilerplate-Code. Jetzt darf eine Datei im Projekt allen Boilerplate-Code weglassen. Sie muss dann lediglich den auszuführenden Code und die nötige using-Ausdrücke enthalten.

Also wird aus dem hier:

namespace Company.Application
{
    using System;
    ...

    public static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
        ...
    }
}

das hier:

using System;
...

Console.WriteLine("Hello World!");
...

Weglassen des Typs beim new-Ausdruck (9.0)

Wenn der Typ des zu erzeugenden Objekts bereits bekannt ist, kann der Typ beim new weggelassen werden:

private List<Chemical> chemicals = new();

Das geht auch beim Aufruf einer Methode:

public TimeSpan GetReactionTime(Chemical secondChemical, ReactionOptions options) {...}

var duration = chemical.GetReactionTime(catalysator, new());

Und in Kombination mit dem Eigenschafteninitialisierer:

ReactionOptions options = new() { Technician = "Heinz Müller" };

Dateibezogenen Namespacedeklaration (10)

Um die Einrückungen in den Codedateien weiter zu minimieren, kann man jetzt dateibezogene Namespacedeklarationen nutzen. So wird aus:

namespace Company.Application
{
    using System;

    using Company.Business;

    public class SpecialHandler
    {
        ...
    }
}

Das hier:

namespace Company.Application;
using System;

using Company.Business;

public class SpecialHandler
{
    ...
}

Damit spart man Platz in zwei Richtungen: Einrückung und Zeilenzahl

Zuweisung und Deklaration in derselben Dekonstruktion (10)

Mit C♯ 7 kann man ein benutzerdefiniertes Objekt mit Hilfe der Deconstruct Method dekonstruieren, allerdings nur alle Werte neuen Variablen oder alle Werte zu bereits existierenden Variablen zuweisen. Diese Einschränkung wurde aufgehoben:

public class Point
{
    ...
    public void Deconstruct(out int x, out int y) {...}
}

var point = new Point(...);

int x = 0;
(x, int y) = point;

Let’s Work together!

Lassen Sie uns Ihnen helfen die Zukunft Ihres Unternehmens zu entwerfen.

anic GmbH Logo

Bitte rufen Sie mich zurück.