Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 8 Min.

C# 10: Neue Features

Die interessantesten Neuerungen von C# 10 und wie man sie einsetzt.
© dotnetpro
Laut Tiobe-Index [1] belegt C# den fünften Platz im Ranking der Programmiersprachen (Stand Dezember 2021) und zählt somit zu den besonders gefragten Programmiersprachen. Ein guter Grund, sich mit den neuen Sprachfeatures auseinanderzusetzen, die C# in Version 10 mitbringt. Um die neuen Features zu untersuchen, wurde die Preview-Version 4.1 von Visual Studio 2022 verwendet.

Globale Using-Anweisungen

Using-Anweisungen wie zum Beispiel using System, using System.Collection.Generic oder System.Linq sind sehr populär und werden häufig in zahlreiche Dateien eines Projekts eingebunden [2]. Um diese Anweisungen nicht in jede Projektdatei immer wieder aufs Neue einfügen zu müssen, hat Microsoft global gültige Using-Statements eingeführt. Diese werden einmal als global deklariert und stehen anschließend in allen Dateien zu Verfügung.Bild 1 zeigt ein kleines Beispielprogramm, welches neben der Datei Program.cs auch noch eine Datei mit Erweiterungsmethoden für den Typ IEnumerable<int> definiert. Diese erfordern die farbig markierten Using-Anweisungen.
Wiederkehrende Using-Anweisungen(Bild 1) © Autor
Die Anweisung using System.Collections.Generic; wird in beiden Dateien benötigt und ist ein Kandidat für einen ­Name­space, der überall verfügbar sein sollte. Bild 2 zeigt die Datei Globals.cs, welche die Using-Befehle enthält, die überall im Projekt verfügbar sind. Durch die Definition globaler Using-Befehle wird im Beispiel aus Bild 2 das Einbinden von System für die Ausgabe mittels Console.WriteLine(content); überflüssig. Dasselbe gilt für die Anweisung using System.Collections.Generic;. Ist sie dennoch in einer Code-Datei des Projekts enthalten, macht Visual Studio darauf aufmerksam, wie es in Bild 3 zu sehen ist.
Globale Using-Befehle(Bild 2) © Autor
Visual Studio 2022 markiertüberflüssige Using-Befehle(Bild 3) © Autor
Die globalen Using-Anweisungen können zudem noch ­eine Stufe tiefer verwendet werden, um im Code Schreib­arbeit einzusparen, beispielsweise so:

global using static System.Console; 
 
Der Effekt: Anstelle von Console.WriteLine(numbers.Event()); kann im Code schlicht WriteLine(numbers.Event()); geschrieben werden.Fehlt ein Using-Befehl, von dem man dachte, dass er bereits vorhanden ist, liefert Visual Studio einen Fehlerhinweis – siehe Bild 4. Das verführt dazu, die globalen Using-Befehle über mehrere Code-Dateien zu streuen. Besser ist es, von vornherein eine Datei Globals.cs anzulegen und alle globalen Usings darin zu platzieren.
Der Tooltipin Visual Studio zeigt Zusatzinformationen(Bild 4) © Autor

Implizite Using-Anweisungen

Bei neu angelegten Projekten sind bereits einige Namespaces implizit als global markiert. Bild 5 zeigt ein Beispiel, in dem Typen aus den Namensräumen System, System.Net.­Http und System.Collections.Generic verwendet werden, ohne dass explizit globale Usings definiert werden mussten. Um dies zu ermöglichen, muss in der Datei csproj die folgende Einstellung zu finden sein:
Implizite Usings(Bild 5) © Autor

<ImplicitUsings>enableImplicitUsings> 
Durch das Entladen des Projekts kann diese Datei betrachtet und gegebenenfalls editiert werden, vergleiche Bild 6.
Implizite Usingsentladen(Bild 6) © Autor
Dieses Feature ist nur bei neuen Projekten aktiviert. Dass es bei bereits bestehenden Projekten deaktiviert ist, hängt damit zusammen, dass das automatische Aktivieren zu einer veränderten Art und Weise führen kann, wie der Compiler den Code interpretiert, wodurch Probleme bei der Aktualisierung von .NET 5 auf .NET 6 auftreten könnten.An dieser Stelle könnte man sich die Fragen stellen, wie dies eigentlich funktioniert und welche Namespaces automatisch für die gesamte Anwendung eingebunden werden.Die Antwort auf diese Frage ist im obj-Verzeichnis in der konfigurierten Build-Konfiguration (zum Beispiel Debug) im Unterordner net6.0 zu finden. In diesem Verzeichnis befindet sich eine Datei namens [Projektname].GlobalUsings.g.cs, welche die entsprechenden Anweisungen enthält, vergleiche Bild 7.
Automatischeimplizite Usings(Bild 7) © Autor

