CodeGym /Kurse /C# SELF /Praktische Anwendung von FP in C#

Praktische Anwendung von FP in C#

C# SELF
Level 51 , Lektion 4
Verfügbar

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 CollectionsLINQ, 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
foreach + if + Add
.Where(predicate)
Liste transformieren
foreach + Berechnungen + Add
.Select(lambda)
Suche nach Kriterium
foreach + if/return
.FirstOrDefault(predicate)
Aggregation
Schleife mit Zählervariable
.Aggregate(seed, func)
Caching
manuelles Dictionary + Checks
Funktion mit closure

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.

1
Umfrage/Quiz
Funktionale Programmierung, Level 51, Lektion 4
Nicht verfügbar
Funktionale Programmierung
Einführung in die funktionale Programmierung
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION