Zum Inhalt springen

C# CSharp Programmierung fortgeschrittene Techniken – Von Variablen zu Azure Cloud Techniken.

C# Charp Programmierung Tutorial

Werbung: Wenn Du ein gutes C# Lern-Buch benötigst, können wir Dir folgendes Buch von Amazon.de empfehlen:

Inhaltsverzeichnis

21. Fortgeschrittene Fehlerbehandlung

Fehlerbehandlung in C# basiert auf dem Konzept von Ausnahmen (Exceptions). Der try-catch-finally Block wird verwendet, um Ausnahmen abzufangen und darauf zu reagieren. Fortgeschrittene Techniken beinhalten das Erstellen eigener benutzerdefinierter Ausnahmen und die Arbeit mit speziellen Ausnahmetypen wie AggregateException.

Benutzerdefinierte Ausnahmen

In manchen Fällen möchtest du eigene Ausnahmeklassen definieren, um speziellere Fehlertypen in deiner Anwendung zu behandeln.

Beispiel für eine benutzerdefinierte Ausnahme:

// Benutzerdefinierte Ausnahmeklasse
public class UngueltigesAlterException : Exception
{
public UngueltigesAlterException(string message) : base(message)
{
}
}

// Nutzung der benutzerdefinierten Ausnahme
public void PruefeAlter(int alter)
{
if (alter < 0)
{
throw new UngueltigesAlterException("Das Alter kann nicht negativ sein.");
}
Console.WriteLine("Das Alter ist gültig: " + alter);
}

try
{
PruefeAlter(-5);
}
catch (UngueltigesAlterException ex)
{
Console.WriteLine("Fehler: " + ex.Message); // Ausgabe: Fehler: Das Alter kann nicht negativ sein.
}

Hier haben wir eine benutzerdefinierte Ausnahme UngueltigesAlterException erstellt, die verwendet wird, um ein ungültiges Alter abzufangen.

finally-Block

Der finally-Block wird immer ausgeführt, unabhängig davon, ob eine Ausnahme auftritt oder nicht. Das ist nützlich, um Ressourcen freizugeben oder Aufräumarbeiten durchzuführen, wie z.B. das Schließen von Dateien oder Datenbankverbindungen.

try
{
// Code, der eine Ausnahme auslösen könnte
int ergebnis = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Fehler: " + ex.Message);
}
finally
{
Console.WriteLine("Aufräumarbeiten werden durchgeführt."); // Wird immer ausgeführt
}

AggregateException

Wenn du mit paralleler oder asynchroner Programmierung arbeitest, kann es sein, dass mehrere Ausnahmen gleichzeitig auftreten. In solchen Fällen wird eine AggregateException ausgelöst, die eine Sammlung von Ausnahmen enthält.

Beispiel für die Handhabung von AggregateException:

try
{
Task t = Task.WhenAll(
Task.Run(() => { throw new InvalidOperationException("Fehler 1"); }),
Task.Run(() => { throw new ArgumentNullException("Fehler 2"); })
);
t.Wait();
}
catch (AggregateException ex)
{
foreach (var innerException in ex.InnerExceptions)
{
Console.WriteLine("Gefangene Ausnahme: " + innerException.Message);
}
}

In diesem Beispiel werden zwei Ausnahmen in parallelen Aufgaben ausgelöst, und AggregateException wird verwendet, um beide Ausnahmen zu behandeln.

22. Parallelität und Multithreading

Parallelität und Multithreading ermöglichen es, mehrere Aufgaben gleichzeitig auszuführen, was besonders bei CPU-intensiven oder I/O-lastigen Operationen nützlich ist.

Threads

In C# kannst du manuell Threads erstellen, um parallele Aufgaben auszuführen:

using System.Threading;

Thread thread = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Thread läuft: " + i);
Thread.Sleep(1000); // Simuliert eine Pause
}
});

thread.Start();

Hier wird ein neuer Thread erstellt, der parallel zur Hauptanwendung läuft. Der Thread führt eine Schleife aus und pausiert eine Sekunde zwischen den Durchläufen.

