CodeGym /Kurse /C# SELF /Reine Funktionen und Unveränderlichkeit

Reine Funktionen und Unveränderlichkeit

C# SELF
Level 51 , Lektion 1
Verfügbar

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

  1. Determinismus: dieselbe Eingabe — dasselbe Ergebnis.
  2. 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;
}
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION