Neuerungen in C# Sprachversion 10

C# 10 ist die neueste Version der C#-Programmiersprache, die im November 2023 veröffentlicht wurde. C# 10 bietet viele neue Funktionen und Verbesserungen, die die Produktivität, Leistung und Lesbarkeit des Codes erhöhen. Hier sind einige der wichtigsten Änderungen von C# 10:

Global Usings

Usings sind Anweisungen, die am Anfang einer C#-Datei stehen und angeben, welche Namespaces oder Typen in der Datei verwendet werden. Zum Beispiel:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Diese Usings ermöglichen es, die Typen aus diesen Namespaces ohne den vollständigen Namen zu verwenden. Zum Beispiel:

Console.WriteLine("Hello, world!");
List<int> numbers = new List<int>();
var query = from n in numbers
            where n % 2 == 0
            select n;

Ohne die Usings müsste man die Typen mit dem vollständigen Namen angeben, was den Code länger und unleserlicher machen würde. Zum Beispiel:

System.Console.WriteLine("Hello, world!");
System.Collections.Generic.List<int> numbers = new System.Collections.Generic.List<int>();
var query = from n in numbers
            where n % 2 == 0
            select n;

Usings sind also sehr praktisch, um den Code zu vereinfachen und zu verkürzen. Allerdings haben sie auch einige Nachteile. Erstens müssen sie in jeder Datei wiederholt werden, was zu viel Boilerplate-Code führt. Zweitens können sie zu Namenskonflikten führen, wenn zwei oder mehr Namespaces oder Typen den gleichen Namen haben. Drittens können sie die Lesbarkeit des Codes beeinträchtigen, wenn zu viele Usings verwendet werden.

Um diese Nachteile zu vermeiden, bietet C# 10 eine neue Funktion an: globale Usings. Globale Usings sind Usings, die in einer separaten Datei definiert werden und für alle Dateien in einem Projekt gelten. Zum Beispiel:

// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Text;
global using System.Threading.Tasks;

Diese Datei kann in einem beliebigen Ordner im Projekt gespeichert werden, solange sie den Namen GlobalUsings.cs hat. Mit dieser Datei kann man die Usings in den anderen Dateien weglassen und den Code noch weiter vereinfachen. Zum Beispiel:

// Program.cs
Console.WriteLine("Hello, world!");
List<int> numbers = new List<int>();
var query = from n in numbers
            where n % 2 == 0
            select n;

Wie funktionieren globale Usings?

Globale Usings werden vom Compiler automatisch erkannt und angewendet, wenn eine Datei mit dem Namen GlobalUsings.cs im Projekt vorhanden ist. Man muss also nichts weiter tun, als diese Datei zu erstellen und die gewünschten Usings darin zu definieren. Man kann auch mehrere globale Usings-Dateien in verschiedenen Ordnern haben, solange sie nicht denselben Namespace oder Typ enthalten.

Globale Usings haben einige Besonderheiten, die man beachten sollte.

  • Sie müssen das Schlüsselwort global vor dem using haben, um sie von normalen Usings zu unterscheiden.
  • Sie können nur Namespaces oder statische Typen enthalten, nicht aber Instanztypen oder Aliase.
  • Sie können nicht innerhalb eines Namespaces oder einer Klasse definiert werden, sondern nur außerhalb.
  • Sie können nicht mit der Direktive #if bedingt werden, sondern gelten immer für alle Dateien.

Dateibereichsnamensräume

Namensräume sind eine wichtige Funktion in C#, die es ermöglicht, den Code in logische Einheiten zu organisieren und Namenskonflikte zu vermeiden. Namensräume werden in C# mit dem Schlüsselwort namespace deklariert, gefolgt von einem Namen und einem Block von geschweiften Klammern, der die Typen enthält, die zu dem Namenraum gehören. Zum Beispiel:

namespace MyNamespace
{
    class MyClass
    {
        // ...
    }
    enum MyEnum
    {
        // ...
    }
}

In diesem Beispiel wird ein Namensraum namens MyNamespace definiert, der eine Klasse namens MyClass und eine Aufzählung namens MyEnum enthält. Um auf diese Typen zuzugreifen, muss man entweder den vollständigen Namen angeben, zum Beispiel MyNamespace.MyClass, oder eine using-Anweisung verwenden, zum Beispiel using MyNamespace.