FileScoped Namespaces

Die Empfehlung bei Namespaces ist, dass je Datei nur ein Name­space vorhanden sein sollte. Bisher war es notwendig, nach dem Namen des Namespace eine öffnende und eine schließende Klammer zu verwenden. Hier ein Beispiel:

// Namespace mit Klammern 
namespace dotnetpro 
{ 
  public class Program 
  { 
    public static void Main() 
    { 
      Console.WriteLine(new Customer("dotnetpro", 
        "Christian", "Havel")); 
    } 
  } 

  public record Customer(
    string companyName, 
    string firstName, 
    string lastName); 
} 
 
Das dem Namespace zugehörige Klammernpaar { ... } führt zur ersten Einrückung des Codes. Wie zuvor erwähnt gilt die Empfehlung, dass eine Datei nur einen Namespace enthalten sollte. Microsoft hat deshalb die FileScoped Namespaces eingeführt. Dabei wird der Namespace mit einem Semikolon abgeschlossen, die beiden Klammern sind nicht mehr notwendig, wodurch auch die erste Einrückung entfällt:

// Namespace ohne Klammern 
namespace dotnetpro; 
public class Program 
{ 
  public static void Main() 
  { 
    Console.WriteLine(new Customer(
      "dotnetpro", "Christian", "Havel")); 
  } 
} 
public record Customer(string companyName, 
  string firstName, string lastName); 
 

Extended Property Patterns

Mit den vorangegangenen C#-Sprachversionen wurden die sogenannten Property Patterns eingeführt, mit denen sich der Befehl case in einer Switch-Anweisung auf eingebettete Members beziehen konnte. Listing 1 zeigt ein Beispiel, in dem zwei record-Typen definiert sind. Das SpaceShip entstammt einem Planetensystem, in dem eine bestimmte Sprache gesprochen wird.
Listing 1: Property Patterns (Klammern)
public static void Main() 
{ 
  var spaceShips = new[] 
  { 
    new SpaceShip("X-Wing", new PlanetSystem( 
      "Epsilon Eridani")), 
    new SpaceShip("B-Wing", new PlanetSystem( 
      "Alpha Centauri")), 
    new SpaceShip("Falcon", null) 
  }; 

  foreach(var spaceShip in spaceShips) 
    Console.WriteLine(GetLanguage(spaceShip)); 
  } 

  public static string GetLanguage( 
    SpaceShip spaceShip) 
  { 
    return spaceShip switch 
    { 
      SpaceShip { PlanetSystem: { 
        star: "Epsilon Eridani" } } => "German", 
      SpaceShip { PlanetSystem: { 
        star: "Alpha Centauri" } } => "Klingon", 
      _ => "unknown" 
    }; 
  } 

  public record SpaceShip(string name, 
    PlanetSystem PlanetSystem); 
  public record PlanetSystem(string star);  
In der Switch-Anweisung wird der eingebettete Member SpaceShip -> PlanetSystem -> Star für die Ermittlung der Sprache ausgewertet. Betrachtet man andere Programmiersprachen, so ist die Notation mit den geschweiften Klammern eher ungewöhnlich. Üblicher ist der Zugriff auf die Member mit Punkten. Microsoft hat dies in Version 10 nun optimiert.Listing 2 zeigt die Funktion mit der nun verfügbaren Punktnotation, die den Zugriff auf die eingebetteten Member einfacher darstellt.
Listing 2: Property Patterns (Punktnotation)
public static string GetLanguage(
  SpaceShip spaceShip) 
{ 
  return spaceShip switch 
  { 
    {PlanetSystem.star: "Epsilon Eridani" } => 
      "German", 
    {PlanetSystem.star: "Alpha Centauri" } => 
      "Klingon", 
     _ => "unknown" 
   }; 
  }  