Parallel-Klasse

Die Parallel-Klasse bietet eine einfache Möglichkeit, parallele Schleifen und Aktionen auszuführen. Sie ist besonders nützlich, wenn du große Datenmengen verarbeiten musst.

int[] zahlen = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

Parallel.For(0, zahlen.Length, i =>
{
Console.WriteLine("Verarbeite Zahl: " + zahlen[i]);
});

In diesem Beispiel wird die Schleife mithilfe der Parallel.For-Methode parallelisiert, sodass die Zahlen gleichzeitig verarbeitet werden.

Task Parallel Library (TPL)

Die Task-Klasse ist eine abstraktere und bevorzugte Möglichkeit, asynchrone oder parallele Arbeit in C# zu implementieren. Sie bietet eine einfachere Möglichkeit, mit parallelen Aufgaben zu arbeiten, und unterstützt auch das Warten und Verketten von Aufgaben.

Beispiel:

Task task1 = Task.Run(() => Console.WriteLine("Task 1 ausgeführt"));
Task task2 = Task.Run(() => Console.WriteLine("Task 2 ausgeführt"));

Task.WaitAll(task1, task2); // Wartet, bis beide Aufgaben abgeschlossen sind

Hier laufen zwei Aufgaben parallel, und das Programm wartet, bis beide Aufgaben abgeschlossen sind, bevor es fortfährt.

async und await mit parallelen Tasks

Du kannst auch async und await verwenden, um parallele Aufgaben einfach zu verwalten:

public async Task FühreParalleleAufgabenAus()
{
Task task1 = Task.Run(() => Console.WriteLine("Aufgabe 1 läuft"));
Task task2 = Task.Run(() => Console.WriteLine("Aufgabe 2 läuft"));

await Task.WhenAll(task1, task2); // Wartet auf beide Aufgaben
Console.WriteLine("Beide Aufgaben abgeschlossen");
}

In diesem Beispiel werden zwei Aufgaben parallel ausgeführt, und das Programm wartet, bis beide abgeschlossen sind, bevor es die nächste Anweisung ausführt.

23. Async Streams

Async Streams erlauben es, asynchrone Datenströme zu konsumieren. Das ist nützlich, wenn du Daten aus einer Quelle beziehst, die schrittweise geliefert werden, wie z.B. Netzwerk- oder Datenbankabfragen.

Beispiel für einen asynchronen Stream:

public async IAsyncEnumerable<int> GeneriereZahlenAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(500); // Simuliert eine Verzögerung
yield return i;
}
}

public async Task KonsumiereZahlenAsync()
{
await foreach (var zahl in GeneriereZahlenAsync())
{
Console.WriteLine(zahl);
}
}

In diesem Beispiel generiert GeneriereZahlenAsync eine Reihe von Zahlen asynchron, und KonsumiereZahlenAsync liest diese Zahlen asynchron aus dem Stream.

24. lock und thread-sichere Programmierung

Wenn du parallele oder multithreaded Programmierung betreibst, kann es vorkommen, dass mehrere Threads gleichzeitig auf dieselben Daten zugreifen. Dies kann zu Problemen wie Race Conditions führen. Um sicherzustellen, dass nur ein Thread zur gleichen Zeit auf eine kritische Sektion des Codes zugreift, kannst du den lock-Mechanismus verwenden.

Beispiel:

private static object lockObj = new object();

public void KritischeSektion()
{
lock (lockObj)
{
// Dieser Code kann nur von einem Thread zur gleichen Zeit ausgeführt werden
Console.WriteLine("Kritische Sektion betreten von Thread " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000); // Simuliert eine lang andauernde Aufgabe
Console.WriteLine("Kritische Sektion verlassen");
}
}

Durch das lock-Schlüsselwort wird sichergestellt, dass der geschützte Codeblock nur von einem Thread zur gleichen Zeit ausgeführt wird.

