Skip to content

C# Spickzettel

Moderne objektorientierte Sprache für .NET, Web und Spiele.

01

Erste Schritte

Hello World

C#-Programme starten in Main(). using System; importiert den System-Namespace (Console, Math usw.). C# 9+ unterstützt Top-Level-Anweisungen: Eine Datei nur mit Console.WriteLine("Hello"); ist ein gültiges Programm. Der Compiler generiert die Klasse und Main automatisch.

csharp
using System;

class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
    }
}

Variablen & Typen

C# ist statisch typisiert. var lässt den Compiler den Typ ableiten (zur Kompilierzeit weiterhin typsicher). decimal (m-Suffix) ist für finanzielle Berechnungen mit exakter Genauigkeit. const ist eine Kompilierzeitkonstante; readonly ist eine Laufzeitkonstante, die im Konstruktor gesetzt wird.

csharp
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42;  // implicitly typed (int)
const double TAX = 0.08;

String-Interpolation

String-Interpolation ($"...") bettet Ausdrücke in geschweifte Klammern ein. Formatbezeichner nach dem : steuern die Ausgabe (F2 für 2 Dezimalstellen, yyyy-MM-dd für Datumsangaben). Verbatim-Strings (@"") behandeln Backslashes wörtlich – nützlich für Dateipfade und reguläre Ausdrücke. Beides kombinieren: $@"...".

csharp
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");

// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}");  // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";

Eingabe & Ausgabe

Console.ReadLine() liest eine ganze Zeile als String. int.Parse konvertiert, wirft aber bei ungültiger Eingabe; int.TryParse ist sicherer – es gibt bool zurück und verwendet einen out-Parameter. Verwenden Sie immer TryParse für Benutzereingaben, um Ausnahmen durch fehlerhafte Daten zu vermeiden.

csharp
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());

Console.WriteLine($"Hi {name}, you are {age}");

// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
    Console.WriteLine($"Valid: {num}");
} else {
    Console.WriteLine("Invalid number");
}

Nullable-Typen

int? ist ein Nullable-Werttyp (Nullable<int>). Der ?.-Operator (null-bedingt) greift sicher auf Member zu – gibt null statt einer Ausnahme zurück. Der ??-Operator (null-coalescing) liefert einen Standardwert für null. Nullable Referenztypen in C# 8+ warnen zur Kompilierzeit über potenzielle null-Werte.

csharp
int? maybeAge = null;  // nullable int
if (maybeAge.HasValue) {
    Console.WriteLine(maybeAge.Value);
}

string? maybeName = null;  // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0;  // null-conditional + null-coalescing

// ValueOrDefault
int age = maybeAge ?? 0;  // 0 if null
02

Strings

String-Methoden

C#-Strings sind unveränderlich – Methoden geben neue Strings zurück statt das Original zu ändern. Häufige Methoden: Length, ToUpper/ToLower, Substring, IndexOf, Replace, Contains, Split. Für intensive String-Manipulation verwenden Sie StringBuilder, um viele Zwischen-Strings zu vermeiden.

csharp
string s = "Hello, World!";
Console.WriteLine(s.Length);        // 13
Console.WriteLine(s.ToUpper());     // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", "));   // ["Hello", "World!"]

StringBuilder

StringBuilder ist veränderlich und effizient zum Erstellen von Strings in Schleifen. String-Verkettung (+) erzeugt jedes Mal einen neuen String, daher ist wiederholte Verkettung O(n^2). StringBuilder amortisiert auf O(n). Verwenden Sie StringBuilder bei mehr als wenigen Verkettungen.

csharp
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();

// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;

Formatierung & Parsing

string.Format verwendet {0}, {1}-Platzhalter. Formatbezeichner: F (fest), N (Zahl mit Trennzeichen), P (Prozent), X (hex). Parsing ist kulturabhängig – Dezimaltrennzeichen unterscheiden sich je nach Locale. Verwenden Sie CultureInfo für explizite Kontrolle oder TryParse für Sicherheit.

csharp
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}";  // "3.14"
string s3 = $"{1234567:N0}";  // "1,234,567" (thousands separator)

// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");

// Culture-aware parsing
double euro = double.Parse("3,14",
    System.Globalization.CultureInfo.GetCultureInfo("de-DE"));

String-Vergleich

Verwenden Sie StringComparison.OrdinalIgnoreCase für case-insensitiven Vergleich – es ist klarer und schneller als ToLower() dann ==. Vermeiden Sie == für kulturabhängige Vergleiche; verwenden Sie string.Compare mit einer CultureInfo. Für Dictionary-Schlüssel verwenden Sie StringComparer.OrdinalIgnoreCase.

csharp
string a = "Hello", b = "hello";

// Case-sensitive
bool equal = a == b;  // false
bool equal2 = a.Equals(b);  // false

// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);

// Best practice for comparison
bool same = string.Equals(a, b,
    StringComparison.OrdinalIgnoreCase);

Reguläre Ausdrücke

Regex (aus System.Text.RegularExpressions) bietet Pattern-Matching. Verwenden Sie @""-Verbatim-Strings, damit Backslashes nicht doppelt escapet werden müssen. Bei wiederholter Verwendung kompilieren Sie eine Regex-Instanz einmal und verwenden sie erneut (RegexOptions.Compiled) für bessere Leistung.

csharp
using System.Text.RegularExpressions;

string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value);  // 123-456-7890

// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"

// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");
03

Zahlen & Mathematik

Numerische Typen

C# hat typen fester Größe: int (32-Bit), long (64-Bit), double (64-Bit-Float), decimal (128-Bit, für Finanzen). Unterstriche (9_000_000) verbessern die Lesbarkeit. Verwenden Sie decimal für Geld – es vermeidet Floating-Point-Rundungsfehler. Suffixe: L (long), f (float), m (decimal).

csharp
int i = 42;
long big = 9_000_000_000L;  // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m;     // exact decimal for money
byte b = 255;

Console.WriteLine(sizeof(int));   // 4
Console.WriteLine(int.MaxValue);  // 2147483647

Math-Klasse

Math bietet statische Methoden für gängige Operationen. Math.Round verwendet standardmäßig Banker's Rounding (rundet auf gerade) – verwenden Sie MidpointRounding.AwayFromZero für schulisches Runden. Math.BigMul behandelt long-Multiplikation, um Overflow zu vermeiden.

csharp
double x = 2.5;
Math.Pow(x, 3);     // 15.625
Math.Sqrt(x);       // 1.581
Math.Abs(-5);       // 5
Math.Floor(3.7);    // 3
Math.Ceiling(3.2);  // 4
Math.Round(3.5);    // 4 (banker's rounding)
Math.Max(3, 7);     // 7
Math.Min(3, 7);     // 3
Math.PI;            // 3.14159...

Zufallszahlen

Random generiert Pseudo-Zufallszahlen. Erstellen Sie eine Instanz und verwenden Sie sie erneut (das Erstellen vieler in einer Schleife kann aufgrund zeitbasierter Seeding Duplikate erzeugen). Für kryptografische Zufälligkeit verwenden Sie System.Security.Cryptography.RandomNumberGenerator. C# 8+ bietet Random.Shared für Thread-Sicherheit.

csharp
var random = new Random();
int r = random.Next(1, 101);     // 1-100
double d = random.NextDouble();   // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);

// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);

// Array of random items
int[] nums = Enumerable.Range(0, 5)
    .Select(_ => random.Next(1, 100)).ToArray();

Typumwandlung

Implizite Konvertierungen erfolgen automatisch, wenn keine Daten verloren gehen (int zu double). Explizite Casts (type) sind erforderlich, wenn Genauigkeit verloren gehen kann. Convert.ToInt32 behandelt viele Typen und rundet (im Gegensatz zu Cast, der abschneidet). Bevorzugen Sie immer TryParse für String-zu-Zahl-Konvertierung, um Ausnahmen zu vermeiden.

csharp
// Implicit (safe, no data loss)
int i = 42;
double d = i;  // int -> double

// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi;  // 3

// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);

// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result);  // false

Integer-Overflow & checked

Standardmäßig wird Integer-Overflow stillschweigend umgebrochen (unchecked). Der checked-Block wirft OverflowException bei Overflow – verwenden Sie ihn für sicherheitskritischen Code. Für wirklich große Zahlen behandelt BigInteger (System.Numerics) beliebig genaue Ganzzahlen ohne Overflow.

csharp
int a = int.MaxValue;  // 2147483647
// a++;  // overflow: wraps to -2147483648 (unchecked by default)

checked {
    a++;  // throws OverflowException!
}

// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big);  // 1267650...376 (31 digits)
04

Kontrollfluss

If / Else

if/else if/else ist Standard. Der ternäre Operator (condition ? a : b) ist ein prägnanter if/else-Ausdruck. C# erfordert boolesche Bedingungen – im Gegensatz zu C werden Ganzzahlen nicht implizit zu bool konvertiert. Verwenden Sie geschweifte Klammern auch bei einzelnen Anweisungen, um Wartungsfehler zu vermeiden.

csharp
int score = 85;
if (score >= 90) {
    Console.WriteLine("A");
} else if (score >= 80) {
    Console.WriteLine("B");
} else {
    Console.WriteLine("C");
}

// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";

Switch & Pattern Matching

Switch-Ausdrücke in C# 8+ (=>) sind prägnant und geben Werte zurück. Das _-Pattern ist der Standardfall. Pattern Matching (is, switch) unterstützt Typ-Pattern, Eigenschafts-Pattern und relationale Pattern (> 18). Dies ist mächtiger als traditionelles switch und reduziert Boilerplate.

csharp
// Classic switch
int day = 3;
string name = day switch {
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    6 or 7 => "Weekend",
    _ => "Invalid"  // default
};

// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
    Console.WriteLine(s);  // "hello"
}

// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
    // matches if Age > 18 and Name == "Alice"
}

Schleifen

for ist für Zählschleifen; foreach iteriert über Collections (Arrays, Listen, IEnumerable); while läuft, bis eine Bedingung false ist. foreach ist schreibgeschützt – Sie können die Collection während der Iteration nicht ändern. Verwenden Sie for, wenn Sie den Index benötigen oder Elemente ändern müssen.

csharp
// for loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
    Console.WriteLine(fruit);
}

// while
int n = 5;
while (n > 0) Console.WriteLine(n--);

Break, Continue & goto

break verlässt die nächste Schleife oder switch; continue springt zur nächsten Iteration. C# hat kein labeled break für verschachtelte Schleifen – verwenden Sie ein Flag, extrahieren Sie in eine Methode mit return, oder verwenden Sie LINQ. goto ist in switch für Fall-Through gültig, aber sonst verpönt.

csharp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.WriteLine(i);  // 0 1 2 4 5 6
}

// goto in switch (rare but valid)
switch (code) {
    case 1:
        DoSomething();
        goto case 2;  // fall through
    case 2:
        DoMore();
        break;
}