Namensräume können auch verschachtelt werden, das heißt, ein Namensraum kann einen oder mehrere andere Namensräume enthalten. Zum Beispiel:

namespace MyNamespace
{
    namespace MySubNamespace
    {
        class MySubClass
        {
            // ...
        }
    }
}

In diesem Beispiel wird ein Unter-Namensraum namens MySubNamespace innerhalb des Namensraums MyNamespace definiert, der eine Klasse namens MySubClass enthält. Um auf diese Klasse zuzugreifen, muss man entweder den vollständigen Namen angeben, zum Beispiel MyNamespace.MySubNamespace.MySubClass, oder eine using-Anweisung verwenden, zum Beispiel using MyNamespace.MySubNamespace.

Namensräume sind also sehr nützlich, um den Code zu strukturieren und zu modularisieren. Allerdings haben sie auch einige Nachteile.

  1. Sie müssen in jeder Datei explizit deklariert werden, was zu viel Boilerplate-Code führt.
  2. Sie müssen mit geschweiften Klammern umgeben werden, was zu unnötigen Einrückungen führt.
  3. Sie können die Lesbarkeit des Codes beeinträchtigen, wenn zu viele Namenräume verwendet werden.

Um diese Nachteile zu vermeiden, bietet C# 10 eine neue Funktion an: Dateibereichsnamensräume. Dateibereichsnamensräume sind Namensräume, die in einer einzigen Zeile deklariert werden und für die gesamte Datei gelten. Zum Beispiel:

namespace MyNamespace;
class MyClass
{
    // ...
}
enum MyEnum
{
    // ...
}

In diesem Beispiel wird ein Dateibereichsnamensraum namens MyNamespace definiert, der alle Typen in der Datei enthält. Um diesen Namensraum zu verwenden, muss man keine geschweiften Klammern oder Einrückungen verwenden. Man kann auch keine anderen Namensräume in der Datei definieren, da der Dateibereichsnamensraum den gesamten Dateibereich abdeckt.

Dateibereichsnamensräume haben einige Besonderheiten, die man beachten sollte.

  1. Sie müssen am Anfang einer Datei stehen, vor allen anderen Anweisungen oder Deklarationen.
  2. Sie können nur einen Namenraum enthalten, nicht mehrere oder verschachtelte.
  3. Sie können nicht mit der Direktive #if bedingt werden, sondern gelten immer für die gesamte Datei.

Implizite Usings

Eine der grundlegenden Funktionen von C# ist die Verwendung von Namespaces, die es ermöglichen, den Code in logische Einheiten zu organisieren und Namenskonflikte zu vermeiden. Namespaces sind Sammlungen von Typen, die einen gemeinsamen Namen haben, wie z.B. System, System.Collections.Generic, System.Linq oder System.Threading.Tasks. Um auf die Typen aus einem Namespace zuzugreifen, muss man entweder den vollständigen Namen angeben, wie z.B. System.Console.WriteLine, oder eine using-Anweisung verwenden, wie z.B. using System.

Using-Anweisungen sind Anweisungen, die am Anfang einer C#-Datei stehen und angeben, welche Namespaces oder Typen in der Datei verwendet werden.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Diese Using-Anweisungen ermöglichen es, die Typen aus diesen Namespaces ohne den vollständigen Namen zu verwenden, wie z.B. Console.WriteLine oder List<int>.

Um mit weniger Boilerplate-Code auszukommen, da man die usings für gewöhnlich in jeder Datei wiederholt, bietet C# 10 implizite Usings. Implizite Usings sind Using-Anweisungen, die automatisch hinzugefügt werden, ohne dass sie explizit geschrieben werden müssen. Sie gelten für alle Dateien in einem Projekt und können in der Projektdatei oder in der global.json-Datei konfiguriert werden.

Funktionsweise

Implizite Usings werden vom Compiler automatisch erkannt und angewendet, wenn eine Projektdatei oder eine global.json-Datei die entsprechende Option enthält. Zum Beispiel:

<!-- csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
// global.json
{
  "sdk": {
    "version": "6.0.100",
    "implicitUsings": true
  }
}

Diese Optionen aktivieren implizite Usings für das gesamte Projekt oder für alle Projekte, die die angegebene SDK-Version verwenden.

Mit impliziten Usings kann man die Using-Anweisungen in den Dateien weglassen und den Code noch weiter vereinfachen.

// Program.cs
Console.WriteLine("Hello, world!");
List<int> numbers = new List<int>();
var query = from n in numbers
            where n % 2 == 0
            select n;

Implizite Usings haben einige Besonderheiten, die man beachten sollte.

  1. Sie können nur Namespaces enthalten, nicht aber Typen oder Aliase.
  2. Sie können nicht innerhalb eines Namespaces oder einer Klasse definiert werden, sondern nur außerhalb.
  3. Sie können nicht mit der Direktive #if bedingt werden, sondern gelten immer für alle Dateien.

Implizite Usings können auch manuell angepasst werden, indem man die Option ImplicitUsings auf einen bestimmten Wert setzt.

<!-- csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>default;Microsoft.AspNetCore</ImplicitUsings>
  </PropertyGroup>
</Project>

Dieser Wert fügt dem Standardwert, der die häufig verwendeten Namespaces wie System, System.Collections.Generic, System.Linq und System.Threading.Tasks enthält, einen zusätzlichen Namespace hinzu: Microsoft.AspNetCore. Dies ist nützlich, wenn man eine Webanwendung erstellt, die diesen Namespace benötigt.

Implizite Usings können auch deaktiviert werden, indem man die Option ImplicitUsings auf disable oder none setzt.

<!-- csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>disable</ImplicitUsings>
  </PropertyGroup>
</Project>

Dieser Wert verhindert, dass implizite Usings angewendet werden, und erfordert, dass alle Using-Anweisungen explizit geschrieben werden. Dies ist nützlich, wenn man die volle Kontrolle über die Using-Anweisungen haben möchte oder wenn man ein bestehendes Projekt auf C# 10 aktualisiert und keine Änderungen am Code vornehmen möchte.

Konstante Verkettung von Zeichenfolgeninterpolationen

Eine der grundlegenden Funktionen von C# ist die Verwendung von Zeichenfolgen, die Sequenzen von Unicode-Zeichen sind, die Text darstellen. Zeichenfolgen können in C# mit doppelten Anführungszeichen erstellt werden, wie z.B. "Hello, world!". Zeichenfolgen können auch mit dem Operator + verkettet werden, um neue Zeichenfolgen zu erstellen, wie z.B. "Hello, " + "world!".

Zeichenfolgen können auch mit dem Dollarzeichen ($), das als Präfix verwendet wird, interpoliert werden, um Ausdrücke in die Zeichenfolge einzufügen, die zur Laufzeit ausgewertet werden. Zum Beispiel:

int x = 10;
int y = 20;
string s = $"The sum of {x} and {y} is {x + y}";

In diesem Beispiel wird eine Zeichenfolge mit drei Interpolationen erstellt, die die Werte von x, y und x + y enthalten. Die Zeichenfolge hat den Wert “The sum of 10 and 20 is 30”.

Zeichenfolgeninterpolationen sind also sehr nützlich, um den Code zu vereinfachen und zu verkürzen. Allerdings haben sie auch einige Nachteile.

  1. Sie können nur zur Laufzeit ausgewertet werden, nicht zur Kompilierzeit.
  2. Sie können nicht als konstante Werte verwendet werden, die zur Kompilierzeit zugewiesen werden müssen.
  3. Sie können die Leistung und den Speicherverbrauch beeinträchtigen, wenn sie häufig verwendet werden.

Um diese Nachteile zu vermeiden, bietet C# 10 eine neue Funktion an: konstante Verkettung von Zeichenfolgeninterpolationen. Das sind Zeichenfolgeninterpolationen, die zur Kompilierzeit ausgewertet werden und als konstante Werte verwendet werden können. Sie werden mit dem Schlüsselwort const deklariert, das als Präfix verwendet wird. Zum Beispiel:

const int x = 10;
const int y = 20;
const string s = $"The sum of {x} and {y} is {x + y}";

In diesem Beispiel wird eine konstante Zeichenfolge mit drei Interpolationen erstellt, die die Werte von x, y und x + y enthalten. Die Zeichenfolge hat den Wert “The sum of 10 and 20 is 30” und wird zur Kompilierzeit zugewiesen.

Funktionsweise

Konstante Verkettung von Zeichenfolgeninterpolationen werden vom Compiler automatisch erkannt und angewendet, wenn eine Zeichenfolge mit dem Dollarzeichen ($), das als Präfix verwendet wird, mit dem Schlüsselwort const deklariert wird. Der Compiler wertet die Interpolationen zur Kompilierzeit aus und erstellt eine konstante Zeichenfolge, die den resultierenden Wert enthält. Der Compiler optimiert auch die Verkettung von konstanten Zeichenfolgeninterpolationen, indem er sie zu einer einzigen Zeichenfolge zusammenfügt. Zum Beispiel:

const string s1 = $"Hello, ";
const string s2 = $"world!";
const string s3 = s1 + s2;

In diesem Beispiel werden drei konstante Zeichenfolgeninterpolationen erstellt, die die Werte “Hello, “, “world!” und “Hello, world!” enthalten. Der Compiler optimiert die Verkettung von s1 und s2 und erstellt eine konstante Zeichenfolge, die den Wert “Hello, world!” enthält.

Einige Besonderheiten, die man beachten sollte:

  1. Sie müssen mit dem Schlüsselwort const deklariert werden, um sie von normalen Zeichenfolgeninterpolationen zu unterscheiden.
  2. Sie können nur konstante Ausdrücke enthalten, die zur Kompilierzeit ausgewertet werden können, wie z.B. Literale, Konstanten oder Enum-Werte.
  3. Sie können nicht mit der Direktive #if bedingt werden, sondern gelten immer für alle Dateien.

Vorteile

  • Sie ermöglichen die Erstellung von konstanten Zeichenfolgen, die zur Kompilierzeit zugewiesen werden und nicht zur Laufzeit ausgewertet werden müssen.
  • Sie verbessern die Leistung und den Speicherverbrauch, indem sie die Anzahl der Allokationen und Verkettungen von Zeichenfolgen reduzieren.
  • Sie vereinfachen den Code und machen ihn lesbarer, indem sie die Verwendung von Ausdrücken in Zeichenfolgen ermöglichen, ohne zusätzliche Formatierung oder Konvertierung zu erfordern.
  • Sie ermöglichen eine flexible Anpassung, indem sie verschiedene Optionen für die Formatierung, Kultur oder Ausrichtung der Interpolationen bieten.

Erweiterte Eigenschaftsmuster

Eine der fortgeschrittenen Funktionen von C# ist der Musterabgleich, der es ermöglicht, den Typ oder den Wert eines Ausdrucks zu überprüfen und entsprechend zu handeln. Musterabgleich wird in C# mit dem Schlüsselwort switch oder dem Operator is verwendet, die verschiedene Arten von Mustern unterstützen, wie z.B. konstante Muster, Typmuster, Tupelmuster, Positions- oder Deconstruct-Muster, relationalen Muster oder logischen Muster.

Eine spezielle Art von Muster, die in C# 8 eingeführt wurde, ist das Eigenschaftsmuster, das es ermöglicht, die Eigenschaften eines Objekts zu überprüfen und zu vergleichen. Eigenschaftsmuster werden mit geschweiften Klammern geschrieben, die den Namen und den Wert der Eigenschaften enthalten, die überprüft werden sollen. Zum Beispiel:

Person person = new Person("Alice", 25);
switch (person)
{
    case { Name: "Alice", Age: var age }:
        Console.WriteLine($"Alice is {age} years old");
        break;
    case { Name: "Bob", Age: > 30 }:
        Console.WriteLine("Bob is older than 30");
        break;
    default:
        Console.WriteLine("Unknown person");
        break;
}

In diesem Beispiel wird ein Objekt vom Typ Person mit zwei Eigenschaften, Name und Age, erstellt und mit einem switch-Ausdruck überprüft. Der switch-Ausdruck verwendet zwei Eigenschaftsmuster, um den Namen und das Alter der Person zu vergleichen und eine entsprechende Nachricht auszugeben.

Eigenschaftsmuster sind sehr nützlich, um den Code zu vereinfachen und zu verkürzen. Allerdings haben sie auch einige Nachteile:

  1. Die Eigenschaften muss explizit benennant werden, was zu viel Wiederholung führen kann, wenn mehrere Eigenschaften überprüft werden sollen.
  2. Die Eigenschaften müssen mit Kommas getrennt werden, was zu Verwirrung führen kann, wenn die Eigenschaften selbst Kommas enthalten.
  3. Es kann die Lesbarkeit des Codes beeinträchtigen, wenn zu viele Eigenschaften in einem Muster verwendet werden.

Erweiterte Eigenschaftsmuster werden vom Compiler automatisch erkannt und angewendet, wenn ein Eigenschaftsmuster mit geschweiften Klammern geschrieben wird. Der Compiler unterstützt verschiedene Verbesserungen und Erweiterungen für die Eigenschaftsmuster, wie z.B.:

  • Weglassen des Eigenschaftsnamens: Wenn der Name der Eigenschaft mit dem Namen der Variablen übereinstimmt, die den Wert der Eigenschaft speichert, kann der Eigenschaftsname weggelassen werden. Zum Beispiel:
Person person = new Person("Alice", 25);
switch (person)
{
    case { Name: "Alice", Age }: // Age is equivalent to Age: var Age
        Console.WriteLine($"Alice is {Age} years old");
        break;
    // ...
}
  • Weglassen der geschweiften Klammern: Wenn das Eigenschaftsmuster nur eine Eigenschaft enthält, können die geschweiften Klammern weggelassen werden. Zum Beispiel:
Person person = new Person("Alice", 25);
switch (person)
{
    case { Name: "Alice" }: // { Name: "Alice" } is equivalent to Name: "Alice"
        Console.WriteLine("Alice is here");
        break;
    // ...
}
  • Verwenden von Semikolons: Wenn das Eigenschaftsmuster mehrere Eigenschaften enthält, können die Eigenschaften mit Semikolons anstelle von Kommas getrennt werden. Dies ist nützlich, wenn die Eigenschaften selbst Kommas enthalten, wie z.B. Tupel oder anonyme Typen. Zum Beispiel:
Person person = new Person(("Alice", "Smith"), 25);
switch (person)
{
    case { Name: ("Alice", var lastName); Age }:
        Console.WriteLine($"Alice {lastName} is {Age} years old");
        break;
    // ...
}
  • Verwenden von relationalen Mustern: Wenn das Eigenschaftsmuster einen Vergleich zwischen der Eigenschaft und einem Wert enthält, kann das relationale Muster verwendet werden, um den Vergleich auszudrücken. Dies ist nützlich, um die Verwendung von Operatoren wie ==, !=, <, >, <= oder >= zu vermeiden. Zum Beispiel:
Person person = new Person("Alice", 25);
switch (person)
{
    case { Name: "Alice"; Age: > 20 and < 30 }:
        Console.WriteLine("Alice is in her twenties");
        break;
    // ...
}

Erweiterte Eigenschaftsmuster haben einige Besonderheiten, die man beachten sollte.

  1. Sie müssen mit einem Typmuster oder einem Positions- oder Deconstruct-Muster kombiniert werden, um den Typ oder die Form des Objekts zu überprüfen, bevor die Eigenschaften überprüft werden.
  2. Sie können nicht mit anderen Arten von Mustern wie konstanten Mustern, Tupelmustern oder logischen Mustern kombiniert werden, da dies zu Mehrdeutigkeiten führen kann.
  3. Sie können nicht mit der Direktive #if bedingt werden, sondern gelten immer für alle Dateien.

Vorteile