25. Speicherverwaltung in C#

C# verwendet eine Garbage Collection (GC), die automatisch nicht mehr benötigte Objekte aus dem Speicher entfernt. Das bedeutet, dass du dich in den meisten Fällen nicht selbst um die Speicherfreigabe kümmern musst. Es gibt jedoch einige fortgeschrittene Techniken, die es dir ermöglichen, die Speicherverwaltung besser zu kontrollieren.

IDisposable und using

Ressourcen wie Datenbankverbindungen, Datei-Handles oder Netzwerkverbindungen benötigen oft eine explizite Freigabe, sobald sie nicht mehr gebraucht werden. Dies erfolgt in C# häufig durch die Implementierung des IDisposable-Interfaces und das using-Statement.

Beispiel:

class DateiManager : IDisposable
{
private FileStream stream;

public DateiManager(string dateiPfad)
{
stream = new FileStream(dateiPfad, FileMode.Open);
}

public void Lesen()
{
// Lese Operation
}

public void Dispose()
{
stream?.Dispose();
Console.WriteLine("Ressourcen wurden freigegeben.");
}
}

// Nutzung von "using" zur automatischen Freigabe der Ressourcen
using (DateiManager manager = new DateiManager("beispiel.txt"))
{
manager.Lesen();
} // Hier wird die Dispose-Methode automatisch aufgerufen.

Das using-Statement sorgt dafür, dass die Methode Dispose automatisch aufgerufen wird, wenn der Codeblock verlassen wird. Dadurch werden Ressourcen effizient freigegeben.

Finalizer

Ein Finalizer ist eine Methode, die aufgerufen wird, bevor ein Objekt durch den Garbage Collector zerstört wird. Du kannst ihn überschreiben, um sicherzustellen, dass wichtige Aufräumarbeiten stattfinden, wenn ein Objekt freigegeben wird.

Beispiel:

class Beispiel
{
~Beispiel()
{
Console.WriteLine("Finalizer wird aufgerufen.");
}
}

Es ist wichtig zu beachten, dass Finalizer weniger häufig verwendet werden, da sie nicht sofort ausgeführt werden und den Garbage Collector beeinflussen können.

26. Unsicherer Code und Zeiger (Unsafe Code)

In C# kannst du unter bestimmten Umständen unsicheren Code schreiben, der den direkten Zugriff auf Speicheradressen mithilfe von Zeigern ermöglicht. Dies kann nützlich sein, wenn du mit niedrigstufigen Operationen arbeitest, wie z.B. die direkte Manipulation von Speicher oder das Arbeiten mit Interop-Bibliotheken.

Unsicherer Code muss explizit als „unsafe“ markiert werden und erfordert spezielle Berechtigungen.

Beispiel:

unsafe
{
int zahl = 10;
int* ptr = &zahl;

Console.WriteLine("Wert der Zahl: " + *ptr); // Dereferenzieren des Zeigers
*ptr = 20; // Wert über den Zeiger ändern
Console.WriteLine("Neuer Wert der Zahl: " + zahl);
}

Unsicherer Code wird selten verwendet, da C# grundsätzlich darauf ausgelegt ist, sicher und verwaltet zu sein. Er wird jedoch in speziellen Fällen wie bei der Arbeit mit Hardware oder nativen Bibliotheken eingesetzt.

27. Attribute und Reflektion für Metadaten

Attribute bieten eine Möglichkeit, zusätzliche Metadaten zu Klassen, Methoden, Eigenschaften usw. hinzuzufügen. Diese Metadaten können zur Laufzeit über Reflection abgefragt werden, um spezielle Verhalten zu implementieren.

Beispiel für benutzerdefinierte Attribute:

// Definiere ein Attribut
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class InfoAttribute : Attribute
{
public string Beschreibung { get; }
public InfoAttribute(string beschreibung)
{
Beschreibung = beschreibung;
}
}

