1. Einführung
Funktionale Programmierung (FP) ist eine Programmierparadigma, bei der der grundlegende Baustein keine Objekte und keine Prozeduren/Methoden sind, sondern die Funktion im mathematischen Sinn. In FP liegt der Fokus darauf zu beschreiben, "was berechnet werden soll", und nicht "wie es berechnet wird".
Du bist schon einzelnen FP-Ideen begegnet, als du mit Lambda-Ausdrücken und LINQ gearbeitet hast. Aber wo liegt der Unterschied? Praktisch: OOP beschreibt Objekte und ihre Interaktion, prozedurales Programmieren ist eine Reihenfolge von Schritten, und FP ist Funktionskomposition, das Übergeben von Verhalten als Wert, Verzicht auf Zustandsänderung (immutability) und das Vermeiden von Nebenwirkungen.
Warum überhaupt eine neue Paradigma?
- Saubererer, vorhersagbarer und besser testbarer Code.
- Einfachere Multithreading-Unterstützung ("kein Zustand — keine Probleme").
- Kürzer und ausdrucksstärker (weniger Code — weniger Bugs).
- Hochgradig wiederverwendbare, abstrakte Bausteine.
Analogie
Stell dir vor, ein Restaurant bekommt die Bestellung: "mach ein Omelett". Der imperative Koch führt eine Liste von Anweisungen aus: Eier nehmen, aufschlagen, verquirlen, braten. Der funktionale Koch sagt: res = omelett(eier) — er arbeitet mit Funktionen und abstrahiert vom internen Zustand der Küche (naja, fast).
In C# kann man beide Ansätze nutzen. Das macht die Sprache sehr flexibel und mächtig — besonders in echten Projekten.
Schlüsselkonzepte der FP
1. Higher-Order-Funktionen
Funktionen kann man als Parameter übergeben, aus anderen Funktionen zurückgeben und in Variablen speichern. Du hast das bereits mit Lambda-Ausdrücken und Delegaten gemacht. In FP sind solche "Funktionen über Funktionen" die Grundlage.
2. Reine Funktionen
Eine Funktion ist "rein", wenn ihr Ergebnis nur von den Parametern abhängt und sie nichts außerhalb von sich verändert (keine Nebenwirkungen). Zwei gleiche Aufrufe mit gleichen Argumenten liefern dasselbe Ergebnis.
3. Immutability (Unveränderlichkeit)
Daten werden nicht "in-place" verändert: ein neuer Zustand ist ein neues Objekt. Das macht das Nachdenken über das Programm deutlich einfacher und hilft bei Multithreading.
4. Keine Nebenwirkungen
Die Funktion schreibt nichts in eine Datei, ändert keine globalen Variablen, zeichnet nichts auf dem Bildschirm — sie gibt einfach ein Ergebnis zurück. Im echten Leben sind Nebenwirkungen unvermeidlich, aber man versucht, sie an den Rändern des Systems zu isolieren.
5. Funktionskomposition
Man kann eine Funktion aus anderen bauen, wie aus Bausteinen. Zum Beispiel: positive Zahlen filtern, ihre Quadrate nehmen und summieren. Jede Operation ist eine eigene Funktion und lässt sich leicht kombinieren (Where → Select → Sum).
2. FP in C#: von Theorie zur Praxis
C# ist eine multiparadigmatische Sprache: sie unterstützt OOP, prozeduralen Stil und einen starken funktionalen Stil (mit Lambdas, Delegates, Extension-Methoden und LINQ).
Lasst es uns an der Beispiel-App erklären
Stell dir vor, wir entwickeln ein Programm, das mit Listen von Zahlen und Strings arbeitet. Unsere Aufgabe ist es, verschiedene Operationen auf diesen Daten im funktionalen Stil anzuwenden.
Beispiel 1: Nutzung von Higher-Order-Funktionen
// Wendet eine Aktion auf alle Elemente einer Liste an
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
Verwendung:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // Funktion als Parameter
Siehst du? Eine Funktion kann in einer Variablen "gespeichert" oder wie ein normaler Wert übergeben werden — genau wie ein Apfel in der Küche!
Beispiel 2: Reine Funktion
Eine Funktion, die den Programmzustand nicht verändert und nur vom Input abhängt:
int MultiplyByTwo(int x)
{
return x * 2;
}
- Hängt von nichts Außenstehendem ab.
- Verändert nichts außerhalb.
- Für x = 5 gibt sie immer 10 zurück.
Im Vergleich dazu eine Funktion, die eine globale Variable nutzt und verändert:
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
Das ist keine reine Funktion — das Ergebnis hängt vom äußeren Zustand ab, und sie verändert diesen Zustand.
Beispiel 3: Unveränderliche Daten
Anstatt Eingabedaten zu ändern, erzeugen wir neue:
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
Die Eingabeliste bleibt komplett unverändert. In Multithreaded-Programmen ist das besonders bequem: weniger Locks und weniger Data Races.
Beispiel 4: Funktionskomposition
Summe der Quadrate aller geraden Zahlen erhalten:
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // Nur gerade lassen
.Select(n => n * n) // Quadrieren
.Sum(); // Summieren
}
Lesbar und deklarativ: jede Operation ist eine separate Funktion.
3. Nützliche Feinheiten
FP, LINQ und C#
LINQ ist praktisch "FP in Aktion" für Collections: du nutzt Higher-Order-Funktionen (Where, Select usw.), bekommst neue Sequenzen ohne die Ursprungsdaten zu mutieren, und jede Transformation ist ein eigener Ausdruck. Das Ergebnis ist ein IEnumerable<T>, das beschreibt, was man bekommen will, und nicht wie man iteriert.
Tabelle der Analogien
| Imperativ (prozedural/OOP) | Funktional (LINQ/FP-Stil) |
|---|---|
|
|
| Eine Collection mutieren | Eine neue Collection bekommen |
| Zustand (total += x) | Reine Funktionen (xs.Sum()) |
| Beschreiben als: "mach dies" | Beschreiben: "was wir bekommen wollen" |
FP vs OOP: zwei Welten — ein C#
Das sind keine konkurrierenden Lager. In echten C#-Projekten kombiniert man sie: das Domain Model baut man oft mit Klassen (OOP), während Collection-Verarbeitung, Datenaggregation und Transformationen im funktionalen Stil via LINQ, Lambdas und Extension-Methoden erfolgen.
Dein Wissen über Delegates ist direkt nützlich: Func<T, TResult>, Predicate<T>, Action<T> — typische Bausteine des FP-Stils.
Universelle Filterfunktion:
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
Aufrufe:
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
Statt vieler ähnlicher Methoden mit unterschiedlichen Bedingungen — eine universelle Funktion.
Warum Arbeitgeber und Interviews FP-Entwickler mögen?
- FP hilft, kleine Codeeinheiten zu testen, ohne das ganze System hochzufahren.
- Die Logik ist einfacher zu warten: weniger Zustände — weniger Fehlerquellen.
- Parallel- und async-Code ist einfacher zu schreiben — kein globaler Zustand, weniger Data Races.
Und wie nicht zum Fanatiker werden?
Ja, FP ist mächtig. Aber C# ist kein rein funktionaler Sprache, und nicht jede Aufgabe verlangt perfekte Reinheit. Scheue dich nicht vor lokalen Variablen und sinnvoller Mutation dort, wo es passt. Wichtiger sind Lesbarkeit, Vorhersagbarkeit und Testbarkeit. FP-Elemente sind ein Werkzeug, keine Religion.
4. Typische Anfängerfehler
Man kann leicht auf scheinbar funktionalen Code hereinfallen, der in Wirklichkeit nicht funktional ist.
Zum Beispiel: eine Funktion gibt eine neue Collection zurück, mutiert aber unterwegs die ursprüngliche Liste — das bricht das Immutability-Prinzip und enttäuscht die Erwartungen des Aufrufers.
Ein weiteres Beispiel: ein Lambda greift auf eine äußere Variable zu und verändert sie. In der funktionalen Paradigma gilt das als Nebenwirkung und macht das Verhalten weniger vorhersagbar.
Der C#-Compiler wird dich nicht stoppen: die Sprache erlaubt beides. Deswegen ist es in FP-Praktiken wichtig, darauf zu achten, dass eine Funktion "für sich lebt", nichts außerhalb ändert und außerhalb nicht liest, außer ihren Argumenten.
GO TO FULL VERSION