Erweiterte Eigenschaftsmuster bieten viele Vorteile:

  • Sie ermöglichen die Überprüfung und den Vergleich von mehreren Eigenschaften eines Objekts in einem einzigen Muster, ohne die Eigenschaften explizit zu benennen oder zu wiederholen.
  • Sie verbessern die Lesbarkeit und die Klarheit des Codes, indem sie die Verwendung von Semikolons anstelle von Kommas und von relationalen Mustern anstelle von Operatoren ermöglichen.
  • Sie vereinfachen den Code und machen ihn kürzer, indem sie die Verwendung von geschweiften Klammern für einzelne Eigenschaften vermeiden.
  • Sie ermöglichen eine flexible Anpassung, indem sie verschiedene Optionen für die Formatierung, Kultur oder Ausrichtung der Eigenschaften bieten.

Struct records

Struct records sind eine neue Funktion in C# 10, die es erlaubt, unveränderliche Werttypen zu definieren, die wie Referenztypen behandelt werden können. Struct records kombinieren die Vorteile von Strukturen und Records, wie zum Beispiel die Speichereffizienz, die Wertgleichheit, die syntaktische Einfachheit und die funktionale Programmierung.

Um einen struct record zu definieren, muss man das Schlüsselwort record vor dem Schlüsselwort struct angeben, gefolgt von dem Namen und den Eigenschaften des Typs. Zum Beispiel:

record struct Point(int X, int Y);

Dieser Code definiert einen struct record Point, der zwei Eigenschaften X und Y vom Typ int hat. Der Compiler generiert automatisch einen parameterlosen Konstruktor, einen Konstruktor mit den gleichen Parametern wie die Eigenschaften, eine ToString-Methode, eine Equals-Methode, eine GetHashCode-Methode und einen Deconstruct-Methode für diesen Typ.

Struct records können auch von anderen struct records oder Schnittstellen erben, aber nicht von Klassen oder anderen Strukturen:

record struct Circle(Point Center, double Radius) : IShape;

Dieser Code definiert einen struct record Circle, der von einem anderen struct record Point erbt und das Interface IShape implementiert. Der Compiler generiert zusätzlich eine Copy-Methode, die eine Kopie des Objekts mit geänderten Eigenschaften erstellt.

Struct records können auch Methoden, Operatoren, Ereignisse und andere Mitglieder enthalten, die man für gewöhnliche Strukturen definieren kann:

record struct Rational(int Numerator, int Denominator)
{
    public static Rational operator +(Rational left, Rational right)
    {
        // ...
    }

    public double ToDouble()
    {
        // ...
    }
}

Dieser Code definiert einen struct record Rational, der eine Methode ToDouble und einen Operator + enthält.

Struct records erlauben es, unveränderliche Datenstrukturen zu erstellen, die keinen unnötigen Speicherplatz verbrauchen, die Wertgleichheit anstatt der Referenzgleichheit verwenden, die mit funktionalen Paradigmen kompatibel sind und die eine klare und prägnante Syntax haben.

Verbesserte Lambdaausdrücke

Lambdaausdrücke sind anonyme Funktionen, die als Parameter an andere Methoden übergeben oder als Ausdrücke ausgewertet werden können. Lambdaausdrücke sind eine wichtige Funktion in C#, die es erlaubt, funktionale Programmierung zu unterstützen, den Code zu vereinfachen und die Lesbarkeit zu erhöhen.

In C# 10 wurden Lambdaausdrücke verbessert, indem sie mehr Funktionen und Flexibilität erhalten haben. Die wichtigsten Verbesserungen sind:

  • Attributierte Lambdaausdrücke: Man kann nun Attribute an Lambdaausdrücke anhängen, um zusätzliche Informationen oder Anweisungen für den Compiler oder die Laufzeit zu liefern. Zum Beispiel kann man das Attribut [UnmanagedCallersOnly] verwenden, um anzugeben, dass ein Lambdaausdruck von nicht verwaltetem Code aufgerufen werden kann. Zum Beispiel:
[UnmanagedCallersOnly]
static Func<int, int> Square = x => x * x;

Dieser Code definiert einen Lambdaausdruck, der eine Zahl quadriert und das Attribut [UnmanagedCallersOnly] hat, das es ermöglicht, ihn von nativem Code aus aufzurufen.

  • Natürliche Typinferenz für Lambdaausdrücke: Man kann nun den Typ eines Lambdaausdrucks aus dem Kontext ableiten, ohne ihn explizit anzugeben. Zum Beispiel kann man einen Lambdaausdruck an eine lokale Variable zuweisen, ohne den Delegattyp zu spezifizieren. Zum Beispiel:
