1. Einführung
Reine Funktion — das ist eine Funktion, die bei gleichen Eingabedaten immer dasselbe Ergebnis zurückgibt und keinerlei Nebeneffekte auslöst. Einfach gesagt: eine reine Funktion "verdirbt" nichts um sich herum und lässt sich auch nicht von außen beeinflussen.
Eine Funktion gilt als rein, wenn:
- Sie nur einen Wert basierend auf ihren Eingabeargumenten berechnet.
- Keine externen Variablen oder den Programmzustand ändert.
- Nicht von externen Variablen oder Zuständen abhängt, die sich ändern könnten.
Zwei goldene Regeln der Reinheit
- Determinismus: dieselbe Eingabe — dasselbe Ergebnis.
- Keine Nebeneffekte: die Funktion ändert nichts außerhalb von sich selbst: keine Dateien, keine globalen Variablen, keine UI (hallo, Console.WriteLine).
Keine Religion, sondern gesunder Menschenverstand
Es mag wie eine akademische Spielerei wirken, aber die Praxis zeigt das Gegenteil:
- Reine Funktionen sind vorhersehbar. Sie sind einfach zu testen — du rufst sie mit bestimmten Argumenten auf und bekommst ein bestimmbares Ergebnis.
- Eine reine Funktion kann man frei im Code verschieben, ohne Angst zu haben, dass irgendwo etwas kaputt geht.
- In einer Multicore-Welt ist eine reine Funktion eine Garantie für Sicherheit: man kann sie parallel ausführen, ohne Datenrennen.
2. Beispiele für reine und unreine Funktionen
Reine Funktion: alles ist vorhersehbar
// Reine Funktion: verändert nichts außerhalb von sich
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
Ruf Add(2, 3) hundertmal hintereinander auf — es wird immer 5 zurückgeben. Langweilig, aber zuverlässig.
Unreine Funktion: Regeln werden gebrochen
// Bricht Reinheit: hängt vom externen Zustand ab (statische Variable)
int counter = 0;
int Increase()
{
counter++;
return counter;
}
Hier wird Increase() jedes Mal einen neuen Wert liefern — also keine Determiniertheit mehr.
// Bricht Reinheit: erzeugt einen externen Effekt (Ausgabe auf der Konsole)
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // Nebeneffekt!
return sum;
}
Wie sieht es mit Zufall und Zeit aus?
Jede Funktion, die DateTime.Now oder Random verwendet, ist nicht mehr rein:
// Nicht rein!
int GetRandomNumber()
{
return new Random().Next();
}
Vergleichstabelle
| Eigenschaft | Reine Funktion | Unreine Funktion |
|---|---|---|
| Immer gleiches Ergebnis für gleiche Argumente | Ja | Nein |
| Nebeneffekte | Nein | Ja |
| Abhängigkeit vom externen Zustand | Nein | Ja |
3. Unveränderlichkeit von Daten: Theorie und Praxis
Unveränderlichkeit (immutability) — das ist der Ansatz, bei dem ein Objekt nach seiner Erstellung nicht mehr verändert werden kann. Wenn ein neuer Wert nötig ist, erstellt man ein neues Objekt.
Warum ist das wichtig?
- Die Anwendung wird resistenter gegen versehentliche Datenänderungen.
- Es gibt keine geheimen "Lecks" von Änderungen: wenn du ein Objekt hast, kann niemand still und heimlich sein Feld ändern.
- Unveränderlichkeit ist die Grundlage vieler automatischer Optimierungen und paralleler Berechnungen.
Einfache Beispiele in C#
Unveränderliche Typen in .NET
Strings (string) sind in C# unveränderlich! Jedes Mal, wenn du string.Concat(s, "world") machst, wird ein neuer String erzeugt.
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
Arrays und Collections: standardmäßig veränderlich
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // Array wurde verändert!
Unveränderlichkeit "in einfachen Worten": Code-Kompilierung
Anstatt ein vorhandenes Objekt/Wert zu ändern, geben wir ein neues zurück:
// Stattdessen:
void AddToList(List<int> list, int value)
{
list.Add(value); // Mutiert!
}
// Besser so:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // Neue Liste
return newList;
}
Illustration: ändern vs. nicht ändern
flowchart LR
A[Ursprungsobjekt] --"Mutation"--> B[Dasselbe Objekt, aber innen anders]
A --"Unveränderlichkeit"--> C[Neues Objekt]
4. Wozu das in echtem C#-Code nötig ist?
- In modernem C# setzen Bibliotheken wie LINQ, Entity Framework und ASP.NET Core oft auf reine Funktionen und Unveränderlichkeit.
- Unveränderlichkeit reduziert die Anzahl der "magischen" Bugs, bei denen irgendwo ein wichtiger Wert verloren geht.
- Reine Funktionen erlauben einfaches automatisches Testen (Unit-Tests), weil man für einen Test nur Ein- und Ausgabe betrachtet, ohne sich um die Außenwelt zu kümmern.
Beispiel: Arbeiten mit Strings
string s = "Hello";
string newS = s.Replace("H", "J"); // s bleibt "Hello"; newS ist "Jello"
Beispiel: LINQ und Collections
Where, Select und andere Methoden geben neue Collections zurück, ohne die alten zu verändern.
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers bleibt unverändert!
Praxisbeispiel: Konfiguration via unveränderliche Objekte
Viele moderne .NET-APIs nutzen unveränderliche Objekte für Konfiguration, z.B. JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// Dieses Objekt wird während der Laufzeit nicht "einfach so" geändert, was die Zuverlässigkeit erhöht.
5. Typische Fehler und Fallstricke
Die rutschige Stelle beginnt, wenn du "versehentlich" Daten in angeblich "reinem" Code mutierst.
Oft passiert das mit Collections: du wolltest eine Liste filtern, hast aber dabei das Original verändert.
Oder du vergisst, dass eine Methode wie List<T>.Add das Objekt in-place ändert.
Hinterhältiges Beispiel:
List<int> DoubleTheNumbers(List<int> xs)
{
// Fehler! Wir mutieren die ursprüngliche Liste und geben dasselbe Objekt zurück.
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
Dieser Code wird sogar eine Laufzeitexception (InvalidOperationException) verursachen, weil wir die Collection während der Iteration ändern — klassisches Mutationsproblem.
Richtig:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION