Kiedy używać ref, a kiedy nie jest to konieczne w C #

104

Mam obiekt, który jest moim stanem pamięci programu, a także kilka innych funkcji roboczych, do których przekazuję obiekt, aby zmodyfikować stan. Przekazywałem to przez odwołanie do funkcji robotniczych. Jednak natrafiłem na następującą funkcję.

byte[] received_s = new byte[2048];
IPEndPoint tmpIpEndPoint = new IPEndPoint(IPAddress.Any, UdpPort_msg);
EndPoint remoteEP = (tmpIpEndPoint);

int sz = soUdp_msg.ReceiveFrom(received_s, ref remoteEP); 

To mnie dezorientuje, ponieważ oba received_si remoteEPzwracają rzeczy z funkcji. Dlaczego remoteEPpotrzebujemy refi received_snie robi?

Jestem też programistą AC, więc mam problem z wyrzuceniem wskaźników z mojej głowy.

Edycja: wygląda na to, że obiekty w C # są wskaźnikami do obiektu pod maską. Więc kiedy przekazujesz obiekt do funkcji, możesz następnie zmodyfikować zawartość obiektu za pomocą wskaźnika, a jedyną rzeczą przekazaną do funkcji jest wskaźnik do obiektu, aby sam obiekt nie był kopiowany. Używasz ref lub out, jeśli chcesz mieć możliwość przełączania się lub tworzenia nowego obiektu w funkcji, który jest podobny do podwójnego wskaźnika.

Rex Logan
źródło

Odpowiedzi:

217

Krótka odpowiedź: przeczytaj mój artykuł o przekazywaniu argumentów .

Długa odpowiedź: gdy parametr typu odwołania jest przekazywany przez wartość, przekazywane jest tylko odwołanie, a nie kopia obiektu. Jest to podobne do przekazywania wskaźnika (według wartości) w C lub C ++. Zmiany wartości samego parametru nie będą widoczne dla wywołującego, ale zmiany w obiekcie, na który wskazuje odniesienie, będą widoczne.

Gdy parametr (dowolnego rodzaju) jest przekazywany przez referencję, oznacza to, że wszelkie zmiany parametru są widoczne dla wywołującego - zmiany parametru zmianami w zmiennej.

Oczywiście artykuł wyjaśnia to wszystko bardziej szczegółowo :)

Przydatna odpowiedź: prawie nigdy nie musisz używać ref / out . Jest to po prostu sposób na uzyskanie kolejnej wartości zwracanej i zwykle należy go unikać właśnie dlatego, że oznacza to, że metoda prawdopodobnie próbuje zrobić za dużo. Nie zawsze tak jest ( TryParseitp. Są kanonicznymi przykładami rozsądnego użycia out), ale używanie ref / out powinno być stosunkowo rzadkie.

Jon Skeet
źródło
38
Myślę, że pomieszałeś swoją krótką i długą odpowiedź; to duży artykuł!
Outlaw Programmer
23
@Outlaw: Tak, ale sama krótka odpowiedź, dyrektywa do przeczytania artykułu, ma tylko 6 słów :)
Jon Skeet
27
Referencja! :)
gonzobrains
5
@Liam Korzystanie ref jak ty może zobaczyć bardziej wyraźny do ciebie, ale może faktycznie być mylące dla innych programistów (ci, którzy wiedzą, co to słowo robi tak), ponieważ jesteś w istocie mówi potencjalnych rozmówców, „mogę zmodyfikować zmienną ty używany w metodzie wywołującej, tj. przypisz go do innego obiektu (lub nawet do wartości null, jeśli to możliwe), więc nie trzymaj się tego, lub upewnij się, że sprawdzasz go, kiedy skończę z tym. To dość mocne i zupełnie inne od „tego obiektu można modyfikować”, co zawsze ma miejsce za każdym razem, gdy przekazujesz odwołanie do obiektu jako parametr.
mbargiel
1
@ M.Mimpen: C # 7 (miejmy nadzieję) pozwoliif (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet
26

Pomyśl o parametrze innym niż ref jako wskaźniku, a parametrze ref jako o podwójnym wskaźniku. To mi najbardziej pomogło.

Prawie nigdy nie powinieneś przekazywać wartości przez ref. Podejrzewam, że gdyby nie problemy związane z interopem, zespół .Net nigdy nie umieściłby tego w oryginalnej specyfikacji. Sposobem OO rozwiązania większości problemów, które rozwiązują parametry ref, jest:

Dla wielu wartości zwracanych

  • Utwórz struktury, które reprezentują wiele zwracanych wartości

Dla prymitywów, które zmieniają się w metodzie w wyniku wywołania metody (metoda ma skutki uboczne w parametrach pierwotnych)

  • Zaimplementuj metodę w obiekcie jako metodę instancji i manipuluj stanem obiektu (nie parametrami) w ramach wywołania metody
  • Użyj rozwiązania z wieloma wartościami zwracanymi i scal wartości zwracane ze swoim stanem
  • Utwórz obiekt, który zawiera stan, którym można manipulować metodą, i przekaż ten obiekt jako parametr, a nie same prymitywy.
Michael Meadows
źródło
8
Mój Boże. Musiałbym przeczytać to 20x, żeby to zrozumieć. Brzmi jak mnóstwo dodatkowej pracy, aby zrobić coś prostego.
PositiveGuy,
Platforma .NET nie jest zgodna z regułą nr 1. („W przypadku wielu zwracanych wartości utwórz struktury”). Weźmy na przykład IPAddress.TryParse(string, out IPAddress).
Swen Kooij
@SwenKooij Masz rację. Zakładam, że w większości miejsc, w których używają parametrów, albo (a) pakują API Win32, albo (b) było to wczesne dni, a programiści C ++ podejmowali decyzje.
Michael Meadows,
@SwenKooij Wiem, że ta odpowiedź jest spóźniona wiele lat, ale tak jest. Jesteśmy przyzwyczajeni do TryParse, ale to nie znaczy, że jest dobry. Byłoby lepiej, gdybyśmy zamiast if (int.TryParse("123", out var theInt) { /* use theInt */ }mieli. var candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }Jest więcej kodu, ale jest znacznie bardziej spójny z projektem języka C #.
Michael Meadows
9

Prawdopodobnie możesz napisać całą aplikację C # i nigdy nie przekazywać żadnych obiektów / struktur przez ref.

Miałem profesora, który powiedział mi to:

Jedyne miejsce, w którym użyjesz referencji, to miejsce, w którym:

  1. Chcesz przekazać duży obiekt (tj. Obiekty / struktura ma w sobie obiekty / struktury na wiele poziomów), a skopiowanie go byłoby kosztowne i
  2. Wzywasz Framework, Windows API lub inny API, który tego wymaga.

Nie rób tego tylko dlatego, że możesz. Możesz zostać ugryziony w tyłek przez jakieś paskudne błędy, jeśli zaczniesz zmieniać wartości w parametrze i nie zwracasz na to uwagi.

Zgadzam się z jego radą i przez ponad pięć lat, które minęły od szkoły, nigdy nie potrzebowałem tego poza wywoływaniem Framework lub Windows API.

Chris
źródło
3
Jeśli planujesz zaimplementować opcję „Zamień”, pomocne może być podanie obok ref.
Brian
@Chris czy będzie jakiś problem, jeśli użyję słowa kluczowego ref dla małych obiektów?
ManirajSS
@TechnikEmpire "Ale zmiany obiektu w zakresie wywoływanej funkcji nie są odzwierciedlane z powrotem do wywołującego." To jest złe. Jeśli przekażę Person do SetName(person, "John Doe"), właściwość name ulegnie zmianie i ta zmiana zostanie odzwierciedlona dla dzwoniącego.
M. Mimpen,
@ M.Mimpen Komentarz usunięty. W tym momencie ledwo opanowałem C # i wyraźnie mówiłem o moich * $$. Dziękuję za zwrócenie mi na to uwagi.
@Chris - Jestem pewien, że przekazanie „dużego” obiektu nie jest drogie. Jeśli po prostu przekazujesz to przez wartość, nadal przekazujesz tylko pojedynczy wskaźnik, prawda?
Ian
3

Ponieważ otrzymane_s jest tablicą, przekazujesz wskaźnik do tej tablicy. Funkcja manipuluje istniejącymi danymi w miejscu, nie zmieniając podstawowej lokalizacji ani wskaźnika. Słowo kluczowe ref oznacza, że ​​przekazujesz rzeczywisty wskaźnik do lokalizacji i aktualizujesz ten wskaźnik w funkcji zewnętrznej, więc wartość w funkcji zewnętrznej ulegnie zmianie.

Np. Tablica bajtów jest wskaźnikiem do tej samej pamięci przed i po, pamięć została właśnie zaktualizowana.

Odwołanie do punktu końcowego w rzeczywistości aktualizuje wskaźnik do punktu końcowego w funkcji zewnętrznej do nowego wystąpienia wygenerowanego wewnątrz funkcji.

Chris Hynes
źródło
3

Pomyśl o odnośniku jako o tym, że przekazujesz wskaźnik przez odniesienie. Nieużywanie ref oznacza, że ​​przekazujesz wskaźnik według wartości.

Jeszcze lepiej, zignoruj ​​to, co właśnie powiedziałem (prawdopodobnie wprowadza w błąd, szczególnie w przypadku typów wartości) i przeczytaj tę stronę MSDN .

Brian
źródło
Właściwie to nieprawda. Przynajmniej druga część. Każdy typ odwołania będzie zawsze przekazywany przez odwołanie, niezależnie od tego, czy używasz ref, czy nie.
Erik Funkenbusch
Właściwie, po dalszej refleksji, nie wyszło to dobrze. W rzeczywistości odwołanie jest przekazywane przez wartość bez typu ref. Oznacza to, że zmiana wartości wskazywanych przez odniesienie zmienia oryginalne dane, ale sama zmiana odniesienia nie zmienia oryginalnego odniesienia.
Erik Funkenbusch
2
Typ odniesienia inny niż odniesienie nie jest przekazywany przez odwołanie. Odwołanie do typu odwołania jest przekazywane przez wartość. Ale jeśli chcesz myśleć o odwołaniu jako o wskaźniku do czegoś, do czego się odwołujesz, to, co powiedziałem, ma sens (ale myślenie w ten sposób może być mylące). Stąd moje ostrzeżenie.
Brian
Link nie działa - zamiast tego wypróbuj ten - docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
GIVE-ME-CHICKEN
0

rozumiem, że wszystkie obiekty wywodzące się z klasy Object są przekazywane jako wskaźniki, podczas gdy typy zwykłe (int, struct) nie są przekazywane jako wskaźniki i wymagają ref. Nie jestem pewien co do ciągu (czy ostatecznie pochodzi z klasy Object?)

Lucas
źródło
Może to wymagać wyjaśnienia. Jaka jest różnica między typami wartości i referencji. Ans, dlaczego jest to istotne w przypadku użycia słowa kluczowego ref w parametrze?
oɔɯǝɹ
W .net wszystko oprócz wskaźników, parametrów typu i interfejsów pochodzi z Object. Ważne jest, aby zrozumieć, że „zwykłe” typy (poprawnie nazywane „typami wartości”) również dziedziczą po obiekcie. Masz rację od tego momentu: typy wartości (domyślnie) są przekazywane przez wartość, podczas gdy typy referencyjne są przekazywane przez odwołanie. Jeśli chciałbyś zmodyfikować typ wartości zamiast zwracać nowy (traktować go jak typ referencyjny wewnątrz metody), musiałbyś użyć na nim słowa kluczowego ref. Ale to zły styl i po prostu nie powinieneś tego robić, chyba że jesteś absolutnie pewien, że musisz :)
buddybubble
1
odpowiedzieć na pytanie: Ciąg pochodzi z object.It to rodzaj odniesienia, który zachowuje się jak typ wartości (dla wydajności i ze względów logicznych)
buddybubble
0

Chociaż ogólnie zgadzam się z odpowiedzią Jona Skeeta i niektórymi innymi odpowiedziami, istnieje przypadek użycia ref, który służy do zaostrzenia optymalizacji wydajności. Podczas profilowania wydajności zaobserwowano, że ustawienie wartości zwracanej metody ma niewielki wpływ na wydajność, podczas gdy użycie refjako argumentu, w którym wartość zwracana jest umieszczana w tym parametrze, powoduje usunięcie tego niewielkiego wąskiego gardła.

Jest to naprawdę przydatne tylko wtedy, gdy wysiłki optymalizacyjne są podejmowane na skrajnych poziomach, poświęcając czytelność, a być może testowalność i łatwość konserwacji, aby zaoszczędzić milisekundy lub może ułamek milisekund.

Jon Davis
źródło
-1

Najpierw reguła punktu zerowego, prymitywy są przekazywane przez wartość (stos), a nie-prymitywne przez odwołanie (sterta) w kontekście zaangażowanych TYPÓW.

Uwzględnione parametry są domyślnie przekazywane przez wartość. Dobry post, który szczegółowo wyjaśnia wszystko. http://yoda.arachsys.com/csharp/parameters.html

Student myStudent = new Student {Name="A",RollNo=1};

ChangeName(myStudent);

static void ChangeName(Student s1)
{
  s1.Name = "Z"; // myStudent.Name will also change from A to Z
                // {AS s1 and myStudent both refers to same Heap(Memory)
                //Student being the non-Primitive type
}

ChangeNameVersion2(ref myStudent);
static void ChangeNameVersion2(ref Student s1)
{
  s1.Name = "Z"; // Not any difference {same as **ChangeName**}
}

static void ChangeNameVersion3(ref Student s1)
{
    s1 = new Student{Name="Champ"};

    // reference(myStudent) will also point toward this new Object having new memory
    // previous mystudent memory will be released as it is not pointed by any object
}

Możemy powiedzieć (z ostrzeżeniem) Typy niebędące prymitywami to nic innego jak wskaźniki A kiedy mijamy je przez ref możemy powiedzieć, że przechodzimy przez Double Pointer

Surender Singh Malik
źródło
Wszystkie parametry są domyślnie przekazywane według wartości w języku C #. Każdy parametr może być przekazany przez odwołanie. W przypadku typów referencyjnych wartość, która jest przekazywana (przez odwołanie lub przez wartość), sama jest odwołaniem. To jest całkowicie niezależne od tego, jak jest przekazywane.
Servy
Zgadzam się @Servy! „Kiedy słyszymy użyte słowa„ odniesienie ”lub„ wartość ”, powinniśmy być bardzo jasne, czy mamy na myśli, że parametr jest parametrem referencyjnym lub wartościowym, czy też mamy na myśli, że dany typ jest odniesieniem lub typem wartości„ Mały zamieszanie tam po mojej stronie, kiedy powiedziałem, Ground Zero zasada pierwsza, prymitywów są przekazywane przez wartość (stos) i Non-Primitive odnośnikiem (heap) Chodziło mi do typów uczestniczących, a nie parametrów! kiedy mówimy o parametry, które są prawidłowe , Wszystkie parametry są przekazywane przez wartość, domyślnie w języku C #.
Surender Singh Malik
Mówienie, że parametr jest parametrem referencyjnym lub wartościowym, nie jest terminem standardowym i jest naprawdę niejednoznaczne, co masz na myśli. Typ parametru może być typem referencyjnym lub typem wartości. Każdy parametr może być przekazywany przez wartość lub przez odwołanie. Są to pojęcia ortogonalne dla dowolnego parametru. Twój post błędnie to opisuje.
Servy
1
Przekazujesz parametry przez odniesienie lub wartość. Terminy „według odniesienia” i „według wartości” nie są używane do opisania, czy typ jest typem wartościowym, czy typem odniesienia.
Servy
1
Nie, to nie jest kwestia percepcji. Kiedy używasz niewłaściwego terminu do odniesienia się do czegoś, to stwierdzenie staje się błędne . Dla poprawnych stwierdzeń ważne jest, aby używać poprawnej terminologii w odniesieniu do pojęć.
Servy