Domyślnie C# przekazuje każdy argument przez wartość: metoda dostaje kopię, a przypisanie do parametru nie wpływa na wywołującego. Modyfikatory ref, out i in przekazują zamiast tego samą zmienną wywołującego.
Wynik:
by value: 50
by ref: 60
Słowo kluczowe pojawia się dwa razy: na liście parametrów (ref int score) i w miejscu wywołania (ref score). Napisanie go przy wywołaniu jest obowiązkowe i celowe: każdy, kto czyta AddPoints(ref score), wie, że score może się zmienić. Jeśli o nim zapomnisz, kompilator zgłosi CS1620, Argument 1 must be passed with the 'ref' keyword.
ref: odczyt i zapis zmiennej wywołującego
Parametr ref to alias zmiennej wywołującego. Metoda może odczytać bieżącą wartość i ją zastąpić. Klasyczny przykład to zamiana wartości (swap), niemożliwa przy zwykłych parametrach:
Wynik:
Linus Ada
volume 100
Zasady dla ref:
- Argument musi być zmienną (lokalną, parametrem, polem albo elementem tablicy), a nie literałem czy wyrażeniem. Właściwość też się nie nadaje:
Clamp(ref player.Volume, 0, 100)to błąd CS0206,A non ref-returning property or indexer may not be used as an out or ref value. Skopiuj właściwość do zmiennej lokalnej, przekaż ją, a potem przypisz z powrotem. - Zmienna musi być na pewno przypisana przed wywołaniem.
int n; Clamp(ref n, 0, 10);to CS0165,Use of unassigned local variable 'n'.
out: dodatkowe wyniki
out służy do wartości, które metoda wytwarza. Wywołujący nie musi inicjalizować zmiennej, a metoda musi ją przypisać na każdej ścieżce przed powrotem:
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];
}
Wewnątrz metody parametr out jest traktowany jako nieprzypisany, dopóki do niego nie zapiszesz, więc nie możesz odczytać starej wartości wywołującego. Na tym polega różnica względem ref: ref działa w obie strony, a out tylko w jedną, od metody do wywołującego.
Wzorzec TryParse
Najbardziej znane zastosowanie out to wzorzec Try z biblioteki standardowej: zwróć bool oznaczający sukces, a wynik umieść w parametrze out, dzięki czemu złe dane wejściowe nigdy nie rzucają wyjątku:
Wynik:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
out var quantity (C# 7.0) deklaruje zmienną bezpośrednio w wywołaniu; jej typ pochodzi z parametru. Przy niepowodzeniu TryParse ustawia ją na 0 i zwraca false. Gdy interesuje cię tylko to, czy parsowanie się udało, odrzuć wartość przez out _: bool isNumber = int.TryParse(input, out _);.
Zwróć uwagę, że TryGetDiscount przypisuje rate na obu ścieżkach: raz jawnie, a raz przekazując go do Dictionary.TryGetValue, które samo jest wywołaniem z out.
out a zwracanie krotki
Przed C# 7 out było głównym sposobem na zwrócenie kilku wartości. Teraz krotki robią to bardziej bezpośrednio:
// 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);
Do zwykłych wielu wyników używaj krotki (albo małej klasy). out zostaw dla wzorca Try, w którym zwracany bool pozwala umieścić wywołanie w warunku if.
Typy referencyjne: zmiana obiektu a jego zastąpienie
Przekazanie instancji klasy przez wartość już pozwala metodzie zmienić obiekt, bo kopiowaną wartością jest referencja do tego samego obiektu. ref dodaje jeszcze jedną możliwość: zastąpienie obiektu, na który wskazuje zmienna wywołującego.
Wynik:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue skierował swoją własną kopię referencji na nową listę; wywołujący nigdy tego nie zobaczył. ResetByRef zmienił samą zmienną wywołującego. W codziennym kodzie rzadko potrzebujesz ref przy parametrze klasy: zamiast tego zwróć nowy obiekt.
Dla struktur różnica jest większa, bo argument będący strukturą jest kopiowany pole po polu. Metoda, która musi zmodyfikować strukturę wywołującego (Point, dużą strukturę ustawień), potrzebuje ref.
in: tylko do odczytu przez referencję
C# 7.2 dodał in, który przekazuje przez referencję, ale zabrania przypisania. Jego celem jest wydajność: duża struktura nie jest kopiowana przy każdym wywołaniu, a wywołujący ma gwarancję, że się nie zmieni.
// 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
}
W miejscu wywołania in jest opcjonalne: działają zarówno Trace(matrix), jak i Trace(in matrix). Dla int, double, DateTime i innych małych typów in nic nie daje, a może być nieco wolniejsze; używaj go dla struktur z kilkoma polami, które często przekazujesz.
Ograniczenia
- Parametry
refioutnie mogą być opcjonalne (CS1741). Parametrinmoże:static void Connect(in int retries = 3)jest poprawne. - Przeciążenia nie mogą różnić się tylko
refiout(lubin), bo na poziomie środowiska uruchomieniowego są tym samym.F(int x)iF(ref int x)mogą współistnieć. - Metody
asynci iteratory (metody zyield return) nie mogą mieć parametrówref,outaniin. Zwracaj z nich krotkę albo obiekt wyniku. - Lambda ani funkcja lokalna nie może przechwycić parametru
ref,outaniinmetody otaczającej; najpierw skopiuj go do zmiennej lokalnej.
Porównanie ref, out i in
| Wywołujący musi najpierw przypisać | Metoda musi przypisać | Metoda może czytać | Słowo kluczowe przy wywołaniu | |
|---|---|---|---|---|
| (brak) | tak | nie | tak (kopię) | brak |
ref | tak | nie | tak | wymagane |
out | nie | tak, na każdej ścieżce | dopiero po przypisaniu | wymagane |
in | tak | niedozwolone | tak | opcjonalne |
Najczęściej zadawane pytania
Jaka jest różnica między ref a out w C#?
Oba przekazują samą zmienną wywołującego, a nie jej kopię. Przy ref zmienna musi być przypisana przed wywołaniem, a metoda może ją czytać i zmieniać. Przy out zmienna nie musi być wcześniej przypisana, metoda nie może jej odczytać przed zapisem i musi ją przypisać przed powrotem (w przeciwnym razie CS0177). out oznacza "to jest dodatkowy wynik".
Co oznacza out var w C#?
Od C# 7.0 możesz zadeklarować zmienną out wewnątrz wywołania: if (int.TryParse(text, out var number)). Typ zmiennej jest wywnioskowany z parametru, a zmienna jest dostępna także po instrukcji. out _ odrzuca wynik, którego nie potrzebujesz.
Czy obiekty są przekazywane przez referencję w C#?
Nie, każdy argument jest przekazywany przez wartość, chyba że napiszesz ref, out lub in. Dla klasy kopiowaną wartością jest referencja, więc metoda może zmienić pola obiektu, a wywołujący to zobaczy. Bez ref nie może natomiast sprawić, by zmienna wywołującego wskazywała inny obiekt.
Czym jest modyfikator parametru in w C#?
in (C# 7.2 i nowsze) przekazuje argument przez referencję, ale tylko do odczytu: metoda nie może do niego przypisać. Istnieje po to, by nie kopiować dużych struktur przy każdym wywołaniu. Dla małych typów, takich jak int czy DateTime, nie daje żadnej korzyści.
Skąd błąd "Argument 1 must be passed with the 'ref' keyword"?
Metoda deklaruje parametr ref, a w wywołaniu pominięto słowo kluczowe. C# wymaga ref (i out) także w miejscu wywołania, żeby czytający widział, że zmienna może się zmienić: Increment(ref count);, a nie Increment(count);.