Funktionale Programmierung in C#: Von der Syntax zur Architektur
\n\nModernes C# hat die Grenzen einer rein objektorientierten Sprache längst überschritten. Beginnend mit Version 3.0 (LINQ) bis hin zu den neuesten .NET-Releases nimmt die Sprache aktiv Paradigmen der funktionalen Programmierung (FP) auf. Wenn Sie Lambdas und LINQ bereits beherrschen, aber tiefer eintauchen möchten, ist dieser Artikel genau das Richtige für Sie. Wir behandeln fortgeschrittene FP-Konzepte, die Ihren C#-Code vorhersagbarer, testbarer und prägnanter machen.
\n\n1. Reine Funktionen und Umgang mit Seiteneffekten
\n\nEine reine Funktion ist eine Funktion, die:
\n- \n
- Für dieselben Argumente immer dasselbe Ergebnis zurückgibt (referenzielle Transparenz). \n
- Keine Seiteneffekte hat (keinen globalen Zustand ändert, nicht in Dateien schreibt, keine HTTP-Anfragen sendet). \n
In C# sind mathematische Operationen ein ideales Beispiel für reine Funktionen. Das Problem ist, dass eine reale Anwendung nicht nur aus reinen Funktionen bestehen kann: Früher oder später müssen Daten gespeichert oder eine E-Mail gesendet werden. Die Lösung ist die Trennung von Logik und Effekten.
\n\nFortgeschrittene Technik: Verwenden Sie Funktoren und Monaden zur Containerisierung von Effekten. Betrachten wir ein einfaches Beispiel mit Maybe<T> (ein Analogon zu nullable, aber funktional):
public class Maybe<T>\n{\n private readonly T _value;\n public bool HasValue { get; }\n \n private Maybe(T value, bool hasValue) => (_value, HasValue) = (value, hasValue);\n \n public static Maybe<T> Some(T value) => new Maybe<T>(value, true);\n public static Maybe<T> None() => new Maybe<T>(default, false);\n \n // Funktor: Select (Map)\n public Maybe<TResult> Select<TResult>(Func<T, TResult> selector) =>\n HasValue ? Maybe<TResult>.Some(selector(_value)) : Maybe<TResult>.None();\n \n // Monade: SelectMany (FlatMap/Bind)\n public Maybe<TResult> SelectMany<TResult>(Func<T, Maybe<TResult>> binder) =>\n HasValue ? binder(_value) : Maybe<TResult>.None();\n}\n\n// Verwendung\nMaybe<int> result = Maybe<int>.Some(10)\n .Select(x => x * 2)\n .SelectMany(x => x > 15 ? Maybe<int>.Some(x) : Maybe<int>.None());\n\nDieser Code wirft keine Ausnahmen und prüft nicht manuell auf null – die gesamte Logik zur Behandlung fehlender Werte ist in der Monade gekapselt.
2. Currying und partielle Anwendung
\n\nIn der klassischen FP nimmt jede Funktion ein Argument entgegen. Currying ist die Umwandlung einer Funktion mit mehreren Parametern in eine Kette von verschachtelten Funktionen, die jeweils ein Argument entgegennehmen. Partielle Anwendung ist das Fixieren eines Teils der Argumente.
\n\nIn C# gibt es kein eingebautes Currying, aber es lässt sich leicht über Closures implementieren:
\n\n// Normale Funktion\nFunc<int, int, int, int> add = (a, b, c) => a + b + c;\n\n// Curry-Version\nFunc<int, Func<int, Func<int, int>>> curriedAdd = a => b => c => a + b + c;\n\n// Partielle Anwendung: erstes Argument fixieren\nFunc<int, Func<int, int>> add5 = curriedAdd(5);\n\n// Verwendung\nint sum = add5(10)(20); // 35\n\nFortgeschrittenes Szenario: Abhängigkeitsinjektion durch partielle Anwendung. Anstatt Konfiguration oder einen Dienst über den Klassenkonstruktor zu übergeben, können diese zum Zeitpunkt der Komposition fixiert werden:
\n\npublic class ReportService\n{\n public Func<string, string, string> FormatReport => \n (template, data) => $"[{DateTime.Now}] {template}: {data}";\n}\n\n// Verwendung mit partieller Anwendung\nvar service = new ReportService();\nvar formatWithTemplate = service.FormatReport("Error"); // Vorlage fixieren\nstring errorMessage = formatWithTemplate("Connection failed");