Zu erwähnen ist, dass die definierten Switch-Cases nicht zutreffen und der default-Wert verwendet wird (hier un­known), wenn der Member, der im Switch-Ausdruck geprüft wird (PlanetSystem), nicht definiert, das heißt null ist (new SpaceShip(“Falcon“, null)), siehe Bild 8.
Ist das Planetensystem „null“gibt es keine Fehlermeldung, sondern es wird der default-Wert zurückgeliefert(Bild 8) © Autor

Constant Interpolated Strings

Hin und wieder kann es vorkommen, dass interpolierte Strings ausschließlich aus konstanten Strings generiert werden. Wie Bild 9 zeigt, war es vor C# 10 nicht möglich, einen solchen interpolierten String einer Konstanten zuzuweisen. Wird die Sprachversion in Visual Studio auf C# 10 geändert, akzeptiert der Compiler das neue Feature, wie Sie Bild 10 entnehmen können.
Konstante String-Interpolationmit C# 9(Bild 9) © Autor
C# 10 erlaubtConstant Interpolated Strings(Bild 10) © Autor

Struktur DateOnly

Wie der Name schon suggeriert, repräsentiert diese Datenstruktur ein Datum ohne eine zeitliche Angabe [3, 4]. Die Erzeugung kann auf verschiedene Arten erfolgen, beispielsweise über den Aufruf des Konstruktors:

var dateOnly = new DateOnly(1976, 8, 8); 
 
Oder über eine der verfügbaren Factory-Funktionen, die das Datum aus einem bestehenden DateTime-Objekt übernimmt:

var date = 
  DateOnly.FromDateTime(
  DateTime.Now); 
 
Wie bei DateTime kann aber auch ein String geparst werden. Im nachfolgenden Code-Schnipsel sehen Sie die Va­riante mit TryParse. Auch die Alternative mit der Funktion Parse steht zur Verfügung, wirft aber gegebenenfalls ­eine Ausnahme.

if (DateOnly.TryParse(
  "28/09/2021", 
  new CultureInfo("en-GB"), 
  DateTimeStyles.None, 
  out var date)) 
{ 
  Console.WriteLine(date); 
} 
 
Da die Menschen verschiedener Länder Datumsangaben unterschiedlich inter­pretieren, wird das Parsen von Datumsangaben bei C# durch die Culture beeinflusst.Die im obigen Code-Schnipsel genutzte Anweisung CultureInfo(“en-GB“) soll dafür sorgen, dass das Datum aus einer Zeichenfolge im Format Tag/Monat/Jahr geparst wird.Durch die Funktionsaufrufe AddDays, AddMonths und AddYears wird eine neue Instanz mit entsprechend angepasstem Datum erstellt:

// Ein neues DateOnly erstellen 
var dateOnly = new DateOnly(1976, 8, 8); 
dateOnly.AddDays(1).AddMonths(1).AddYears(1); 

Struktur TimeOnly

Ebenfalls neu ist die Struktur TimeOnly. Sie repräsentiert eine Uhrzeit ohne das zugehörige Datum [3, 4]. Sie ist genauso einfach zu handhaben wie DateOnly, bietet allerdings zwei Besonderheiten. Den Einstieg in diese Struktur zeigt folgende Anweisung, die zur Ausgabe von 00:00 führt:
Console.WriteLine(TimeOnly.MinValue); 
 
Wird dieselbe Anweisung anstelle von MinValue mit dem Zusatz MaxValue genutzt, liefert sie als Ergebnis den String-Wert 23:59 zurück:

Console.WriteLine(TimeOnly.MaxValue); 
 
TimeOnly bietet zudem die recht praktische Funktion Between, mit der geprüft werden kann, ob eine Uhrzeit zwischen zwei anderen Uhrzeitangaben liegt. Hier ein Beispiel, das prüft, ob es gerade zwischen 10 und 15 Uhr ist:

var startTime = new TimeOnly(10, 00); 
var endTime = new TimeOnly(15, 00); 
var currentTime = 
  TimeOnly.FromDateTime(DateTime.Now); 

bool between = 
  currentTime.IsBetween(
  startTime, endTime); 

Console.WriteLine( 
  $"start: {startTime} end: {endTime} 
  current: {currentTime} 
  between: {between}"); 
 
Die Operatoren der Struktur unterstützen den Vergleich der Instanzen, so führt der nachstehende Code zur Ausgabe start: 10:00 end: 15:00 - start before end: True.

Console.WriteLine( 
  $"start: {startTime} end: {endTime} - 
  start before end: {startTime < endTime}");  

Strings auf null prüfen

Guard-Anweisungen für Eingabeparameter wurden bisher programmiert, wie sie der nachstehende Code zeigt. Und vermutlich fällt darin auch gleich der bewusst eingebaute Fehler beim Auslösen der Ausnahme von firstName auf, welcher als Parameter nameof(name) entgegennimmt anstelle von nameof(firstName).

// Bisherige Prüfung auf null 
public void Execute(
    string name, string firstName, string company) 
{ 
  if (name is null) 
    throw new ArgumentNullException(nameof(name)); 
  if (firstName is null) 
    throw new ArgumentNullException(nameof(name)); 
  if (company is null) 
    throw new ArgumentNullException( 
      nameof(company)); 
  // logic 
} 
 
Einfacher geht das jetzt mit C# 10 [5]. Die Guard-Anweisungen können wie folgt implementiert werden – ein Fehler wie die oben eingebaute Verwechslung würde sofort auffallen:

// Sicherere Prüfung durch die CallerArgumentExpression 
public void Execute(
    string name, string firstName, string company) 
{ 
  ArgumentNullException.
    ThrowIfNull(name); 
  ArgumentNullException.
    ThrowIfNull(firstName); 
  ArgumentNullException.
    ThrowIfNull(company); 
} 
Technisch umgesetzt wurde dies durch das CallerArgumentExpressionAttribute, das zu der Gruppe von Attributen gehört, mit denen Informationen über den Aufrufer ermittelt werden können. Mehr dazu erfahren Sie unter [6] und [7].

Daten in Chunks verarbeiten

Nicht immer ist es sinnvoll, alle Einträge einer Liste in einem Durchgang zu durchlaufen. Deshalb bietet C# 10 eine Erweiterung, um Listen in kleinen Häppchen (Chunks) zu verarbeiten.Der nachfolgende Code zeigt ein Beispiel, mit dem die Chunks aus einem IEnumerable ausgelesen werden:

// Mit Chunks iterieren 
var list = Enumerable.Range(1, 100); 
const int size = 10; 
int chunkCounter = 0; 
foreach(var chunk in list.Chunk(size)) { 
  Console.WriteLine($"##### ChunkNr.: 
    {chunkCounter++}#####"); 

  foreach(var item in chunk) { 
    Console.WriteLine($"-> {item}"); 
  } 
} 
 
In Bild 11 finden Sie die zum Code gehörende Ausgabe der Chunks auf der Konsole.
Ausgabe von Chunks(Bild 11) © Autor

Erweiterungen von LINQ

Bei Abfragen mit der Erweiterungsmethode FirstOrDefault ist eventuell nicht ganz klar, was diese Funktion liefert, wenn das Wiederfindungsmerkmal nicht gefunden wird.
// Default-Wert 
var list = new List {1, 2, 3}; 
var required = 4; 
var found = list.FirstOrDefault(x => x == required); 
Console.WriteLine(found); 
 
In obigem Beispiel wird der Wert 0 ausgegeben, was dem default(int) entspricht. Wäre der Wert von required ebenfalls 0, wäre nicht klar, ob eine Übereinstimmung gefunden wurde, oder ob anstelle dessen der default(int) als Rückgabe geliefert wurde. Mit .NET 6 ist es nun möglich, einen eigenen Standardwert zu definieren:

var found = list.FirstOrDefault(x => x == required, -1); 
 
Bezugnehmend auf das zuvor genannte Problem ist es mit dieser Variante nun einfacher möglich, zu unterscheiden, ob es sich beim Resultat um den Standardwert handelt oder ob der Wert tatsächlich vorhanden war.

PriorityQueue

Bei einer PriorityQueue ist jedes Element mit der Information über seine Priorität versehen. Soll ein Element aus der Queue geholt werden, erfolgt eine Prüfung, welches der Elemente die höchste Priorität hat, und dieses wird geliefert, unabhängig davon, wann es zur Queue hinzugefügt wurde.Das Verhalten ist somit anders als bei einer klassischen Queue, die mit „first in, first out“ (FIFO) arbeitet, und auch anders als bei einem Stack, der mit „last in, first out“ (LIFO) arbeitet. Im folgenden Beispiel werden vier Elemente zu einer Queue hinzugefügt. Der zweite Parameter (Datentyp int) steht für die Priorität, wobei der geringste Wert der höchsten Priorität entspricht.

// Priority Queue 
var queue = new PriorityQueue(); 
queue.Enqueue("Element A", 0); 
queue.Enqueue("Element B", 20); 
queue.Enqueue("Element C", 5); 
queue.Enqueue("Element D", 3); 

while (queue.TryDequeue( 
  out string element, out int priority)) 
  Console.WriteLine($"element: {element} - 
    priority: {priority}"); 
 
In Bild 12 sehen Sie die erwartungsgemäße Ausgabe dieses Code-Schnipsels: ADCB.
Ausgabe einer Priority Queue(Bild 12) © Autor

Erweiterungsfunktionen Max und MaxBy

Mithilfe der Erweiterungsfunktion Max wird der Maximalwert in einer Sequenz ermittelt. Der folgende Code führt somit zur Ausgabe der Zahl 3.

// Die Funktion Max 
var list = new List { 1, 2, 3 }; 
Console.WriteLine(list.Max()); 
 
Wird Max auf einen komplexen Typ angewendet, so wird der Maximalwert ebenfalls als Integer-Wert zurückgegeben. Anders ist dies bei der Funktion MaxBy. Sie liefert das komplette Objekt zurück, das die Bedingung erfüllt, siehe Bild 13.
Ausgabe von Max und MaxBy(Bild 13) © Autor

// Max und MaxBy mit komplexen Typen 
var users = new List { 
  new User("Alf", 35), new User("Hubert", 18), 
  new User("Alfred", 55) 
}; 

Console.WriteLine(users.Max(x => x.age)); 
Console.WriteLine(users.MaxBy(x => x.age)); 
record User(string name, int age); 

Fazit

.NET 6 in Kombination mit C# 10 bringt einige praktische Neue­rungen mit, die das Programmieren nochmals spürbar bequemer machen. Die neuen Features in Kombination mit Visual Studio 2022 hinterlassen einen erstklassigen Eindruck.

Fussnoten

  1. Tiobe Index, http://www.tiobe.com/tiobe-index/
  2. Global-Using-Direktiven,
  3. DateOnly und TimeOnly,
  4. DateOnly und TimeOnly,
  5. Null argument checks,
  6. Jetbrains, Caller Argument Expressions,
  7. Microsoft, Caller Argument Expressions,

Neueste Beiträge

Arbeiten mit Tabellen und KI in Dataverse
Microsoft unterstützt die zentrale Datenmanagement-Lösung Dataverse in Power Apps mit KI-Features.
7 Minuten
6. Aug 2025
Müssen Ziele SMART sein?
Wenn es um Ziele im Projektmanagement oder in der Führung einer Organisation geht, stoßen wir schnell und fast ausnahmslos auf das Akronym SMART. Was steckt dahinter, und kann es nicht auch sinnvolle Ziele geben, die nicht SMART sind?
8 Minuten
Managed DevOps Pools - Azure DevOps Pipelines Security
Agent Pools als Managed Service mit einfacher Integration in private Netzwerke und Authentisierung mittels Managed Identity tragen deutlich zur Sicherheit der Agent-Infrastruktur bei.
7 Minuten
7. Aug 2025
Miscellaneous

Das könnte Dich auch interessieren

Sicher ist sicher - Azure DevOps Pipelines Security
Als integraler Bestandteil der Entwicklungsumgebung ist Azure DevOps Pipelines oft Ziel von Angriffen. Da ist es gut zu wissen, wo die Schwachstellen des Systems liegen.
14 Minuten
16. Jun 2025
CodeProject.AI Server in neuer Version - Lokaler AI-Server
CodeProject.AI Server (jetzt in Version 2.1.10) ist ein lokal installierter, selbstgehosteter, schneller, kostenloser und Open Source Artificial Intelligence Server für jede Plattform und jede Sprache.
2 Minuten
Für Einsteiger: Backend-Webentwicklung mit .NET - Microsoft
Auf YouTube bietet Microsoft eine Videoserie für Einsteiger in die Backend-Webentwicklung mit .NET.
2 Minuten
13. Feb 2024
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige