1. Einführung
Wenn man von Theorie zur Praxis übergeht, ist es logisch zu fragen: «Warum sollte ich als .NET- und C#-Entwickler all diese Techniken der funktionalen Programmierung nutzen?»
Tatsächlich ist C# keine rein funktionale Sprache wie F# oder Haskell. Aber seit Version 3.0 bis hin zu C# 14 hat es viele FP-Werkzeuge bekommen, die die Qualität und Ausdruckskraft des Codes deutlich steigern können.
Hier funktionieren sie besonders gut:
- Arbeiten mit Collections — LINQ, Map/Reduce, Filterung, Aggregation, Sortierung und anderes „Data-Magic“.
- Pure Functions — weniger Bugs durch State und Side-Effects, leichteres Debugging.
- Higher-order Functions — universelle, wiederverwendbare Komponenten, mit denen man gerne arbeitet.
- Immutability in Multithreading — einer der wichtigsten Hebel für sicheren Code in parallelen und asynchronen Szenarien.
- Function Composition — macht komplizierte Business-Logik kompakt, lesbar und testbar.
Tabelle: Vergleich OOP-Ansatz und FP-Ansatz in C#
| Aufgabe | Imperativ (OOP/alte Schule) | Funktional (FP) |
|---|---|---|
| Liste filtern | |
|
| Liste transformieren | |
|
| Suche nach Kriterium | |
|
| Aggregation | |
|
| Caching | |
|
2. LINQ: das Functionalste in C#
Wenn du gedacht hast, dass „funktionale Programmierung“ nur über Listen, Filter und so .Where, .Select, .Aggregate ist — Glückwunsch: genau so ist es! LINQ ist die Quintessenz von FP in C#.
Erinnerung: wie LINQ aufgebaut ist
LINQ operiert auf Collections mittels Ketten von Methoden, die Funktionen als Parameter nehmen (z.B. Lambdas). Zum Beispiel:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Wir bekommen nur gerade Zahlen und verdoppeln sie
var result = numbers
.Where(x => x % 2 == 0)
.Select(x => x * 2);
foreach (var number in result)
Console.WriteLine(number);
Was passiert hier?
- .Where — eine Higher-order Function: sie nimmt eine Funktion (x => x % 2 == 0) und liefert eine andere Collection zurück.
- .Select — nimmt ebenfalls eine Funktion (x => x * 2).
- Wir verändern die ursprüngliche Collection nicht, sondern bekommen ein neues Ergebnis.
Dieser Stil ist gut lesbar und erweiterbar (du kannst noch .OrderBy, .Take, .Distinct etc. anhängen).
Beachte! Lambda-Ausdrücke sind ein bequemer Weg, einen „Delegate on the fly“ zu erstellen. LINQ wäre ohne FP-Unterstützung in C# nicht möglich.
Schemata: Funktionale Verarbeitung einer Collection
Collection --> Where(x => bool) --> Select(x => y) --> Neues Ergebnis
3. Funktionskomposition und Daten-Pipeline
In FP nutzt man oft Komposition: eine komplexe Operation wird als Kette kleiner Funktionen gebaut, jede macht ihre eigene Aufgabe.
Beispiel: String-Verarbeitungskette
Zustandsverändernder Stil (OOP):
string s = " hello world ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!
Mehr „funktional“ — als Pipeline von Funktionen:
Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";
// Funktionskomposition — wir wenden sie nacheinander an
Func<string, string> pipeline = x => addBang(upper(trim(x)));
Console.WriteLine(pipeline(" hello world ")); // HELLO WORLD!
Ein einfacher Compose-Kombinator:
Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
x => g(f(x));
// Und jetzt pipeline über Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);
Console.WriteLine(pipeline2(" hello again ")); // HELLO AGAIN!
4. Arbeiten mit Immutability: Schutz vor Bugs
Immutability ist ein Grundbaustein von FP. Wir verändern Datenstrukturen nicht, sondern geben neue zurück. Das ist besonders wichtig in Multithreading-Apps.
Beispiel: „falsch“ (mutierbar)
List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;
Beispiel: „richtig“ (funktional)
var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();
Im modernen C# gibt es Collections wie ImmutableList<T> und andere Typen im Namespace System.Collections.Immutable:
using System.Collections.Immutable;
var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // Gibt eine neue Liste zurück!
5. Higher-order Functions im echten Leben
Higher-order Functions sind ein Weg, universelle Komponenten zu schreiben ohne viele bedingte Anweisungen.
Beispiel: Universeller Filter für Users
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User { Name = "Vasya", Age = 26 },
new User { Name = "Katya", Age = 17 },
new User { Name = "Lyosha", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
return source.Where(u => predicate(u)).ToList();
}
// Nutzung:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);
6. Pattern matching und switch-Ausdrücke
Modernes C# nutzt Pattern Matching intensiv: switch-Ausdrücke ersetzen oft schwere if-Ketten.
object value = 123;
string description = value switch
{
int i when i > 100 => "Große Zahl",
string s when s.Length > 3 => "Lange Zeichenkette",
null => "Leerer Wert",
_ => "Unbekannt"
};
Console.WriteLine(description); // Große Zahl
7. Memoization: Ergebnisse von Funktionen cachen
Memoization bedeutet, das Ergebnis einer Funktion für gleiche Argumente zu cachen. In C# lässt sich das leicht selbst implementieren.
Func<int, int> SlowFib = null; // Rekursive Fibonacci-Funktion
var cache = new Dictionary<int, int>();
SlowFib = n =>
{
if (cache.ContainsKey(n))
return cache[n];
if (n <= 1)
cache[n] = n;
else
cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
return cache[n];
};
Console.WriteLine(SlowFib(40)); // Blitzschnell!
8. Currying und partielles Anwenden
Partielles Anwenden bedeutet, einen Teil der Argumente einer Funktion zu fixieren. In C# macht man das bequem mit Lambdas.
Func<int, int, int> add = (a, b) => a + b;
// Wir fixieren das erste Argument
Func<int, int> add10 = b => add(10, b);
Console.WriteLine(add10(5)); // 15
Console.WriteLine(add10(100)); // 110
9. Deklarativer Stil mit Funktionen
Imperativ:
var result = new List<int>();
foreach (var n in numbers)
{
if (n > 0)
result.Add(n * n);
}
Deklarativ:
var result = numbers
.Where(n => n > 0)
.Select(n => n * n)
.ToList();
10. Praktische Aufgabe
Wir implementieren ein Filtermodul für Aufgaben in einem „Task-Manager für Studierende“.
Modell:
class StudentTask
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
public int Priority { get; set; }
}
Ausgangsdaten:
var tasks = new List<StudentTask>
{
new StudentTask { Title = "Sdelat domashku", IsCompleted = false, Priority = 2 },
new StudentTask { Title = "Popit kofi", IsCompleted = true, Priority = 3 },
new StudentTask { Title = "Posmotret lekciyu", IsCompleted = false, Priority = 1 }
};
Universeller Filter:
List<StudentTask> FilterTasks(
List<StudentTask> all,
Predicate<StudentTask> predicate)
{
return all.Where(t => predicate(t)).ToList();
}
// Suche nach unerledigten Aufgaben mit Priority > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);
// Ausgabe
foreach (var task in importantTasks)
Console.WriteLine(task.Title);
Prädikats-Kombinatoren:
Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;
// Mehrere Kriterien kombinieren, Variante 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));
// Variante 2: Kombinator-Funktion für zwei Prädikate
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);
var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));
11. Besonderheiten und typische Fehler beim Einsatz des funktionalen Ansatzes in C#
Erstens: denk dran, C# ist streng typisiert. Manchmal musst du Typen explizit angeben, besonders wenn Funktionen Delegates oder komplexe Lambdas zurückgeben. Sonst bekommst du Compiler-Fehler wegen nicht abgeleiteter Typen.
Zweitens: übergib keine Funktionen mit Side-Effects an Stellen, wo pure Functions erwartet werden. Das Verändern externer Variablen zerstört Vorhersagbarkeit. Versuche, dass deine Funktionen keinen State außerhalb ihrer Scope mutieren.
Drittens: pass auf Closure-Capture auf, besonders in async- und Multithread-Code. Eine Variable, deren Wert sich ändert nachdem sie an eine Lambda übergeben wurde (z.B. in LINQ), kann zu subtilen Bugs führen.
Und schließlich: übermäßiger FP-Stil kann den Code für ein Team, das das nicht gewohnt ist, schwerer wartbar machen. Nutze FP dort, wo es wirklich vereinfacht, nicht nur der „Schönheit“ wegen.
GO TO FULL VERSION