Standardmäßig übergibt C# jedes Argument als Wert: Die Methode bekommt eine Kopie, und eine Zuweisung an den Parameter wirkt sich nicht auf den Aufrufer aus. Die Modifizierer ref, out und in übergeben stattdessen die Variable des Aufrufers selbst.
Ausgabe:
by value: 50
by ref: 60
Das Schlüsselwort steht zweimal da: in der Parameterliste (ref int score) und an der Aufrufstelle (ref score). An der Aufrufstelle ist es Pflicht, und das ist Absicht: Wer AddPoints(ref score) liest, weiß, dass sich score ändern kann. Vergisst du es, meldet der Compiler CS1620, Argument 1 must be passed with the 'ref' keyword.
ref: die Variable des Aufrufers lesen und schreiben
Ein ref-Parameter ist ein Alias für die Variable des Aufrufers. Die Methode kann den aktuellen Wert lesen und ersetzen. Das klassische Beispiel ist ein Tausch, der mit normalen Parametern unmöglich ist:
Ausgabe:
Linus Ada
volume 100
Regeln für ref:
- Das Argument muss eine Variable sein (eine lokale Variable, ein Parameter, ein Feld oder ein Array-Element), kein Literal und kein Ausdruck. Auch eine Property zählt nicht:
Clamp(ref player.Volume, 0, 100)ist Fehler CS0206,A non ref-returning property or indexer may not be used as an out or ref value. Kopiere die Property in eine lokale Variable, übergib diese und weise sie zurück. - Die Variable muss vor dem Aufruf definitiv zugewiesen sein.
int n; Clamp(ref n, 0, 10);ist CS0165,Use of unassigned local variable 'n'.
out: zusätzliche Ergebnisse
out ist für Werte, die die Methode erzeugt. Der Aufrufer muss die Variable nicht initialisieren, und die Methode muss ihr auf jedem Pfad vor der Rückkehr einen Wert zuweisen:
static void Split(string fullName, out string first, out string last)
{
// error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
first = fullName.Split(' ')[0];
}
Innerhalb der Methode gilt ein out-Parameter als nicht zugewiesen, bis du ihn beschreibst, du kannst also den alten Wert des Aufrufers nicht lesen. Das ist der Unterschied zu ref: ref geht in beide Richtungen, out nur von der Methode zum Aufrufer.
Das TryParse-Muster
Der bekannteste Einsatz von out ist das Try-Muster der Basisbibliothek: bool für den Erfolg zurückgeben und das Ergebnis in einen out-Parameter legen, damit fehlerhafte Eingaben nie werfen:
Ausgabe:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
out var quantity (C# 7.0) deklariert die Variable direkt im Aufruf; ihr Typ kommt aus dem Parameter. Bei einem Fehlschlag setzt TryParse sie auf 0 und gibt false zurück. Wenn dich nur interessiert, ob das Parsen gelingt, verwirf den Wert mit out _: bool isNumber = int.TryParse(input, out _);.
Beachte, wie TryGetDiscount rate auf beiden Pfaden zuweist: einmal explizit und einmal durch Weiterreichen an Dictionary.TryGetValue, das selbst ein out-Aufruf ist.
out gegenüber einem zurückgegebenen Tupel
Vor C# 7 war out der wichtigste Weg, mehrere Werte zurückzugeben. Tupel erledigen das heute direkter:
// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);
// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);
Nimm ein Tupel (oder eine kleine Klasse) für einfache mehrfache Ergebnisse. Behalte out für das Try-Muster, bei dem die bool-Rückgabe den Aufruf in einer if-Bedingung stehen lässt.
Referenztypen: das Objekt ändern oder ersetzen
Eine Klasseninstanz als Wert zu übergeben erlaubt der Methode bereits, das Objekt zu ändern, weil der kopierte Wert eine Referenz auf dasselbe Objekt ist. ref fügt eine weitere Fähigkeit hinzu: das Objekt zu ersetzen, auf das die Variable des Aufrufers zeigt.
Ausgabe:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue hat seine eigene Kopie der Referenz auf eine neue Liste zeigen lassen; der Aufrufer hat davon nie etwas gesehen. ResetByRef hat die Variable des Aufrufers selbst geändert. Im Alltagscode brauchst du ref bei einem Klassenparameter selten: Gib stattdessen das neue Objekt zurück.
Bei Structs ist der Unterschied größer, da ein Struct-Argument Feld für Feld kopiert wird. Eine Methode, die einen Struct des Aufrufers ändern muss (einen Point, einen großen Struct mit Einstellungen), braucht ref.
in: schreibgeschützt per Referenz
C# 7.2 hat in eingeführt, das per Referenz übergibt, aber Zuweisungen verbietet. Der Zweck ist Performance: Ein großer Struct wird nicht bei jedem Aufruf kopiert, und der Aufrufer hat die Garantie, dass er sich nicht ändert.
// C# 7.2 and later
struct Matrix4
{
public double M11, M12, M13, M14, M21, M22, M23, M24,
M31, M32, M33, M34, M41, M42, M43, M44;
}
static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;
static void Reset(in Matrix4 m)
{
m.M11 = 0;
// error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}
An der Aufrufstelle ist in optional: Trace(matrix) und Trace(in matrix) funktionieren beide. Bei int, double, DateTime und anderen kleinen Typen bringt in nichts und kann sogar etwas langsamer sein; nimm es für Structs mit mehreren Feldern, die du oft übergibst.
Einschränkungen
- Parameter mit
refundoutkönnen nicht optional sein (CS1741). Einin-Parameter schon:static void Connect(in int retries = 3)ist erlaubt. - Überladungen können sich nicht nur durch
refgegenüberout(oderin) unterscheiden, weil sie auf Ebene der Runtime identisch sind.F(int x)undF(ref int x)können nebeneinander existieren. async-Methoden und Iteratoren (Methoden mityield return) können keine Parameter mitref,outoderinhaben. Gib aus ihnen stattdessen ein Tupel oder ein Ergebnisobjekt zurück.- Ein Lambda oder eine lokale Funktion kann keinen Parameter der umgebenden Methode mit
ref,outoderinerfassen; kopiere ihn zuerst in eine lokale Variable.
ref, out und in im Vergleich
| Aufrufer muss vorher zuweisen | Methode muss zuweisen | Methode darf lesen | Schlüsselwort beim Aufruf | |
|---|---|---|---|---|
| (keiner) | ja | nein | ja (eine Kopie) | keins |
ref | ja | nein | ja | Pflicht |
out | nein | ja, auf jedem Pfad | erst nach der Zuweisung | Pflicht |
in | ja | nicht erlaubt | ja | optional |
Häufig gestellte Fragen
Was ist der Unterschied zwischen ref und out in C#?
Beide übergeben die Variable des Aufrufers selbst statt einer Kopie. Bei ref muss die Variable vor dem Aufruf zugewiesen sein, und die Methode darf sie lesen und ändern. Bei out muss die Variable vorher nicht zugewiesen sein, die Methode kann sie nicht vor dem Schreiben lesen und muss ihr vor der Rückkehr einen Wert zuweisen (sonst CS0177). out bedeutet „das ist ein zusätzliches Ergebnis“.
Was bedeutet out var in C#?
Seit C# 7.0 kannst du die out-Variable im Aufruf deklarieren: if (int.TryParse(text, out var number)). Der Typ der Variable wird aus dem Parameter abgeleitet, und sie ist auch nach der Anweisung gültig. out _ verwirft ein Ergebnis, das du nicht brauchst.
Werden Objekte in C# per Referenz übergeben?
Nein, jedes Argument wird als Wert übergeben, außer du schreibst ref, out oder in. Bei einer Klasse ist der kopierte Wert eine Referenz, die Methode kann also die Felder des Objekts ändern, und der Aufrufer sieht das. Was sie ohne ref nicht kann: die Variable des Aufrufers auf ein anderes Objekt zeigen lassen.
Was ist der Parametermodifizierer in in C#?
in (ab C# 7.2) übergibt ein Argument per Referenz, aber schreibgeschützt: Die Methode kann ihm nichts zuweisen. Es existiert, um das Kopieren großer Structs bei jedem Aufruf zu vermeiden. Bei kleinen Typen wie int oder DateTime bringt es nichts.
Warum bekomme ich „Argument 1 must be passed with the 'ref' keyword“?
Die Methode deklariert einen ref-Parameter, und der Aufruf hat das Schlüsselwort weggelassen. C# verlangt ref (und out) auch an der Aufrufstelle, damit Leser sehen, dass sich die Variable ändern kann: Increment(ref count);, nicht Increment(count);.