1. Einführung
Stell dir vor, wir haben eine Funktion, die für einen übergebenen User (zum Beispiel per Name) direkt Alter, Registrierungsdatum und einen Aktiv-Flag zurückgeben soll. In C und frühen Versionen von C# war es normal, dass der Rückgabetyp einer Funktion nur einen Wert enthält. Was tun, wenn es mehrere Werte sein sollen? Diese Frage hat verschiedene Ansätze hervorgebracht, jeder mit eigenen Vor- und Nachteilen.
In den vorherigen Vorlesungen haben wir schon Tupel kennengelernt. Jetzt schauen wir uns out-Parameter an und vergleichen diese beiden Ansätze.
Die beliebtesten Ansätze:
- Verwendung von out-Parametern.
- Ein Objekt mit den nötigen Feldern zurückgeben (anonymer Typ oder eigene Klasse).
- Ein Tupel (ValueTuple) zurückgeben.
In dieser Vorlesung schauen wir uns zwei dieser Ansätze genauer an – out-Parameter und Tupel – und vergleichen sie direkt, damit du die Vorteile jedes Ansatzes beim Zurückgeben mehrerer Werte aus einer Funktion verstehst.
2. out-Parameter: ein Gruß aus der Vergangenheit
Wenn du eine Funktion wie diese siehst:
void GetUserInfo(string userName, out int age, out DateTime registrationDate, out bool isActive)
{
// hier Berechnungen
age = 42;
registrationDate = new DateTime(2010, 1, 1);
isActive = true;
}
dann fragt man sich: Ist das eine Funktion oder eine kleine Autowaschanlage? So viele Sachen werden nach außen zurückgegeben, und alles am eigentlichen Rückgabewert vorbei!
Wie das funktioniert
Der aufrufende Code muss vorher Variablen deklarieren, die die Funktion dann befüllt:
int age;
DateTime reg;
bool isActive;
GetUserInfo("Bob", out age, out reg, out isActive);
// Jetzt sind alle Variablen befüllt
Console.WriteLine($"{age}, {reg}, {isActive}");
Nachteile des out-Ansatzes
- Schwerer zu lesen: Die Methodensignatur wird sehr breit, und die Bedeutung der Rückgabewerte ist nicht immer klar aus den Parameternamen.
- Unpraktisch bei Call-Chains: So eine Methode kann man nicht einfach in eine Kette einbauen (z.B. das Ergebnis direkt an eine andere Methode übergeben).
- Mutiert übergebene Variablen: Die Methode muss existierende Variablen verändern; vergisst du das out, gibt's einen Compilerfehler.
- Initialisierungs-Overhead: Der Compiler zwingt dich, externe Variablen zu deklarieren, selbst wenn du das Ergebnis nur einmal brauchst.
- Begrenzte Lesbarkeit bei großen Methoden: Wenn es viele out-Parameter gibt, verliert man schnell den Überblick, was wofür ist.
Hier fangen Tupel an, richtig zu glänzen.
3. Tupel — die Evolution des Ansatzes
Vergleichen wir am Beispiel
Mit Tupel:
public (int Age, DateTime RegistrationDate, bool IsActive) GetUserInfo(string userName)
{
// Datenbanksuche simulieren
return (42, new DateTime(2010, 1, 1), true);
}
Verwendung:
// Wir bekommen alle Werte auf einmal und geben ihnen Namen
var (age, regDate, isActive) = GetUserInfo("Bob");
Console.WriteLine($"{age}, {regDate}, {isActive}");
Keine unnötigen Deklarationen, kein out, alles super lesbar!
Vergleichstabelle: out vs tuple
| Kriterium | Out-Parameter | Tupel (ValueTuple) |
|---|---|---|
| Signatur | lang, out-Parameter in der Liste | kompakt, alles kommt als ein "Paket" |
| Verwendung | Variablen deklarieren, dann Methode aufrufen | kann direkt deconstructed werden |
| Lesbarkeit | geht oft verloren, wenn es viele out-Parameter gibt | Elementnamen sind sofort klar |
| Einfach kombinierbar? | nein | ja, kann verschachtelt und weitergegeben werden |
Tupel VS. out — was passiert unter der Haube
Mit out-Parametern arbeitet die Funktion eigentlich mit Speicher außerhalb ihres eigenen "Territoriums": vereinfacht gesagt, sie verändert Variablen, die woanders erzeugt wurden. Das erfordert Vorsicht, weil man versehentlich den Zustand von Variablen "vermüllen" kann (zum Glück zwingt der Compiler dich zum Zuweisen!).
Ein Tupel dagegen ist eine Datenstruktur, die die Funktion komplett initialisiert und dann zurückgibt. Alles ist ein Paket, das du unterwegs nicht verlierst und nicht vergisst.
Code lesen und pflegen
Seien wir ehrlich – wenn du nach einer Woche fremden Code anschaust, willst du lieber sowas sehen:
(var age, var city, var isActive) = GetUserInfo("Anna");
anstatt
int age;
string city;
bool isActive;
GetUserInfo("Anna", out age, out city, out isActive);
Tupel machen Methodensignaturen weniger noisy und das Ergebnis ihrer Verwendung ist sofort verständlich.
4. Situationen, in denen Tupel besonders praktisch sind
1. Wenn du Ergebnis und Fehlermeldung zurückgeben willst
public (bool Success, string ErrorMessage) TryProcess(string data)
{
if (string.IsNullOrEmpty(data))
return (false, "Keine Daten");
// Daten verarbeiten...
return (true, "");
}
var (ok, error) = TryProcess(input);
if (!ok)
Console.WriteLine($"Fehler: {error}");
2. Für Funktionen, die mehrere Suchergebnisse zurückgeben
public (User? FoundUser, int Index) FindUserByName(User[] users, string name)
{
for (int i = 0; i < users.Length; i++)
{
if (users[i].Name == name)
return (users[i], i);
}
return (null, -1);
}
3. Für “Paare” von Werten: Minimum und Maximum berechnen
public (int min, int max) FindMinMax(int[] numbers)
{
int min = numbers[0], max = numbers[0];
foreach (var n in numbers)
{
if (n < min) min = n;
if (n > max) max = n;
}
return (min, max);
}
var (minValue, maxValue) = FindMinMax(arr);
GO TO FULL VERSION