Iteratoren & yield

yield return macht eine Methode zu einem Iterator, der Werte verzögert erzeugt – einen nach dem anderen bei Bedarf. Dies ist speichereffizient für große oder unendliche Sequenzen. Der Compiler generiert eine Zustandsmaschine. yield break beendet die Iteration früh. LINQ verwendet Iteratoren extensiv.

csharp
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
    for (int i = 0; i < 5; i++) {
        yield return i;
    }
}

foreach (int n in GetNumbers()) {
    Console.WriteLine(n);  // 0 1 2 3 4
}

// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
05

Methoden & Delegates

Methodendefinition

Methoden können ausdrucksbasiert sein (=>) für Einzeiler. Standardparameter machen Argumente optional. Benannte Argumente (name:) verbessern die Lesbarkeit bei Aufrufen mit vielen Parametern und erlauben das Überspringen optionaler. C# unterstützt kein Methoden-Overloading nur mit unterschiedlichen Rückgabetypen.

csharp
static int Add(int a, int b) {
    return a + b;
}

// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name = "Guest", int times = 1) {
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hi {name}!");
}

Greet();              // Hi Guest!
Greet("Alice", 3);    // Hi Alice! x3
Greet(times: 2);      // named argument

out, ref & params

out-Parameter müssen von der Methode zugewiesen werden (Aufrufer muss nicht initialisieren). ref-Parameter müssen vom Aufrufer initialisiert werden und können geändert werden. params erlaubt eine variable Anzahl von Argumenten. C# 7+ erlaubt out var-Deklaration inline: TryParse(s, out var result).

csharp
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
    result = 0;
    return int.TryParse(s, out result);
}

// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x);  // x is now 6

// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3);  // 6
Sum();         // 0

Delegates & Events

Delegates sind typsichere Funktionszeiger. Func<T,TResult> nimmt Eingaben und gibt einen Wert zurück; Action<T> gibt void zurück; Predicate<T> gibt bool zurück. Events sind Delegates mit +=/-= Abonnement und ?.Invoke für sicheres Auslösen (null-Check). Verwenden Sie Events für das Observer-Pattern (Pub/Sub).

csharp
// Delegate type
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4));  // 7

// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");

Lambda-Ausdrücke

Lambdas sind anonyme Methoden mit =>. Sie erfassen Variablen aus dem umschließenden Scope (Closures). Erfasste Variablen werden bei Ausführung des Lambda ausgewertet, nicht bei Erstellung – Vorsicht in Schleifen. Verwenden Sie Lambdas extensiv mit LINQ, Events und Callbacks.

csharp
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;

// Type inference
Func<int, int, int> add = (a, b) => a + b;

// Statement lambda
Action<string> log = msg => {
    Console.WriteLine($"[{DateTime.Now}] {msg}");
};

// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5));  // 15

Erweiterungsmethoden

Erweiterungsmethoden fügen vorhandenen Typen Methoden hinzu, ohne sie zu ändern. Das this-Schlüsselwort vor dem ersten Parameter markiert sie als Erweiterungsmethode. Sie müssen in einer statischen Klasse sein. LINQ ist vollständig als Erweiterungsmethoden auf IEnumerable implementiert. Dies ist ein mächtiger Weg, Hilfsmethoden hinzuzufügen.

csharp
static class StringExtensions {
    public static int WordCount(this string s) {
        return s.Split(new[] { ' ' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

string text = "Hello World C#";
Console.WriteLine(text.WordCount());  // 3

// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);
06

Collections

List<T>

List<T> ist ein dynamisches Array (wie vector in C++ oder ArrayList in Java). Add/Remove sind O(1) amortisiert / O(n). Count gibt die Anzahl der Elemente (nicht die Kapazität). Find/FindAll verwenden Prädikate. Bei häufigen Einfügungen/Löschungen in der Mitte kann LinkedList<T> besser sein.

csharp
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count);  // 4
Console.WriteLine(nums.Contains(4));  // true

// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);

Dictionary<TKey, TValue>

Dictionary ist eine Hash-Tabelle mit O(1) durchschnittlichem Lookup. TryGetValue ist sicherer als der Indexer (der KeyNotFoundException wirft). Für case-insensitive String-Schlüssel übergeben Sie StringComparer.OrdinalIgnoreCase an den Konstruktor. Dictionary erhält keine Einfügereihenfolge (verwenden Sie OrderedDictionary bei Bedarf).

csharp
var ages = new Dictionary<string, int> {
    ["Alice"] = 30,
    ["Bob"] = 25
};
ages["Charlie"] = 35;

// Safe access
if (ages.TryGetValue("Alice", out int age)) {
    Console.WriteLine($"Alice: {age}");
}

// Iterate
foreach (var kv in ages) {
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// Case-insensitive keys
var dict = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

HashSet<T> & SortedSet<T>

HashSet<T> speichert eindeutige Elemente mit O(1)-Lookup – verwenden Sie es für Deduplizierung und Mitgliedschaftstests. Es unterstützt Mengenoperationen (Union, Intersect, Except). SortedSet<T> (Rot-Schwarz-Baum) hält Elemente sortiert mit O(log n)-Operationen. Verwenden Sie HashSet, wenn die Reihenfolge keine Rolle spielt, SortedSet, wenn sie es tut.

csharp
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2);  // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count);  // 4
Console.WriteLine(set.Contains(3));  // true

// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other);  // set: {3, 4}
set.UnionWith(other);      // set: {3, 4, 5}

// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 };  // {1, 2, 3}

Queue<T> & Stack<T>

Queue<T> ist FIFO (Enqueue/Dequeue) – verwenden Sie für Aufgabenplanung, BFS. Stack<T> ist LIFO (Push/Pop) – verwenden Sie für Undo/Redo, DFS, Ausdrucksauswertung. Beide sind O(1) für ihre Kernoperationen. Für nebenläufige Szenarien verwenden Sie ConcurrentQueue und ConcurrentStack aus System.Collections.Concurrent.

csharp
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue();  // "first"
string peek = queue.Peek();     // "second"

// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop();   // 2
int peek = stack.Peek(); // 1

Arrays

Arrays haben feste Größe. Array.Sort sortiert in-place. Mehrdimensionale Arrays ([,]) sind rechteckig (gleichmäßige Spalten). Jagged Arrays ([][]) sind Arrays von Arrays (Zeilen können unterschiedliche Längen haben). Verwenden Sie List<T> für dynamische Größen; Arrays für leistungskritische Daten fester Größe.

csharp
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length);  // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);

// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]);  // 3

// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
07

Klassen & OOP

Klasse & Eigenschaften

Auto-Properties ({ get; set; }) generieren automatisch Backing-Fields. Ausdrucksbasierte Properties (=>) berechnen bei Zugriff. Init-only-Properties in C# 9+ ({ get; init; }) sind nur während der Konstruktion setzbar und ermöglichen Unveränderlichkeit. Objekt-Initialisierer: new Person { Name = "X" }.

csharp
class Person {
    // Auto-properties (C# 3+)
    public string Name { get; set; }
    public int Age { get; set; }

    // Read-only property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}

var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult);  // true

Vererbung & virtual

virtual markiert eine Methode zum Überschreiben; override in Unterklassen liefert die neue Implementierung. C# erfordert explizites override (im Gegensatz zu Java). sealed verhindert, dass eine Klasse geerbt oder eine Methode überschrieben wird. Alle Klassen erben implizit von object (System.Object).

csharp
class Animal {
    public virtual void Speak() {
        Console.WriteLine("...");
    }
}

class Dog : Animal {
    public override void Speak() {
        Console.WriteLine("Woof");
    }
}

Animal a = new Dog();
a.Speak();  // Woof (polymorphism)

// sealed prevents further inheritance
sealed class Puppy : Dog { }

Interfaces

Interfaces definieren Verträge ohne Implementierung. Eine Klasse kann mehrere Interfaces implementieren (im Gegensatz zur einfachen Klassenvererbung). C# 8+ erlaubt Standard-Interface-Methoden. Verwenden Sie Interfaces für Polymorphismus und Dependency Injection. Namenskonvention: Präfix mit I (IShape, ILogger).

csharp
interface IShape {
    double Area();  // interface member
    double Perimeter { get; }
}

class Circle : IShape {
    public double Radius { get; set; }
    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter => 2 * Math.PI * Radius;
}

// Default interface methods (C# 8+)
interface ILogger {
    void Log(string msg);
    void Error(string msg) => Log($"ERROR: {msg}");  // default
}

Abstrakte Klassen

Abstrakte Klassen können nicht instanziiert werden und können sowohl abstrakte (müssen überschrieben werden) als auch konkrete (virtual) Member haben. Verwenden Sie abstrakte Klassen bei gemeinsamer Implementierung; Interfaces für reine Verträge. Eine Klasse kann nur eine abstrakte Klasse erben, aber viele Interfaces implementieren.

csharp
abstract class Shape {
    public abstract double Area();  // must be overridden
    public virtual void Describe() {
        Console.WriteLine($"Area: {Area()}");
    }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

// Cannot instantiate abstract class
// var s = new Shape();  // ERROR
var r = new Rectangle { Width = 3, Height = 4 };

Records & Structs

Records (C# 9+) bieten wertbasierte Gleichheit, Unveränderlichkeit und prägnante Syntax – ideal für DTOs und Datenmodelle. Der with-Ausdruck erstellt eine Kopie mit geänderten Properties. Structs sind Werttypen (bei Zuweisung kopiert, auf dem Stack allokiert) – verwenden Sie für kleine, leichte Daten. Klassen sind Referenztypen (auf dem Heap allokiert).

csharp
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // true (value equality)

// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };

// Struct: value type (stack-allocated)
struct Vector {
    public double X, Y;
    public Vector(double x, double y) { X = x; Y = y; }
}
08

LINQ

Where & Select

LINQ (Language Integrated Query) bietet SQL-ähnliche Abfragen auf Collections. Where filtert, Select transformiert (map). LINQ ist verzögert – Abfragen werden erst beim Enumerieren ausgeführt (z. B. via ToList()). Dies ermöglicht effizientes Chaining ohne Zwischen-Collections. Methodensyntax (oben) ist am häufigsten; Abfragesyntax ist ebenfalls verfügbar.

csharp
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };

// Filter
var evens = nums.Where(n => n % 2 == 0);  // 2, 4, 6

// Map/Transform
var squares = nums.Select(n => n * n);  // 1, 4, 9, 16, 25, 36

// Chain operations
var result = nums
    .Where(n => n > 2)
    .Select(n => n * 10)
    .ToList();  // 30, 40, 50, 60

Sortierung & Gruppierung

OrderBy/OrderByDescending sortieren; ThenBy fügt sekundäre Sortierkriterien hinzu. GroupBy gruppiert Elemente nach einem Schlüssel und gibt IGrouping<key, element>-Gruppen zurück. Jede Gruppe hat einen Key und ist selbst ein IEnumerable ihrer Mitglieder. Dies ersetzt manuelle Schleifen und Dictionaries für Gruppierung.

csharp
var people = new[] {
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 30 },
};

// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);

// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
    Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 people

Aggregation

LINQ-Aggregationsmethoden (Sum, Average, Min, Max, Count) berechnen einzelne Werte aus Collections. Aggregate ist das allgemeinste – es ist ein Fold/Reduce, das eine Funktion kumulativ anwendet. Diese werfen bei leeren Sequenzen; verwenden Sie die *OrDefault-Varianten oder prüfen Sie vorher auf Leere.

csharp
int[] nums = { 1, 2, 3, 4, 5 };

Console.WriteLine(nums.Sum());    // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min());    // 1
Console.WriteLine(nums.Max());    // 5
Console.WriteLine(nums.Count());  // 5

// Count with predicate
int evens = nums.Count(n => n % 2 == 0);  // 2

// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b);  // 120
string joined = nums.Aggregate("", (s, n) => s + n);  // "12345"

First, Single & ElementAt

First gibt das erste Element zurück (wirft bei leer); FirstOrDefault gibt default (0 für int, null für Referenztypen) zurück, wenn nicht gefunden. Single erfordert genau ein Element (wirft sonst) – verwenden Sie für Validierung. Any/All geben bool zurück, ohne die gesamte Collection zu enumerieren (Kurzschluss).

csharp
int[] nums = { 1, 2, 3, 4, 5 };

int first = nums.First();           // 1
int firstEven = nums.First(n => n % 2 == 0);  // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10);  // 0 (default)

int single = new[] { 42 }.Single();  // 42
// nums.Single();  // throws! more than one element

int third = nums.ElementAt(2);  // 3 (0-indexed)

bool any = nums.Any(n => n > 3);  // true
bool all = nums.All(n => n > 0);  // true

Join & Zip

Join führt einen Inner Join aus (wie SQL), der Elemente nach Schlüssel verknüpft. Zip paart Elemente aus zwei Sequenzen nach Position. Beide erzeugen neue Sequenzen. LINQ unterstützt auch GroupJoin (Left Join mit Gruppierung). Diese sind mächtig zum Kombinieren verwandter Daten aus mehreren Quellen.

csharp
var users = new[] {
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
};
var orders = new[] {
    new { UserId = 1, Item = "Book" },
    new { Id = 2, Item = "Pen" },
};

// Join (inner join)
var joined = users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Item });
// { Alice, Book }

// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"
09

Fehlerbehandlung

Try / Catch / Finally

Fangen Sie spezifische Exceptions zuerst ab, dann die allgemeine Exception zuletzt (vom spezifischsten zum allgemeinsten). finally läuft immer – verwenden Sie es für Cleanup (Dateien schließen, Ressourcen freigeben). Vermeiden Sie catch (Exception) für Kontrollfluss; fangen Sie nur ab, was Sie behandeln können. Verwenden Sie throw; (nicht throw e;), um den Stack-Trace zu erhalten.

csharp
try {
    int x = 10;
    int y = 0;
    int z = x / y;  // throws DivideByZeroException
} catch (DivideByZeroException e) {
    Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
    Console.WriteLine($"Unexpected: {e.Message}");
} finally {
    // always runs, even with return/throw
    Console.WriteLine("Cleanup");
}

Benutzerdefinierte Exceptions

Leiten Sie benutzerdefinierte Exceptions von Exception (oder einer spezifischeren Basisklasse) ab. Fügen Sie Kontextfelder hinzu, die beim Debuggen helfen. Rufen Sie immer den Basiskonstruktor mit der Nachricht auf. Konventionsgemäß enden Exception-Klassennamen mit Exception. Verwenden Sie benutzerdefinierte Exceptions für domänenspezifische Fehlerbedingungen, die Aufrufer behandeln können.

csharp
class ValidationError : Exception {
    public string Field { get; }
    public ValidationError(string field, string msg)
        : base(msg) {
        Field = field;
    }
}

throw new ValidationError("email", "Invalid format");

try {
    // ...
} catch (ValidationError e) {
    Console.WriteLine($"{e.Field}: {e.Message}");
}

using & IDisposable

using stellt sicher, dass Dispose() aufgerufen wird, selbst wenn eine Exception auftritt – dies ist C#s RAII-Äquivalent für Ressourcenverwaltung. Die using-Deklaration (C# 8+) ist sauberer – Dispose wird am Ende des Scopes aufgerufen. Implementieren Sie IDisposable für Klassen, die nicht verwaltete Ressourcen halten (File-Handles, Datenbankverbindungen).

csharp
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
    string content = file.ReadToEnd();
}  // file.Dispose() called automatically

// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope

// Implementing IDisposable
class Resource : IDisposable {
    public void Dispose() {
        // release unmanaged resources
        GC.SuppressFinalize(this);
    }
}

Null-Behandlung

Nullable Referenztypen in C# 8+ (string?) ermöglichen Kompilierzeit-Null-Sicherheit. ?. gibt null statt NullReferenceException zurück. ?? liefert einen Fallback. Der throw-Ausdruck (?? throw) ist prägnant für Validierung. Aktivieren Sie <Nullable>enable</Nullable> in .csproj für projektweite Null-Prüfung.

csharp
string? name = null;

// Null-conditional operator
int? length = name?.Length;  // null (no exception)

// Null-coalescing operator
string display = name ?? "Unknown";  // "Unknown"

// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));

// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }

Exception-Filter

Exception-Filter (when) fügen catch-Blöcken Bedingungen hinzu – die Exception wird nur gefangen, wenn der Filter true ist. Dies ist mächtiger als Fangen und erneutes Werfen, weil es den Stack-Trace erhält. Das Logging-Pattern (when gibt false zurück) lässt Sie Exceptions beobachten, ohne sie zu behandeln.

csharp
try {
    // ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
    Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
    Console.WriteLine("Server error");
} catch (HttpRequestException e) {
    Console.WriteLine($"HTTP error: {e.Message}");
}

// Log without catching
catch (Exception e) when (LogError(e)) {
    // never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }
10

Datei-I/O & Async

File-Klasse (schnelle I/O)

File bietet statische Methoden für schnelle einmalige Dateioperationen – einfach, aber lädt die gesamte Datei in den Speicher. Für große Dateien verwenden Sie StreamReader/StreamWriter, um zeilenweise zu verarbeiten. File.Exists prüft Existenz, aber es gibt eine TOCTOU-Race – behandeln Sie immer Exceptions von Dateioperationen.

csharp
using System.IO;

// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");

// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");

// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length);  // bytes

Streams (StreamReader/Writer)

StreamReader/StreamWriter verarbeiten Dateien zeilenweise, ohne alles in den Speicher zu laden – verwenden Sie für große Dateien. Die using-Anweisung stellt sicher, dass die Datei geschlossen wird, selbst bei einer Exception. StreamWriter puffert Daten; rufen Sie Flush() auf, um sofort zu schreiben, oder lassen Sie Dispose es erledigen.

csharp
using System.IO;

// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
    Console.WriteLine(line);
}

// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush();  // optional, using disposes and flushes

async / await

async/await ermöglicht nicht-blockierende asynchrone Operationen. Die Methode gibt Task<T> (oder Task für void) zurück. await pausiert die Methode, ohne den Thread zu blockieren – der Thread wird für andere Arbeit freigegeben. Dies ist essenziell für I/O-gebundene Operationen (Dateien, Netzwerk, Datenbank) in UI- und Web-Anwendungen.

csharp
async Task<string> ReadFileAsync(string path) {
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);

// Async doesn't block the calling thread
// The thread is freed while waiting for I/O

Task & Parallel

Task.Run plant Arbeit auf dem Thread-Pool. Task.WhenAll wartet auf mehrere Tasks gleichzeitig (verwenden Sie nicht async/await für CPU-gebundene Arbeit – verwenden Sie Task.Run). Parallel.For ist für CPU-gebundene parallele Schleifen. PLINQ (AsParallel) parallelisiert LINQ-Abfragen. Verwenden Sie async für I/O, Parallel/PLINQ für CPU.

csharp
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;

// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2);  // wait for both

// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
    Process(i);  // runs on multiple threads
});

// PLINQ
var results = nums.AsParallel()
    .Where(n => IsPrime(n))
    .ToArray();

JSON-Serialisierung

System.Text.Json ist der moderne, hochperformante JSON-Serializer (ersetzt Newtonsoft.Json für die meisten Fälle). Serialize/Deserialize behandeln Objekt<->JSON. Verwenden Sie JsonSerializerOptions für Formatierung (indented, camelCase). Für Async verwenden Sie die stream-basierten Methoden, um großes JSON nicht in den Speicher zu laden.

csharp
using System.Text.Json;

var person = new { Name = "Alice", Age = 30 };

// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);

// With options
var options = new JsonSerializerOptions {
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);

// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);
11

LINQ Deep Dive

Abfrage- vs. Methodensyntax

LINQ hat zwei äquivalente Syntaxen. Abfragesyntax ist SQL-ähnlich und besser lesbar für komplexe Joins/Gruppierungen. Methodensyntax (fluent) ist häufiger, unterstützt alle Operatoren und verkettet natürlich. Beide kompilieren zum gleichen IL. Verwenden Sie Abfragesyntax für komplexe Mehr-Klausel-Abfragen; Methodensyntax für einfache Ketten. Beide sind verzögert ausgewertet (deferred execution).

csharp
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * 2;

// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
                  .OrderByDescending(n => n)
                  .Select(n => n * 2);

// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus more

Verzögerte vs. sofortige Ausführung

LINQ verwendet verzögerte Ausführung – Where/Select/OrderBy bauen nur eine Abfrage; sie läuft beim Enumerieren. Das bedeutet, dass Ergebnisse den Zustand der Quelle zum Enumerationszeitpunkt widerspiegeln, nicht zum Abfrageerstellungszeitpunkt. ToList/ToArray/Count/usw. erzwingen sofortige Ausführung und erfassen einen Snapshot. Vorsicht: zweimaliges Enumerieren einer verzögerten Abfrage führt sie zweimal aus. Cachen Sie mit ToList, wenn Sie stabile Ergebnisse benötigen.

csharp
var nums = new List<int> { 1, 2, 3 };

// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4);  // query sees this!
Console.WriteLine(query.Count());  // 3 (2, 3, 4)

// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList();  // runs now
nums.Add(5);
Console.WriteLine(list.Count);  // still 3 (snapshot)

// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, All

Gruppierung und Joining

GroupBy gruppiert Elemente nach einem Schlüssel und gibt IGrouping<key, element>-Sequenzen zurück. Join führt Inner Joins aus (Schlüssel abgleichen). Group Join (join...into) erstellt hierarchische Ergebnisse und ermöglicht Left Outer Joins via DefaultIfEmpty(). Diese Operationen sind mächtig für Datenanalyse. Für große Datensätze ziehen Sie ToLookup für wiederholte Lookups in Betracht (es ist eine vorberechnete Gruppierung).

csharp
var students = new[] {
    new { Name = "Alice", Grade = "A", Dept = "CS" },
    new { Name = "Bob", Grade = "B", Dept = "Math" },
    new { Name = "Carol", Grade = "A", Dept = "CS" },
};

// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
    Console.WriteLine(`{g.Key}: {g.Count()}`);
    // CS: 2, Math: 1
}

// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
             join c in courses on s.Dept equals c.Dept
             select new { s.Name, c.Course };

// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
                join c in courses on s.Dept equals c.Dept into sc
                from c in sc.DefaultIfEmpty()
                select new { s.Name, Course = c?.Course };

Aggregation und Quantoren

LINQ bietet Standard-Aggregationen (Sum, Min, Max, Average, Count) und ein generisches Aggregate für benutzerdefinierte Reduktionen (wie JavaScript reduce). Quantoren (Any, All, Contains) geben Booleans zurück und machen Kurzschluss – Any stoppt beim ersten Match, All stoppt beim ersten Nicht-Match. Verwenden Sie Any() (nicht Count() > 0), um Existenz zu prüfen – es ist effizienter und lesbarer.

csharp
var nums = new[] { 1, 2, 3, 4, 5 };

// Aggregations
int sum = nums.Sum();              // 15
double avg = nums.Average();       // 3.0
int min = nums.Min();              // 1
int max = nums.Max();              // 5
int count = nums.Count();          // 5

// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b);  // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b);  // 25

// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0);  // true
bool allPos = nums.All(n => n > 0);        // true
bool contains3 = nums.Contains(3);         // true

// LongCount for > int.MaxValue elements
long big = largeList.LongCount();

IEnumerable und IQueryable

IEnumerable<T> ist für In-Memory-Collections (LINQ to Objects) – verwendet Delegates, führt lokal aus. IQueryable<T> ist für entfernte Quellen (Entity Framework, LINQ to SQL) – verwendet Ausdrucksbäume, übersetzt in die Abfragesprache der Quelle (SQL). IQueryable komponiert serverseitige Abfragen effizient. Wechseln Sie mit AsEnumerable() zu IEnumerable, wenn Sie clientseitige Logik benötigen, die nicht übersetzt werden kann.

csharp
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1);  // executes in memory

// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18);  // generates SQL
// SELECT * FROM Users WHERE Age > 18

// IQueryable composes queries that execute server-side
var query = db.Users
    .Where(u => u.Active)
    .OrderBy(u => u.Name)
    .Take(10);
// All translated to one SQL query

// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
    .Where(u => ExpensiveCheck(u));
12

Async/Await Deep Dive

Task und Task<T>

Task repräsentiert eine Async-Operation ohne Ergebnis; Task<T> gibt T zurück. ValueTask<T> (C# 7) vermeidet Heap-Allokation, wenn das Ergebnis oft synchron verfügbar ist (Caching-Szenarien) – kann aber nur einmal awaited werden. Verwenden Sie Task.FromResult für synchrone Ergebnisse in Async-APIs. Bevorzugen Sie Task über void für Async-Methoden (void ist nur für Event-Handler).

csharp
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
    await Task.Delay(500);
    return 42;
}

// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
    if (_cache != null) return new ValueTask<int>(_cache);
    return new ValueTask<int>(LoadFromDbAsync());
}

// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42);  // already complete
Task completed = Task.CompletedTask;

CancellationToken

CancellationToken ermöglicht kooperative Abbruch. Übergeben Sie es an Async-Methoden und prüfen Sie ThrowIfCancellationRequested() in Schleifen. Der Aufrufer bricht via CancellationTokenSource.Cancel() ab; die Methode entscheidet, wann/wie sie reagiert. Akzeptieren Sie immer einen CancellationToken-Parameter in Async-APIs (besonders Bibliothekscode). Verwenden Sie CancellationTokenSource mit einer TimeSpan für Timeouts. Abbruch wirft OperationCanceledException.

csharp
async Task DownloadAsync(string url, CancellationToken ct) {
    using var client = new HttpClient();
    // Pass token to async operations
    var data = await client.GetByteArrayAsync(url, ct);
    // Check manually in loops
    foreach (var chunk in data) {
        ct.ThrowIfCancellationRequested();
        Process(chunk);
    }
}

// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timed out or cancelled");
}

// Cooperative cancellation: caller cancels, method checks
cts.Cancel();  // request cancellation

Task.WhenAll vs. WhenAny

Task.WhenAll wartet auf mehrere Tasks gleichzeitig und fährt fort, wenn alle fertig sind (Parallelität). Task.WhenAny fährt fort, wenn der erste Task fertig ist (Rennen/Redundanz). WhenAll wirft standardmäßig nur die erste Exception; inspizieren Sie jeden Task's IsFaulted/Exception, um alle Fehler zu sehen. Für Fire-and-Forget verwenden Sie Task.Run und behandeln Sie Exceptions intern, um unbeobachtete Exceptions zu vermeiden.

csharp
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
    FetchAsync("url1"),
    FetchAsync("url2"),
    FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails

// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;

// WhenAll with exception handling
try {
    await Task.WhenAll(tasks);
} catch (Exception ex) {
    // Only first exception is thrown; access all via Task.Exception
    foreach (var inner in tasks.Where(t => t.IsFaulted))
        Console.WriteLine(inner.Exception);
}

Async-Streams (IAsyncEnumerable)

IAsyncEnumerable<T> (C# 8) ist ein Async-Stream – liefert Items, sobald sie verfügbar sind, mit awaits zwischen Yields. Konsumiert mit await foreach. Perfekt für Streaming großer Datensätze oder Echtzeitdaten, ohne alles im Speicher zu puffern. Das [EnumeratorCancellation]-Attribut stellt sicher, dass das Abbruch-Token korrekt fließt, wenn der Stream mit WithCancellation konsumiert wird.

csharp
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;  // async yield
    }
}

// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
    Console.WriteLine(n);
}

// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
        yield return MapRecord(reader);
}

Async-Fallen (Deadlocks)

Die #1 Async-Falle: .Result oder .Wait() auf einem Task aufzurufen kann deadlocken, wenn ein SynchronizationContext vorhanden ist (UI-Apps, Legacy-ASP.NET). await erfasst den Kontext; .Result blockiert den Thread, den die Fortsetzung benötigt. Fix: Machen Sie Methoden durchgehend async oder verwenden Sie ConfigureAwait(false) in Bibliothekscode. async void ist gefährlich – Exceptions können nicht gefangen werden und es ist nicht awaitbar. Verwenden Sie stattdessen async Task.

csharp
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
    string data = GetDataAsync().Result;  // DEADLOCK risk!
    return data;
}

// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
    string data = await GetDataAsync();
    return data;
}

// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.

// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result;  // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);

// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!
13

Events & Delegates

Delegate-Grundlagen

Delegates sind typsichere Funktionszeiger. Benutzerdefinierte Delegates (delegate int MathOp(int,int)) sind weitgehend durch eingebaute Action (void-Rückgabe) und Func<T> (gibt T zurück) ersetzt. Delegates sind Multicast – sie können mehrere Methoden halten (mit + kombiniert). Beim Aufruf laufen alle Methoden. Der Rückgabewert eines Multicast-Delegates ist das Ergebnis der letzten Methode. Verwenden Sie Action/Func für meisten modernen Code.

csharp
// Declare a delegate type
public delegate int MathOp(int a, int b);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(2, 3));  // 5
op = Mul;
Console.WriteLine(op(2, 3));  // 6

// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)

// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;  // returns bool

Events (Publisher/Subscriber)

Events sind eine eingeschränkte Form von Delegates – nur die deklarierende Klasse kann sie aufrufen, aber externer Code kann abonnieren/stornieren (+=/-=). Verwenden Sie EventHandler<TArgs> für das Standard-Pattern. Verwenden Sie immer den null-bedingten Operator (Click?.Invoke), da ein Event ohne Abonnenten null ist. Melden Sie immer ab, um Speicherlecks zu vermeiden (das Event hält eine starke Referenz auf den Handler).

csharp
public class Button {
    // Event: restricted multicast delegate
    public event EventHandler<EventArgs> Click;

    public void SimulateClick() {
        // Null-conditional invoke pattern (thread-safe)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Custom event args
public class SearchEventArgs : EventArgs {
    public string Query { get; init; }
}

// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod;  // method group

// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;

void HandlerMethod(object sender, EventArgs e) { /* ... */ }

INotifyPropertyChanged (Datenbindung)

INotifyPropertyChanged ist das Standard-Pattern für Datenbindung in WPF, MAUI und WinForms. Wenn sich eine Property ändert, lösen Sie PropertyChanged aus, damit die gebundene UI sich aktualisiert. [CallerMemberName] (C# 5) übergibt automatisch den Namen der aufrufenden Property – keine Magic Strings. Die Prüfung (if != value) verhindert unnötige Benachrichtigungen. ViewModels verwenden dies intensiv; Source Generators (CommunityToolkit.Mvvm) können diesen Boilerplate automatisch generieren.

csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged {
    private string _name;
    public string Name {
        get => _name;
        set {
            if (_name != value) {
                _name = value;
                OnPropertyChanged();  // raises event
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName auto-fills the property name
    protected void OnPropertyChanged([CallerMemberName] string prop = null) {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

// WPF/MAUI bindings automatically update UI when Name changes

Event vs. Delegate

Der Hauptunterschied: Ein öffentliches Delegate-Feld kann von jedem aufgerufen, neu zugewiesen oder gelöscht werden. Ein Event beschränkt externen Zugriff auf nur += (abonnieren) und -= (stornieren) – nur die deklarierende Klasse kann es aufrufen oder löschen. Diese Kapselung ist der Grund, warum Events der Standard für das Observer-Pattern sind. Events generieren auch thread-sichere add/remove-Accessoren und integrieren mit Designer-Tooling.

csharp
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
    public Notify OnDone;  // field-like, public
}
// External code can do:
//   bad.OnDone();        // invoke (BAD: not the owner)
//   bad.OnDone = null;   // clear all subscribers (BAD)

// Event: only owner can invoke/clear
public class Good {
    public event Notify OnDone;  // restricted
    public void Complete() {
        OnDone?.Invoke();  // OK: owner invokes
    }
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside

// Event = delegate + access restriction
// Under the hood: event generates add/remove accessors

Weak Events (Speicherleck-Vermeidung)

Events halten starke Referenzen auf Abonnenten und verursachen Speicherlecks, wenn der Abonnent garbage-collected werden sollte, der Publisher aber weiterlebt. Lösungen: explizit abmelden (am besten), WeakEventManager (WPF) verwenden oder ein Weak-Event-Pattern mit WeakReference implementieren. Dies ist eine häufige Quelle von Lecks in langlebigen Apps (Server, Desktop-Apps). Melden Sie immer in Dispose ab oder wenn der Lebenszyklus des Abonnenten endet.

csharp
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long

// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);

// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
    private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
    public void Subscribe(EventHandler<TArgs> h) =>
        _handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
    public void Raise(object sender, TArgs args) {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        foreach (var wr in _handlers) {
            if (wr.TryGetTarget(out var h)) h(sender, args);
        }
    }
}

// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishers
14

Reflection

Typinformationen

Reflection inspiziert Typ-Metadaten zur Laufzeit. typeof(T) erhält einen Type zur Kompilierzeit; obj.GetType() zur Laufzeit. Type bietet Name, IsClass, IsValueType und Methoden zum Enumerieren von Membern (GetMethods, GetProperties, GetFields). BindingFlags steuern, was zurückgegeben wird (NonPublic, Instance, Static, Public). Reflection ist mächtig, aber langsam – cachen Sie Ergebnisse, wenn möglich.

csharp
using System;

Type t = typeof(string);
// Or: Type t = "hello".GetType();

Console.WriteLine(t.Name);          // String
Console.WriteLine(t.FullName);      // System.String
Console.WriteLine(t.IsClass);       // true
Console.WriteLine(t.IsValueType);   // false
Console.WriteLine(t.IsAbstract);    // false

// Get methods
foreach (var m in t.GetMethods())
    Console.WriteLine(m.Name);

// Get properties
var props = t.GetProperties();

// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t));  // false

Instanziierung und Aufruf

Activator.CreateInstance erstellt Objekte dynamisch. MethodInfo.Invoke ruft Methoden via Reflection auf (langsam aufgrund von Argument-Boxing und Sicherheitsprüfungen). Für wiederholte Aufrufe ist CreateDelegate viel schneller – erstellt einen stark typisierten Delegate, der Reflection-Overhead umgeht. Reflection bricht Kompilierzeit-Typsicherheit und ist langsamer als direkte Aufrufe; verwenden Sie es für Frameworks (Serialisierung, DI, ORMs) und Caching-Szenarien.

csharp
using System.Reflection;

// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")

// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });

// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);

// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);

// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
    method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));

Attribute

Attribute hängen Metadaten an Code-Elemente (Klassen, Methoden, Properties). Definieren Sie mit [AttributeUsage(...)] unter Angabe gültiger Ziele. Abruf via GetCustomAttributes<T>() (generisch, modern) oder Attribute.GetCustomAttribute. Eingebaute Attribute: [Obsolete], [Serializable], [DllImport], [Conditional]. Frameworks verwenden Attribute extensiv: [HttpGet] (ASP.NET), [JsonProperty] (JSON), [Table] (EF Core).

csharp
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
    [Author("Charlie")]
    public void DoWork() { }
}

// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name);  // Alice, Bob

// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));

Emit und dynamische Code-Generierung

Reflection.Emit generiert IL zur Laufzeit für maximale Leistung in dynamischen Szenarien (Ausdruckskompilierung, Serializer, Mock-Frameworks). Sie bauen Typen/Methoden und emitieren IL-Opcodes direkt. Dies ist fortgeschritten – verwenden Sie Ausdrucksbäume (Expression.Compile) für einfacheren dynamischen Code. Emit wird von DLR (dynamische Sprachen), ORMs und Serialisierungsbibliotheken verwendet, um schnellen typisierten Code zur Laufzeit zu generieren.

csharp
using System.Reflection;
using System.Reflection.Emit;

// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);

// Add a method
var method = type.DefineMethod("Add",
    MethodAttributes.Public | MethodAttributes.Static,
    typeof(int), new[] { typeof(int), typeof(int) });

// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);  // load first arg
il.Emit(OpCodes.Ldarg_1);  // load second arg
il.Emit(OpCodes.Add);      // add
il.Emit(OpCodes.Ret);      // return

Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 });  // 7

Ausdrucksbäume

Ausdrucksbäume repräsentieren Code als Daten (ein Baum aus Expression-Nodes). Sie sind die Grundlage von IQueryable (LINQ-Provider übersetzen sie in SQL usw.). Compile() macht einen Ausdruck zu einem ausführbaren Delegate. Sie können Ausdrücke manuell für dynamische Code-Generierung erstellen (lesbarer als Reflection.Emit). Ausdrucksbäume ermöglichen ORMs, Rule-Engines und dynamisches LINQ.

csharp
using System.Linq.Expressions;

// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;

// Inspect the tree
Console.WriteLine(expr.Body.NodeType);  // Add
Console.WriteLine(expr.Body.Type);      // int

// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3);  // 6

// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18

// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();
15

Erweiterungsmethoden & Patterns

Erweiterungsmethoden

Erweiterungsmethoden lassen Sie Methoden zu vorhandenen Typen 'hinzufügen', ohne sie zu ändern. Definiert in einer statischen Klasse mit 'this' auf dem ersten Parameter. Sie sind syntaktischer Zucker – der Compiler schreibt email.IsEmail() zu StringExtensions.IsEmail(email) um. LINQ besteht vollständig aus Erweiterungsmethoden auf IEnumerable. Verwenden Sie Erweiterungen, um Hilfsmethoden zu versiegelten Klassen, Interfaces oder Drittanbieter-Typen hinzuzufügen. Bringen Sie sie mit using in den Scope.

csharp
public static class StringExtensions {
    // Must be static, in static class, first param with 'this'
    public static bool IsEmail(this string s) =>
        s.Contains('@') && s.Contains('.');

    public static string Repeat(this string s, int n) =>
        string.Concat(Enumerable.Repeat(s, n));

    // Chainable
    public static string Truncate(this string s, int max) =>
        s.Length <= max ? s : s[..max];
}

// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3);  // "ababab"
string short_ = "hello world".Truncate(5);  // "hello"

// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0);  // extension

Fluent Interfaces

Fluent Interfaces geben 'this' zurück, um Method-Chaining zu ermöglichen, und erzeugen lesbaren Code, der wie ein Satz liest. Häufig in Buildern (QueryBuilder), Konfiguration (ASP.NET, EF) und DSLs. Erweiterungsmethoden können primitive Typen um Fluent-Methoden erweitern (5.Seconds()). Halten Sie Methoden rein (neue Instanz zurückgeben) für unveränderliche Fluent-APIs, oder mutieren und this zurückgeben für Builder.

csharp
public class QueryBuilder {
    private string _table = "";
    private string _where = "";
    private int _limit;

    public QueryBuilder From(string table) { _table = table; return this; }
    public QueryBuilder Where(string clause) { _where = clause; return this; }
    public QueryBuilder Limit(int n) { _limit = n; return this; }

    public string Build() =>
        $"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}

// Fluent chaining
var sql = new QueryBuilder()
    .From("users")
    .Where("age > 18")
    .Limit(10)
    .Build();

// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
    public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
    public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds();  // TimeSpan
var timeout = 2.Minutes();

Iterator-Methoden (yield)

yield return macht eine Methode zu einem Iterator – der Compiler generiert eine Zustandsmaschine. Ausführung ist verzögert: Code läuft nur, wenn Items abgerufen werden. Dies ermöglicht unendliche Sequenzen und effizientes Streaming (ein Item nach dem anderen verarbeiten, ohne zu puffern). yield break beendet die Iteration. Die Methode muss IEnumerable<T> oder IEnumerator<T> zurückgeben. LINQ-Operatoren basieren auf yield. Erneutes Enumerieren führt die Methode erneut aus.

csharp
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
    for (int i = 0; i <= max; i += 2)
        yield return i;
}

// Lazy: nothing runs until enumerated
var evens = Evens(100);  // no execution yet
foreach (var e in evens) { /* runs here */ }

// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (!pred(item)) yield break;
        yield return item;
    }
}

// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
var first10 = Naturals().Take(10);  // 0..9

Dependency Injection

Dependency Injection (DI) übergibt Abhängigkeiten via Konstruktoren statt sie intern zu erstellen. Vorteile: Testbarkeit (Implementierungen in Tests austauschen), lose Kopplung, Single Responsibility. .NETs eingebauter DI-Container (Microsoft.Extensions.DependencyInjection) behandelt Registrierung und Lebensdauer (AddTransient, AddScoped, AddSingleton). Hängen Sie immer an Interfaces ab, nicht an konkrete Klassen, für Flexibilität.

csharp
// Interface for the dependency
public interface IEmailSender {
    Task SendAsync(string to, string body);
}

// Implementation
public class SmtpEmailSender : IEmailSender {
    public async Task SendAsync(string to, string body) { /* SMTP */ }
}

// Consumer: depends on abstraction, not concretion
public class UserService {
    private readonly IEmailSender _email;
    public UserService(IEmailSender email) => _email = email;  // inject

    public async Task RegisterAsync(string email) {
        // ... save user ...
        await _email.SendAsync(email, "Welcome!");
    }
}

// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();

// Resolve
var userService = app.Services.GetRequiredService<UserService>();

Factory- und Builder-Pattern

Das Factory-Pattern zentralisiert Objekterstellung und verbirgt konkrete Klassen vor Aufrufern (nützlich, wenn Erstellung komplex ist oder der Typ zur Laufzeit gewählt wird). Das Builder-Pattern konstruiert komplexe Objekte Schritt für Schritt mit Fluent-Methoden und vermeidet 'teleskopierende Konstruktoren' (viele Parameter). Beide verbessern Lesbarkeit und Wartbarkeit. C#-Records mit 'with'-Ausdrücken ersetzen oft Builder für unveränderliche Daten.

csharp
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }

public static class ShapeFactory {
    public static IShape Create(string type) => type switch {
        "circle" => new Circle(),
        "square" => new Square(),
        _ => throw new ArgumentException()
    };
}

// Builder: step-by-step construction of complex objects
public class Pizza {
    public string Size { get; set; }
    public bool Cheese { get; set; }
    public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
    private readonly Pizza _p = new();
    public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
    public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
    public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
    public Pizza Build() => _p;
}

var pizza = new PizzaBuilder()
    .WithSize("large").AddCheese().AddPepperoni().Build();
16

Pattern Matching (C# 7-10+)

Typ-Pattern und is

Pattern Matching (C# 7+) kombiniert Typ-Prüfung und Variablenbindung in einem Schritt: 'o is string s' prüft und weist zu. Eigenschafts-Pattern ({ Length: > 3 }) matchen Objekt-Properties. C# 9 fügt 'not', 'and', 'or'-Kombinatoren hinzu. Switch-Ausdrücke (C# 8) geben Werte zurück und verwenden Patterns – viel sauberer als if-else-Ketten. Das _ ist das Discard-Pattern (Standardfall).

csharp
object o = "hello";

// Type pattern (C# 7)
if (o is string s) {
    Console.WriteLine(s.Length);  // s is string here
}

// Combined with null check
if (o is string { Length: > 3 } longStr) {
    Console.WriteLine(longStr);  // string with length > 3
}

// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }

// Switch on type
string Describe(object o) => o switch {
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    double d => $"Double: {d}",
    null => "null",
    _ => "Unknown"
};

Eigenschafts- und positionelle Patterns

Eigenschafts-Pattern matchen Objekt-Properties: { Prop: pattern }. Positionelle Patterns dekonstruieren via Deconstruct-Methode (Records haben dies automatisch). Patterns komponieren: Sie können sie schachteln und mit relationalen Operatoren (<, > usw.) kombinieren. C# 10 erlaubt verschachtelte Eigenschafts-Kurzschreibweise (Name.Length statt Name: { Length: }). Pattern Matching macht komplexe bedingte Logik deklarativ und lesbar.

csharp
public record Point(int X, int Y);
public record Person(string Name, int Age);

// Property pattern
string Describe(Person p) => p switch {
    { Age: < 18 } => "Minor",
    { Age: >= 65 } => "Senior",
    { Name.Length: > 10 } => "Long name",
    { } => "Adult"  // non-null Person
};

// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
    (0, 0) => "Origin",
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "On axis"
};

// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };

Relationale und logische Patterns (C# 9)

C# 9 führte relationale Patterns (<, >, <=, >=) und logische Kombinatoren (and, or, not) für Patterns ein. Diese machen Switch-Ausdrücke ausdrucksstark für Bereichsprüfungen und Kategorie-Logik. 'or' kombiniert Patterns (matcht, wenn eines zutrifft); 'and' erfordert beide; 'not' negiert. Klammern gruppieren für Vorrang. Dies ersetzt ausführliche if-else-Ketten durch deklarative, erschöpfende (compilergeprüfte) Logik.

csharp
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium",
    >= 100 => "Large"
};

// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
    Student => 0.2m,
    Senior or Veteran => 0.15m,
    not null => 0.0m,
    null => throw new ArgumentNullException()
};

// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);

Listen-Patterns (C# 11)

Listen-Patterns (C# 11) matchen die Form von Arrays und Listen: leer [], einzelnes Element [x], genau [a, b] oder mit Slices [first, .., last]. Das Slice-Pattern (..) erfasst null oder mehr mittlere Elemente in einer Variablen. Kombiniert mit var und Guards (when) ermöglicht dies ausdrucksstarkes Matching auf Sequenzen. Nützlich für Parsing, Validierung und Algorithmus-Implementierung.

csharp
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch {
    [] => "Empty",
    [single] => $"Single: {single}",
    [first, second] => $"Pair: {first}, {second}",
    [first, .., last] => $"First: {first}, Last: {last}",  // slice
    [.., var last] => $"Last: {last}",
    _ => "Other"
};

// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
    Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}

// With guards
string Classify(int[] arr) => arr switch {
    [var a, var b] when a == b => "Equal pair",
    [var a, var b] => "Unequal pair",
    _ => "Other"
};

Switch-Ausdrücke

Switch-Ausdrücke (C# 8) sind Ausdrücke, die Werte zurückgeben, im Gegensatz zu Switch-Anweisungen. Sie verwenden Pattern Matching (=>) und sind prägnanter. Tuple-Switching ((a, b) switch) behandelt mehrere Werte. Der Compiler prüft Erschöpfbarkeit für Enums (warnt, wenn ein Fall fehlt). Schließen Sie immer einen Default (_) ein, außer Sie wollen eine Laufzeit-Exception. Switch-Ausdrücke werden Switch-Anweisungen für wertzurückgebende Logik vorgezogen.

csharp
// Traditional switch statement (verbose)
string Old(int n) {
    switch (n) {
        case 1: return "one";
        case 2: return "two";
        default: return "many";
    }
}

// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
    1 => "one",
    2 => "two",
    _ => "many"
};

// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
    (6 or 7, _) => "Weekend",
    (_, true) => "Holiday",
    (1, false) => "Monday",
    _ => "Weekday"
};

// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)
17

Records & Unveränderlichkeit

Record-Grundlagen (C# 9)

Records (C# 9) sind Referenztypen mit wertbasierter Gleichheit – zwei Records sind gleich, wenn ihre Daten gleich sind (im Gegensatz zu Klassen, die Referenzgleichheit verwenden). Der Compiler generiert Equals, GetHashCode, ToString und Deconstruct automatisch. Der 'with'-Ausdruck erstellt eine Kopie mit geänderten Properties (nicht-zerstörende Mutation). Records sind ideal für DTOs, Value Objects und unveränderliche Datenmodelle.

csharp
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);

// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

// Value equality (auto-generated)
Console.WriteLine(p1 == p2);  // true (compares properties)
Console.WriteLine(p1.Equals(p2));  // true

// ToString (auto-generated, readable)
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

// Deconstruction
var (name, age) = p1;

// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31

Record Structs (C# 10)

C# 10 fügte Record Structs (Werttypen mit Record-Features) und readonly Record Structs (unveränderliche Werttypen) hinzu. Wahl: Record Class (Referenztyp, für größere/geteilte Daten), Record Struct (Werttyp, für kleine Daten, vermeidet Heap-Allokation), readonly Record Struct (unveränderlicher Werttyp, am sichersten). Record Structs haben Wertgleichheit und 'with'-Support wie Record Classes. Verwenden Sie readonly Record Struct für kleine unveränderliche Werte wie Point, Money.

csharp
// record class (default, reference type)
public record PersonRef(string Name, int Age);

// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);

// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);

// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
    public int Value { get; set; } = Value;
}

var p = new Point(1, 2);
var p2 = p with { X = 5 };  // copy with X=5
Console.WriteLine(p == p2);  // false

// Records can have additional members
public record Person(string Name) {
    public int Age { get; init; }  // extra init-only prop
    public string DisplayName => Name.ToUpper();
}

Init-only-Setter

Init-only-Setter (C# 9) erlauben Setzen während der Objektinitialisierung, aber nicht danach – Unveränderlichkeit nach Konstruktion ohne Konstruktor-Boilerplate. 'required' (C# 11) zwingt den Aufrufer, eine Property im Initialisierer zu setzen (Kompilierzeit-Prüfung). Records verwenden standardmäßig Init-Setter. Dies ermöglicht unveränderliche Objekt-Pattern mit Objekt-Initialisierer-Syntax, die lesbarer ist als Konstruktorparameter.

csharp
public class Config {
    // init: settable only during construction (object initializer)
    public string Name { get; init; } = "";
    public int Timeout { get; init; } = 30;
}

var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other";  // ERROR: init-only after construction

// Required properties (C# 11): must be set in initializer
public class User {
    public required string Email { get; init; }
    public string? DisplayName { get; init; }
}

// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };

// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
//                public int Y { get; init; }

Record-Vererbung

Records unterstützen Vererbung: Ein abgeleiteter Record inkludiert Basis-Record-Parameter in seinem Konstruktor. Der 'with'-Ausdruck erhält den Laufzeittyp (erstellt einen Dog aus einem Dog, nicht einem Animal). Gleichheit prüft den Laufzeittyp – zwei Records sind nur gleich, wenn sie denselben Typ mit gleichen Daten haben. Dadurch funktionieren Records korrekt in polymorphen Collections, im Gegensatz zu naiver Wertgleichheit.

csharp
// Base record
public record Animal(string Name);

// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);

// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);

var d = new Dog("Rex", "Labrador");
Console.WriteLine(d);  // Dog { Name = Rex, Breed = Labrador }

// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name);  // Dog (not Animal)

// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b);  // true

// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c);  // false (different runtime types)

Records vs. Klassen vs. Structs

Klassen verwenden Referenzgleichheit (== vergleicht Referenzen); Records verwenden Wertgleichheit (vergleicht Daten); Structs verwenden standardmäßig Wertgleichheit, sind aber Werttypen (bei Zuweisung kopiert). Verwenden Sie Records für unveränderliche Daten mit Wertsemantik (DTOs, Value Objects, Messages). Verwenden Sie Klassen für veränderliche Entitäten mit Identität (User, Order) und Vererbungshierarchien. Verwenden Sie Structs für kleine (≤16 Bytes) Werte, die kopiert werden sollen (Point, Color, Money).

csharp
// Class: reference type, reference equality
public class PersonClass {
    public string Name { get; set; }
    public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2);  // false (different references)

// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2);  // true (value equality)

// Struct: value type, value equality (manual)
public struct PointStruct {
    public int X, Y;
    public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2));  // true (default struct equality)

// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)
18

Pattern Matching Deep

Eigenschafts-Pattern

Eigenschafts-Pattern matchen inline gegen Objekt-Properties. Der Switch-Ausdruck gibt direkt einen Wert zurück. Kombinierte Patterns (and/or) erstellen flexible Bedingungen. null und _ (Discard) behandeln Edge Cases. Dies ersetzt ausführliche if-else-Ketten durch deklarativen, lesbaren Code.

csharp
static decimal CalcDiscount(Customer c) => c switch
{
    { IsPremium: true, Orders.Count: > 100 } => 0.20m,
    { IsPremium: true } => 0.10m,
    { Orders.Count: > 50 } => 0.05m,
    { Country: "US" or "CA" } => 0.03m,
    null => 0m,
    _ => 0m
};

Listen-Patterns

Listen-Patterns (C# 11) matchen Arrays und indizierbare Sequenzen. [] matcht leer, [single] matcht ein Element, [first, .. rest] verwendet Slice-Pattern, um verbleibende Elemente zu erfassen. Nützlich für Befehle-Parsing, Sequenz-Validierung und rekursive Algorithmen.

csharp
int Sum(int[] n) => n switch
{
    [] => 0,
    [int single] => single,
    [int first, int second] => first + second,
    [int first, .. int[] rest] => first + Sum(rest),
};

// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4

Tuple-Pattern

Tuple-Pattern matchen mehrere Werte gleichzeitig. Jeder Fall testet ein Tuple von Werten. Kombiniert mit relationalen Patterns (>, <, >=) und logischen Patterns (and, or, not) erstellt dies mächtiges mehrdimensionales Matching ohne verschachtelte if-Anweisungen.

csharp
static string Classify(int t, bool rain) => (t, rain) switch
{
    (< 0, _) => "Freezing",
    (>= 0 and < 15, true) => "Cold and wet",
    (>= 0 and < 15, false) => "Cold",
    (>= 15 and < 25, _) => "Mild",
    (>= 25, true) => "Hot and wet",
    (>= 25, false) => "Hot",
};

Typ-Pattern & Guards

Typ-Pattern matchen Laufzeittypen. Die when-Klausel fügt zusätzliche Guards hinzu. var matcht alles (inklusive null). Reihenfolge ist wichtig: spezifischere Patterns müssen vor allgemeinen kommen. Dies ist der idiomatische Weg für typbasiertes Dispatching in modernem C#.

csharp
object Process(object input) => input switch
{
    int i when i > 0 => $"Positive: {i}",
    int i => $"Non-positive: {i}",
    string s when s.Length > 10 => s.Substring(0, 10),
    string s => s,
    IList list => $"List with {list.Count} items",
    var x => x?.ToString() ?? "null"
};

Verschachtelte Patterns

Patterns schachteln beliebig tief. var innerhalb eines Patterns erfasst den gematchten Wert für Guards oder den Ausdrucks-Body. Dieses Beispiel matcht Point-Properties innerhalb von Line-Records. Verschachtelte Patterns sind mächtig, um komplexe Objektgraphen prägnant zu validieren.

csharp
record Point(int X, int Y);
record Line(Point Start, Point End);

string Describe(Line line) => line switch
{
    Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
    Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
        when x1 == x2 && y1 == y2 => "Degenerate line",
    Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
    Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
    _ => "Diagonal line"
};
19

Records & With-Ausdrücke

Record-Wertgleichheit

Records bieten standardmäßig wertbasierte Gleichheit: Zwei Instanzen mit denselben Daten sind gleich. Der Compiler generiert Equals, GetHashCode, ToString und den ==-Operator. Records sind Referenztypen, aber für Unveränderlichkeit konzipiert. Verwenden Sie sie für DTOs, Value Objects und Daten, die nach Inhalt vergleichen sollen.

csharp
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

With-Ausdrücke

Der with-Ausdruck erstellt eine Kopie eines Records mit geänderten Properties. Das Original bleibt unverändert (nicht-zerstörende Mutation). Dies ist der idiomatische Weg, unveränderliche Daten zu aktualisieren. Unter der Haube verwendet der Compiler einen geschützten Kopierkonstruktor und Init-only-Setter.

csharp
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };

// Original is unchanged (immutable)
Console.WriteLine(alice.Age);      // 30
Console.WriteLine(alice31.Age);    // 31
Console.WriteLine(aliceMarried.Name); // Alice Smith

Positionell vs. Init-only

Positionelle Records verwenden Primary-Constructor-Syntax und generieren Deconstruct-Methoden. Init-only-Records verwenden Objekt-Initialisierer-Syntax und erlauben Standardwerte und required-Modifier. Wählen Sie positionell für einfache Werttypen; Init-only für komplexe Records mit optionalen oder berechneten Properties.

csharp
// Positional record (primary constructor)
public record Point(int X, int Y);

// Init-only record (more flexible properties)
public record Employee
{
    public required string Name { get; init; }
    public decimal Salary { get; init; }
    public string Dept { get; init; } = "General";
}

var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };

Record Structs & Vererbung

Record Structs sind Werttypen (bei Zuweisung kopiert) und können nicht von anderen Record Structs erben. Referenztyp-Records unterstützen Vererbung. readonly Record Struct verhindert Mutation. Verwenden Sie Record Struct für kleine unveränderliche Werte; Record Class für größere Objekte, die von Referenzsemantik und Vererbung profitieren.

csharp
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);

// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal);  // True
var copied = dog with { Breed = "Poodle" };

Deconstruct & Matching

Positionelle Records generieren automatisch eine Deconstruct-Methode, die Tuple-Dekonstruktion ermöglicht. Dies integriert sich nahtlos mit Pattern Matching: Sie können Record-Properties positionell oder nach Namen matchen. Dekonstruktion funktioniert in var-Pattern, Tuple-Pattern und Switch-Ausdrücken.

csharp
var person = new Person("Alice", 30);
var (name, age) = person;  // Deconstruct
Console.WriteLine($"{name} is {age}");

// Works with pattern matching
string Greet(Person p) => p switch
{
    ("Alice", _) => "Hi Alice!",
    (_, < 18) => "Hello young one",
    var (n, a) => $"Hello {n}, age {a}"
};
20

Source Generators

Basis-Source-Generator

Source Generators laufen zur Kompilierzeit und fügen C#-Quelldateien zur Kompilierung hinzu. IIncrementalGenerator ist die moderne API (C# 9+). RegisterPostInitializationOutput fügt statischen Code hinzu, ohne Benutzercode zu analysieren. Generatoren sind read-only: Sie können Dateien hinzufügen, aber vorhandenen Code nicht ändern.

csharp
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource("Hello.g.cs", """
                namespace Generated;
                public static class Hello
                {
                    public static string World => "Hello!";
                }
                """);
        });
    }
}

Generierung aus Attributen

ForAttributeWithMetadataName findet Typen, die mit einem bestimmten Attribut markiert sind. Die Pipeline: Syntax-Provider filtert Nodes, Transform extrahiert Daten, Collect batcht Ergebnisse, RegisterSourceOutput emitiert Code. Diese inkrementelle Pipeline cacht Ergebnisse und regeneriert nur, wenn sich Eingaben ändern, was Builds schnell hält.

csharp
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
    "MyApp.AutoToStringAttribute",
    predicate: (node, _) => node is ClassDeclarationSyntax,
    transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
    .Collect()
    .RegisterSourceOutput((spc, classes) =>
    {
        foreach (var cls in classes)
            GenerateToString(spc, cls);
    });

StringBuilder-Ausgabe

Generatoren produzieren Quellcode als Strings. StringBuilder ist effizient für mehrzeilige Ausgabe. Die Zielklasse muss partial sein, damit der Generator Member hinzufügen kann. Generierte Dateien erscheinen in der IDE unter Dependencies > Analyzers. Verwenden Sie .g.cs-Suffix konventionsgemäß, um generierte Dateien zu unterscheiden.

csharp
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
    var sb = new StringBuilder();
    sb.AppendLine($"namespace {cls.ContainingNamespace};");
    sb.AppendLine($"public partial class {cls.Identifier}");
    sb.AppendLine("{");
    sb.AppendLine("  public override string ToString()");
    sb.AppendLine("  {");
    var props = cls.Members.OfType<PropertyDeclarationSyntax>();
    sb.AppendLine($"    return $"{cls.Identifier}: " +
      string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
    sb.AppendLine("  }");
    sb.AppendLine("}");
    ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}

JsonSerializerContext

System.Text.Json enthält einen eingebauten Source Generator, der Serialisierungslogik vorab kompiliert und Laufzeit-Reflection eliminiert. Dies verbessert Leistung und unterstützt AOT-Kompilierung (Native AOT). Deklarieren Sie Typen mit JsonSerializable, dann verwenden Sie den generierten Kontext für Serialize/Deserialize-Aufrufe.

csharp
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);

Häufige Anwendungsfälle

Beliebte Generator-Anwendungsfälle: DTO-Generierung, DI-Registrierung, stark typisierte Konfiguration, AutoMapper-Profile, Logging-Templates und JSON-Serialisierung. Bibliotheken wie MediatR, AutoMapper und Microsoft.Extensions.Logging verwenden Generatoren, um Boilerplate zu reduzieren und AOT-Kompatibilität zu verbessern.

csharp
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }

// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();

// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)
21

Span & Memory

Span-Grundlagen

Span<T> bietet eine typsichere, speichersichere Sicht über zusammenhängenden Speicher ohne Kopieren. Es kann Arrays, stackalloc-Speicher, nicht verwalteten Speicher oder Strings umschließen. Span ist ein ref struct (nur Stack), daher kann es nicht geboxt, in Feldern gespeichert oder von Lambdas erfasst werden. Verwenden Sie es für hochperformantes Slicing und Parsing.

csharp
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };

// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3);  // [20, 30, 40]
slice[0] = 99;  // Modifies original array

// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

Memory für Async

Memory<T> ist das Heap-sichere Gegenstück zu Span<T>. Es kann in Feldern gespeichert, von Lambdas erfasst und über await-Grenzen hinweg verwendet werden. Konvertieren Sie via .Span zu Span<T>, wenn Sie tatsächliche Arbeit machen. Verwenden Sie Memory für Async-APIs und Span für synchrone Hot Paths.

csharp
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
    await Task.Delay(100);
    Span<byte> span = buffer.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] = (byte)(span[i] * 2);
}

byte[] data = new byte[1024];
await ProcessAsync(data);  // Implicit conversion to Memory<byte>

Allokationen vermeiden

Span eliminiert intermediäre String-Allokationen beim Parsing und Slicing. Für Hot Paths, die große Eingaben verarbeiten (HTTP-Parsing, JSON, Log-Dateien), kann dies GC-Drastisch reduzieren. int.TryParse und andere Primitive haben Span-Overloads. Rufen Sie ToString() nur auf, wenn Sie tatsächlich einen String benötigen.

csharp
// Traditional (allocates new string)
string Extract(string input, int start, int len)
    => input.Substring(start, len);

// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
    => input.Slice(start, len);

// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
    => int.TryParse(span, out result);

stackalloc & Unmanaged

stackalloc allokiert auf dem Call-Stack (automatisch freigegeben, kein GC-Druck), ist aber auf kleine Größen beschränkt. Für große Puffer verwenden Sie NativeMemory.Alloc/Free (C# 7+) oder Marshal.AllocHGlobal. Umschließen Sie nicht verwalteten Speicher immer in einem Span und geben Sie ihn in einem finally-Block frei, um Lecks zu verhindern.

csharp
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i;

// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
    int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
    try
    {
        Span<int> span = new Span<int>(ptr, 1024);
        span.Fill(42);
    }
    finally
    {
        NativeMemory.Free(ptr);
    }
}

Span-freundliche APIs

Entwerfen Sie APIs so, dass sie Span<T> oder ReadOnlySpan<T> für Puffer akzeptieren und Array-Allokationen vermeiden. Der Aufrufer entscheidet, ob er stackalloc, ein Array oder nicht verwalteten Speicher verwendet. Span.TryWrite (C# 10+) formatiert direkt in ein Span, ohne einen String zu allokieren – nützlich für Logging und Serialisierung.

csharp
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
    int count = 0;
    foreach (var v in new[] { 1, 2, 3 })
    {
        if (count >= dest.Length) break;
        dest[count++] = v;
    }
    return count;
}

// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());
22

Interpolierte Strings Deep

Formatbezeichner

String-Interpolation ($"...") bettet Ausdrücke in geschweifte Klammern ein. Formatbezeichner nach einem Doppelpunkt steuern die Ausgabe: C für Währung, F für feste Dezimalstellen, X für Hex, D für Ziffern mit Padding, P für Prozent, yyyy-MM-dd für Datumsangaben. Der Compiler übersetzt dies zu string.Format oder FormattableString.

csharp
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}");  // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}");  // FF
Console.WriteLine($"Padded: {age:D5}");  // 00030
Console.WriteLine($"Percent: {0.85:P0}");  // 85%

Raw String Interpolation

Verbatim interpolierte Strings ($@) bewahren Backslashes wörtlich und erlauben mehrzeilige Inhalte. Raw String Interpolation (C# 11) verwendet dreifache Anführungszeichen und eliminiert alle Escapes: kein Escapen von geschweiften Klammern oder Anführungszeichen nötig. Die Anzahl der $-Zeichen steuert, wie viele Klammern für Interpolation nötig sind.

csharp
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
  ""name"": ""{name}"",
  ""age"": {age}
}}";

// Raw string interpolation (C# 11)
string query = $$"""
    SELECT * FROM Users
    WHERE Name = '{{name}}'
    AND Age > {{age - 5}}
    """;
// $$ means {{ }} for interpolation, no escaping needed

FormattableString & Kultur

FormattableString bewahrt die Interpolationsstruktur und ermöglicht verzögerte Formatierung mit spezifischen Kulturen. Dies ist kritisch für Bibliotheken, die nicht von der aktuellen Kultur abhängen sollten. FormattableString.Invariant erzwingt invariante Kultur und verhindert lokalspezifische Probleme in Serialisierung und Logging.

csharp
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";

// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);

// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");

Interpolierte String-Handler

Interpolierte String-Handler (C# 10) lassen den Compiler Interpolationskomponenten direkt an eine Methode übergeben und vermeiden String-Allokation, wenn das Ergebnis nicht benötigt wird. So funktioniert hochperformantes Logging: Wenn das Log-Level deaktiviert ist, wird kein String gebaut. Der Handler empfängt Literale und formatierte Werte separat.

csharp
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
    public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
    {
        enabled = logger.IsEnabled;
        // Skip allocation if logging is disabled
    }
    public void AppendLiteral(string s) { /* ... */ }
    public void AppendFormatted<T>(T value) { /* ... */ }
}

// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");

Span-basierte Formatierung

Span.TryWrite formatiert interpolierte Strings direkt in einen Stack-allokierten Puffer und vermeidet Heap-Allokation vollständig. string.Create mit einem interpolierten String und Kultur gibt Ihnen kulturabhängige Formatierung ohne intermediäre Allokationen. Dies sind essenzielle Techniken für Hot Paths in hochdurchsatzanwendungen.

csharp
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];

if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
    var result = buffer[..chars];
    Console.WriteLine(result);
}

// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");
23

Datei-Scoped Namespaces

Datei-Scoped-Syntax

Datei-Scoped Namespaces (C# 10) deklarieren den Namespace für die gesamte Datei mit einem Semikolon statt geschweifter Klammern. Dies entfernt eine Einrückungsebene und macht Code flacher und leichter lesbar. Nur ein Datei-Scoped Namespace pro Datei ist erlaubt. Alle Typen in der Datei gehören zu diesem Namespace.

csharp
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
    public class UserService
    {
        public void Process() { }
    }
}

// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;

public class UserService
{
    public void Process() { }
}

Global Usings

Global Usings (C# 10), die in einer Datei deklariert sind, gelten für alle Dateien im Projekt. Kombinieren Sie mit Datei-Scoped Namespaces für sauberen, minimalen Boilerplate. Die .NET SDK-Projekte auto-generieren Global Usings für gängige Namespaces (ImplicitUsings). Reihenfolge: Global Usings, dann Datei-Usings, dann Namespace.

csharp
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;

namespace MyApp.Api;

public class UserController
{
    // Can use List, LINQ, Json, Models without per-file usings
}

Verschachtelte Namespaces

Datei-Scoped Namespaces verwenden punktierte Namen für Hierarchie: MyApp.Services.Authentication ist äquivalent zu verschachtelten Namespace-Blöcken. Mehrere Dateien können denselben Namespace teilen. Sie können keine Typen in einem anderen Namespace innerhalb derselben Datei verschachteln; verwenden Sie separate Dateien.

csharp
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;

public class TokenService { }

// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services;  // Same namespace

Projektkonfiguration

ImplicitUsings in der Projektdatei auto-generiert Global Usings für gängige System-Namespaces. Kombiniert mit Datei-Scoped Namespaces eliminiert dies den meisten Boilerplate. Nullable aktiviert Nullable-Referenztypen. Dies sind die Standardwerte für neue .NET 6+-Projekte.

csharp
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

<!-- ImplicitUsings generates:
     global using System;
     global using System.Collections.Generic;
     global using System.IO;
     global using System.Linq;
     global using System.Net.Http;
     global using System.Threading;
     global using System.Threading.Tasks;
-->

Migration & Best Practices

Verwenden Sie .editorconfig, um Datei-Scoped Namespaces zu erzwingen (IDE0161). Die IDE bietet eine Ein-Klick-Migration von traditionell zu Datei-Scoped. Best Practice: ein öffentlicher Typ pro Datei, mit Dateiname passend zum Typnamen. Dies hält Dateien klein und Navigation einfach.

csharp
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error  // Prefer file-scoped

// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace

// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }
24

Required Members

Required Properties

Der required-Modifier (C# 11) zwingt Aufrufer, Properties während der Objekterstellung zu initialisieren. Der Compiler emitiert CS9035, wenn ein Required-Member nicht gesetzt ist. Dies ersetzt Konstruktor-Validierung für obligatorische Properties. Funktioniert mit Init-only-Settern und Objekt-Initialisierern und bietet Kompilierzeit-Garantien.

csharp
public class User
{
    public required Guid Id { get; init; }
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }  // Optional
}

// Compiler enforces initialization
var user = new User
{
    Id = Guid.NewGuid(),
    Name = "Alice",
    Email = "[email protected]"
};

// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };

SetsRequiredMembers

Das SetsRequiredMembers-Attribut teilt dem Compiler mit, dass ein Konstruktor alle Required-Member initialisiert, sodass Aufrufer den Konstruktor ohne Objekt-Initialisierer verwenden können. Dies überbrückt die Lücke zwischen Required-Properties und traditionellen Konstruktoren. Das Attribut verifiziert nicht zur Laufzeit; es ist ein Kompilierzeit-Versprechen.

csharp
public class Configuration
{
    public required string ConnectionString { get; init; }
    public required int Port { get; init; }

    [SetsRequiredMembers]
    public Configuration(string conn, int port)
    {
        ConnectionString = conn;
        Port = port;
    }
}

// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
    ConnectionString = "localhost",
    Port = 5432
};

Required mit Records

Required-Member funktionieren mit Records. Für positionelle Records werden Required-Properties im Body deklariert, nicht im Primary-Constructor. Der with-Ausdruck bewahrt Required-Werte automatisch. Standardwerte (wie OpenedAt) machen Properties ohne den required-Modifier optional.

csharp
public record Account
{
    public required string AccountNumber { get; init; }
    public required decimal Balance { get; init; }
    public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}

// Positional record with required
public record Customer(string Name)
{
    public required string Email { get; init; }
}

var cust = new Customer("Alice") { Email = "[email protected]" };

Init vs. Required vs. Set

init macht eine Property nur während der Konstruktion setzbar (danach unveränderlich). required erzwingt Initialisierung. Zusammen (required + init) erstellen sie obligatorische unveränderliche Properties. required + set erlaubt Modifikation nach der Konstruktion. Wählen Sie basierend darauf, ob sich der Wert nach der Erstellung ändern soll.

csharp
// Init-only: can be set in initializer, but not required
public class Settings
{
    public string Name { get; init; } = "Default";
}

// Required + init: must be set, immutable after
public class SecureSettings
{
    public required string ApiKey { get; init; }
}

// Required + set: must be set, but can be modified later
public class MutableConfig
{
    public required string Environment { get; set; }
}

JSON-Serialisierung

System.Text.Json respektiert den required-Modifier: Deserialisierung wirft JsonException, wenn eine Required-Property im JSON fehlt. Dies bietet Laufzeit-Validierung passend zum Kompilierzeit-Vertrag. Kombinieren Sie mit init für unveränderliche DTOs, die garantiert alle Pflichtfelder ausgefüllt haben.

csharp
using System.Text.Json;

public class ApiResponse
{
    public required string Status { get; init; }
    public required int Code { get; init; }
    public string? Message { get; init; }
}

// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);

// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");
25

Collection Expressions

Collection-Expression-Syntax

Collection Expressions (C# 12) bieten eine einheitliche Syntax [..] zum Initialisieren beliebiger Collection-Typen. Der Compiler leitet den Typ ab und verwendet den entsprechenden Builder. Dies ersetzt ausführliche new List<int> { 1, 2, 3 }-Syntax. Funktioniert mit Arrays, List<T>, Span<T>, IEnumerable<T> und benutzerdefinierten Collections mit einem CollectionBuilder-Attribut.

csharp
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];

// Empty collections
int[] empty = [];
List<int> emptyList = [];

// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];

Spread-Operator

Der Spread-Operator (..) flacht eine Collection in den umschließenden Collection-Ausdruck ab. Dies ersetzt Concat, AddRange und manuelle Schleifen. Sie können Spreads mit einzelnen Elementen mischen. Der Spread funktioniert mit jedem IEnumerable<T> und macht das Kombinieren mehrerer Datenquellen prägnant.

csharp
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];

// Spread elements with ..
int[] combined = [..a, ..b, 7, 8];  // [1,2,3,4,5,6,7,8]

// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];

// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];

Return- & Parameter-Pattern

Collection Expressions funktionieren in Return-Anweisungen, Argumenten und Zuweisungen. Der Compiler konvertiert automatisch zum Zieltyp. Bedingte Spreads (Ternär mit ..) lassen Sie optional Elemente inkludieren. Dies macht das Erstellen von Collections aus mehreren optionalen Quellen sauber und lesbar.

csharp
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];

// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);

// Conditional spread
bool includeExtra = true;
var items = [
    1, 2, 3,
    .. includeExtra ? [4, 5] : []
];

Benutzerdefinierte Collection-Builder

Das CollectionBuilder-Attribut verbindet einen Collection-Typ mit einer Factory-Methode und ermöglicht Collection-Expression-Support. Die Create-Methode empfängt ein ReadOnlySpan<T> der Elemente. Dies erlaubt Drittanbieter-Collections, an der einheitlichen [..]-Syntax teilzunehmen. Das Builder-Pattern hält die Initialisierung effizient.

csharp
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
    private readonly List<T> _items = new();
    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    public static MyCollection<T> Create(ReadOnlySpan<T> items)
    {
        var col = new MyCollection<T>();
        foreach (var item in items) col._items.Add(item);
        return col;
    }
}

// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];

Params Collections

C# 12 erlaubt params mit jedem Collection-Typ, nicht nur Arrays. Collection Expressions können an params-Methoden übergeben werden. Wenn das Ziel Span<T> ist, kann der Compiler Stack-Allokation für kleine Collections verwenden und Heap-Allokation vollständig vermeiden. Dies macht variadische Methoden sowohl ergonomisch als auch effizient.

csharp
// C# 12: params with collection expressions
void Log(params string[] messages)
{
    foreach (var m in messages) Console.WriteLine(m);
}

// Traditional params call
Log("Error", "Warning", "Info");

// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);

// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]);  // May use stackalloc internally

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.