// Anwenden des Attributs auf eine Klasse
[Info("Dies ist eine Beispielklasse.")]
class Beispiel
{
[Info("Dies ist eine Beispielmethode.")]
public void Methode()
{
Console.WriteLine("Methode wurde ausgeführt.");
}
}

// Nutzung von Reflection, um die Attribute zu lesen
Type typ = typeof(Beispiel);
object[] attribute = typ.GetCustomAttributes(false);

foreach (InfoAttribute attr in attribute)
{
Console.WriteLine("Klassenbeschreibung: " + attr.Beschreibung);
}

Attribute ermöglichen es dir, deinen Code besser zu annotieren, und werden oft für Frameworks, Validierungen oder zur Generierung von Code verwendet.

28. Memory Span<T> und Stackalloc

Span<T> ist eine der neueren Ergänzungen in C#, die eine speichereffiziente Möglichkeit bietet, mit Sequenzen von Daten (wie Arrays) zu arbeiten, ohne unnötige Kopien zu erstellen. Span<T> ermöglicht dir, Abschnitte von Speicher zu verwalten, und kann auf dem Stack oder Heap liegen.

Beispiel mit Span<T>:

Span<int> span = stackalloc int[5] { 1, 2, 3, 4, 5 };

for (int i = 0; i < span.Length; i++)
{
Console.WriteLine(span[i]);
}

Der stackalloc-Operator erlaubt es, Speicher direkt auf dem Stack zu reservieren, was in manchen Szenarien schneller ist als die Heap-Allokation. Span ist besonders nützlich, wenn du Performance optimieren möchtest, da es weniger Overhead verursacht.

29. ValueTask

ValueTask ist eine Alternative zu Task, die in Szenarien verwendet wird, in denen ein Ergebnis häufig sofort verfügbar ist, ohne dass eine asynchrone Operation tatsächlich erforderlich ist. ValueTask spart Speicher und Overhead, da es weniger Speicherzuweisungen erfordert als ein reguläres Task.

Beispiel:

public async ValueTask<int> BerechneAsync(int x)
{
if (x == 0)
{
return 42; // Kein Task erforderlich, da das Ergebnis sofort verfügbar ist
}

await Task.Delay(1000); // Simuliert eine asynchrone Operation
return x * 2;
}

ValueTask<int> result = BerechneAsync(0);
Console.WriteLine(result.Result); // Ausgabe: 42

ValueTask wird in performancekritischen Szenarien bevorzugt, in denen häufig synchrone Rückgabewerte erwartet werden, aber die Möglichkeit einer asynchronen Ausführung vorhanden ist.

30. Saubere Code-Prinzipien und Patterns

Fortgeschrittene C#-Entwickler achten darauf, dass ihr Code wartbar, erweiterbar und effizient bleibt. Hier sind einige der wichtigsten Prinzipien und Muster:

SOLID-Prinzipien

  • Single Responsibility Principle: Jede Klasse sollte nur eine Aufgabe haben.
  • Open/Closed Principle: Klassen sollten offen für Erweiterungen, aber geschlossen für Modifikationen sein.
  • Liskov Substitution Principle: Abgeleitete Klassen sollten durch ihre Basisklassen ersetzbar sein.
  • Interface Segregation Principle: Verwende viele spezifische Interfaces anstelle eines großen.
  • Dependency Inversion Principle: Abhängigkeiten sollten von Abstraktionen und nicht von konkreten Implementierungen abhängen.

DRY (Don’t Repeat Yourself)

Wiederholter Code sollte vermieden werden. Stattdessen sollten gemeinsame Funktionalitäten in Methoden ausgelagert oder durch Vererbung, Polymorphismus oder Generics gelöst werden.

KISS (Keep It Simple, Stupid)

Halte den Code einfach und verständlich. Vermeide unnötige Komplexität.

YAGNI (You Ain’t Gonna Need It)

Implementiere keine Features, die du nicht jetzt brauchst. Übermäßige Vorausplanung kann den Code unnötig aufblähen und schwer wartbar machen.

Seiten: 1 2 3 4 5 6