var Add = (x, y) => x + y;

Dieser Code definiert einen Lambdaausdruck, der zwei Zahlen addiert und ihn einer lokalen Variable Add zuweist, ohne den Typ Func<int, int, int> anzugeben.

  • Statische Lambdaausdrücke: Man kann nun das Schlüsselwort static vor einem Lambdaausdruck verwenden, um anzugeben, dass er keinen Zugriff auf das this-Objekt oder die Instanzvariablen der umgebenden Klasse hat. Dies kann die Performance verbessern, da keine zusätzlichen Objekte erzeugt werden müssen, um den Lambdaausdruck zu erfassen. Zum Beispiel:
class Calculator
{
    public int Factorial(int n) => n <= 1 ? 1 : n * Factorial(n - 1);

    public void PrintFactorials(int n)
    {
        Enumerable.Range(1, n).Select(static x => Factorial(x)).ToList().ForEach(Console.WriteLine);
    }
}

Dieser Code definiert eine Methode PrintFactorials, die die Fakultäten von 1 bis n berechnet und ausgibt. Dabei wird ein statischer Lambdaausdruck verwendet, der die Methode Factorial aufruft, ohne das this-Objekt zu erfassen.

  • Erweiterte Lambdaausdrücke: Man kann nun mehrere Anweisungen in einem Lambdaausdruck schreiben, ohne geschweifte Klammern oder einen return-Ausdruck zu verwenden. Dies kann den Code kürzer und klarer machen. Zum Beispiel:
Func<int, int, int> Max = (x, y) =>
    if (x > y)
        x;
    else
        y;

Dieser Code definiert einen Lambdaausdruck, der das Maximum von zwei Zahlen zurückgibt, ohne geschweifte Klammern oder einen return-Ausdruck zu verwenden.

Erweiterte Generika

Erweiterte Generika sind eine neue Funktion in C# 10, die es erlaubt, generische Typen mit zusätzlichen Einschränkungen zu versehen. Diese Einschränkungen können Attribute, Schnittstellen, Delegaten oder andere generische Typen sein. Erweiterte Generika ermöglichen es, den Typsicherheitsgrad zu erhöhen, die Code-Wiederverwendbarkeit zu verbessern und die Lesbarkeit zu erhöhen.

Um erweiterte Generika zu verwenden, muss man das Schlüsselwort where nach dem generischen Typnamen angeben, gefolgt von einer oder mehreren Einschränkungen, die durch Kommas getrennt sind:

class Stack<T> where T : struct, IComparable<T>, new()
{
    // ...
}

Dieser Code definiert eine generische Klasse Stack<T>, die nur Werttypen (struct), die das Interface IComparable<T> implementieren und einen parameterlosen Konstruktor haben (new()), als Typargumente akzeptiert. Wenn man versucht, einen anderen Typ zu verwenden, wird ein Kompilierfehler ausgelöst.

Erweiterte Generika können auch für Methoden, Delegaten, Schnittstellen und Strukturen verwendet werden:

interface IRepository<T> where T : class, IDisposable
{
    // ...
}
delegate TResult Func<T, TResult>(T arg) where T : notnull;
struct Pair<TFirst, TSecond> where TFirst : TSecond
{
    // ...
}
T Max<T>(T x, T y) where T : IComparable<T>
{
    // ...
}

Diese Beispiele zeigen, wie man erweiterte Generika für verschiedene Szenarien anwenden kann. Dabei werden einige neue Einschränkungen in C# 10 eingeführt, wie zum Beispiel notnull, die verlangt, dass der Typ nicht null sein darf, oder TFirst : TSecond, die eine Vererbungsbeziehung zwischen den Typen erfordert.

Erweiterte Generika erlauben es, den Typumfang zu verfeinern, die Typüberprüfung zur Kompilierzeit zu verstärken, die Anzahl der notwendigen Casts zu reduzieren, die Performance zu optimieren und die Intellisense-Funktion zu verbessern.

Leave a Reply

Your email address will not be published. Required fields are marked *